mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
* test(coverage): batch 13–14 — core/all registry, proxy_config tool (#530) Add 36 new tests: - core/all: registry integrity (no duplicates, schema-controller parity), rpc_method_name, namespace_description, validate_params, schema lookup - proxy_config: parse_scope, parse_string_list (CSV, array, errors), parse_optional_string_update, env_snapshot, proxy_json, security gates All 4096 tests pass. * test(coverage): batch 15 — memory/store/unified/helpers pure functions (#530) Add 31 new tests for helpers module: - vec_to_bytes/bytes_to_vec roundtrip, cosine_similarity (identical, orthogonal, mismatched, zero), collapse_whitespace, normalize_search_text, tokenize_search_terms, normalize_graph_entity/predicate, json_string_array, merge_unique_string_arrays, json_i64 (int/float/missing/string), recency_score (current/old/future), chunk_document_content All 4127 tests pass. * test(coverage): batch 16 — terminal, supervision, security detect, trigger history (#530) Add 27 new tests across 4 modules: - accessibility/terminal: is_text_role, is_terminal_app, looks_like_terminal_buffer, extract_terminal_input_context, noise line detection - channels/runtime/supervision: compute_max_in_flight_messages clamping - security/detect: all sandbox backend fallback paths (landlock, firejail, bubblewrap, docker on non-linux), disabled-via-enabled-false - composio/trigger_history: list_recent with limit, empty store, field validation All 4154 tests pass. * test(coverage): batch 17 — learning, update, encryption, doctor, service schemas + skills/types (#530) Add 49 new tests across 6 modules: - learning/schemas: catalog, schema validation, unknown fallback - update/schemas: check/apply schemas, optional staging_dir, controller parity - encryption/schemas: encrypt/decrypt schemas, read_required helper - doctor/schemas: report/models schemas, read_optional helper (none/null/value/error) - service/schemas: 8 lifecycle functions, restart optional params, daemon_host - skills/types: ToolResult success/error/json/mixed, serde roundtrip, ToolContent All 4203 tests pass. * test(coverage): batch 18 — accessibility/keys key-state probes (#530) Add tests for is_tab_key_down/is_escape_key_down (platform-safe), macOS-specific constant validation. All 4216 tests pass. * test(coverage): batch 19 — service/restart, memory/store/factories (#530) Add 7 new tests: - service/restart: default source/reason, whitespace trimming, empty-string defaults, RestartStatus serde, startup delay noop - memory/store/factories: effective_memory_backend_name, migration-disabled error All 4223 tests pass. * test(coverage): batch 20 — tree_summarizer schemas, subconscious schemas (#530) Add 38 new tests across 2 modules: - tree_summarizer/schemas: catalog parity, all 5 functions, param helpers (read_required, read_optional, read_optional_timestamp), type_name, namespace_input - subconscious/schemas: catalog parity, all 10 functions, required input validation, field/field_req/field_opt helpers All 4261 tests pass. --------- Co-authored-by: Steven Enamakel <31011319+senamakel@users.noreply.github.com>
This commit is contained in:
co-authored by
Steven Enamakel
parent
ec309c8f90
commit
f098cfda40
Generated
+1
-1
@@ -4307,7 +4307,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openhuman"
|
||||
version = "0.52.17"
|
||||
version = "0.52.18"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
|
||||
Generated
+1
-1
@@ -4,7 +4,7 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "OpenHuman"
|
||||
version = "0.52.16"
|
||||
version = "0.52.18"
|
||||
dependencies = [
|
||||
"env_logger",
|
||||
"log",
|
||||
|
||||
+153
@@ -474,4 +474,157 @@ mod tests {
|
||||
let err = validate_registry(®istered, &declared).expect_err("expected duplicate input");
|
||||
assert!(err.contains("duplicate required input `use_cache` in `doctor.models`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_registry_accepts_valid_registry() {
|
||||
let declared = vec![
|
||||
schema("ns1", "fn1", vec![]),
|
||||
schema("ns1", "fn2", vec![]),
|
||||
schema("ns2", "fn1", vec![]),
|
||||
];
|
||||
let registered = declared
|
||||
.iter()
|
||||
.map(|s| RegisteredController {
|
||||
schema: s.clone(),
|
||||
handler: noop_handler,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert!(validate_registry(®istered, &declared).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_method_name_formats_correctly() {
|
||||
let s = schema("memory", "doc_put", vec![]);
|
||||
assert_eq!(rpc_method_name(&s), "openhuman.memory_doc_put");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registered_controller_rpc_method_name() {
|
||||
let s = schema("billing", "get_balance", vec![]);
|
||||
let rc = RegisteredController {
|
||||
schema: s,
|
||||
handler: noop_handler,
|
||||
};
|
||||
assert_eq!(rc.rpc_method_name(), "openhuman.billing_get_balance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_description_known_namespaces() {
|
||||
assert!(namespace_description("memory").is_some());
|
||||
assert!(namespace_description("billing").is_some());
|
||||
assert!(namespace_description("config").is_some());
|
||||
assert!(namespace_description("health").is_some());
|
||||
assert!(namespace_description("voice").is_some());
|
||||
assert!(namespace_description("webhooks").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_description_unknown_returns_none() {
|
||||
assert!(namespace_description("nonexistent_xyz").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_params_accepts_valid_params() {
|
||||
let s = schema(
|
||||
"test",
|
||||
"fn",
|
||||
vec![FieldSchema {
|
||||
name: "key",
|
||||
ty: TypeSchema::String,
|
||||
comment: "a key",
|
||||
required: true,
|
||||
}],
|
||||
);
|
||||
let mut params = Map::new();
|
||||
params.insert("key".into(), Value::String("value".into()));
|
||||
assert!(validate_params(&s, ¶ms).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_params_rejects_missing_required() {
|
||||
let s = schema(
|
||||
"test",
|
||||
"fn",
|
||||
vec![FieldSchema {
|
||||
name: "key",
|
||||
ty: TypeSchema::String,
|
||||
comment: "a key",
|
||||
required: true,
|
||||
}],
|
||||
);
|
||||
let params = Map::new();
|
||||
let err = validate_params(&s, ¶ms).unwrap_err();
|
||||
assert!(err.contains("missing required param 'key'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_params_rejects_unknown_param() {
|
||||
let s = schema("test", "fn", vec![]);
|
||||
let mut params = Map::new();
|
||||
params.insert("unknown".into(), Value::Null);
|
||||
let err = validate_params(&s, ¶ms).unwrap_err();
|
||||
assert!(err.contains("unknown param 'unknown'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_params_accepts_empty_for_no_required() {
|
||||
let s = schema("test", "fn", vec![]);
|
||||
assert!(validate_params(&s, &Map::new()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_registered_controllers_is_nonempty() {
|
||||
let controllers = all_registered_controllers();
|
||||
assert!(
|
||||
controllers.len() > 50,
|
||||
"expected many controllers, got {}",
|
||||
controllers.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_controller_schemas_matches_registered_count() {
|
||||
let schemas = all_controller_schemas();
|
||||
let controllers = all_registered_controllers();
|
||||
assert_eq!(schemas.len(), controllers.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_for_rpc_method_finds_known_method() {
|
||||
let schema = schema_for_rpc_method("openhuman.health_snapshot");
|
||||
assert!(schema.is_some(), "health.snapshot should be findable");
|
||||
let s = schema.unwrap();
|
||||
assert_eq!(s.namespace, "health");
|
||||
assert_eq!(s.function, "snapshot");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_for_rpc_method_returns_none_for_unknown() {
|
||||
assert!(schema_for_rpc_method("openhuman.nonexistent_method_xyz").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_method_from_parts_finds_known() {
|
||||
let method = rpc_method_from_parts("health", "snapshot");
|
||||
assert_eq!(method.as_deref(), Some("openhuman.health_snapshot"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_method_from_parts_returns_none_for_unknown() {
|
||||
assert!(rpc_method_from_parts("fake", "method").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_rpc_methods_in_registry() {
|
||||
let controllers = all_registered_controllers();
|
||||
let mut methods: Vec<String> = controllers.iter().map(|c| c.rpc_method_name()).collect();
|
||||
let original_len = methods.len();
|
||||
methods.sort();
|
||||
methods.dedup();
|
||||
assert_eq!(
|
||||
methods.len(),
|
||||
original_len,
|
||||
"duplicate RPC methods found in registry"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,3 +103,32 @@ const KCG_EVENT_SOURCE_STATE_COMBINED_SESSION_STATE: i32 = 0;
|
||||
const KVK_TAB: u16 = 48;
|
||||
#[cfg(target_os = "macos")]
|
||||
const KVK_ESCAPE: u16 = 53;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_tab_key_down_returns_bool() {
|
||||
// Just verify it doesn't panic and returns a bool.
|
||||
let _result: bool = is_tab_key_down();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_escape_key_down_returns_bool() {
|
||||
let _result: bool = is_escape_key_down();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn input_monitoring_recheck_interval_is_positive() {
|
||||
assert!(INPUT_MONITORING_RECHECK_MS > 0);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn kvk_constants_are_correct() {
|
||||
assert_eq!(KVK_TAB, 48);
|
||||
assert_eq!(KVK_ESCAPE, 53);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,3 +80,108 @@ pub fn extract_terminal_input_context(text: &str) -> String {
|
||||
}
|
||||
fallback
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_text_role_accepts_known_roles() {
|
||||
assert!(is_text_role(Some("AXTextArea")));
|
||||
assert!(is_text_role(Some("AXTextField")));
|
||||
assert!(is_text_role(Some("AXSearchField")));
|
||||
assert!(is_text_role(Some("AXComboBox")));
|
||||
assert!(is_text_role(Some("AXEditableText")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_text_role_rejects_other_roles() {
|
||||
assert!(!is_text_role(Some("AXButton")));
|
||||
assert!(!is_text_role(Some("AXImage")));
|
||||
assert!(!is_text_role(None));
|
||||
assert!(!is_text_role(Some("")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_terminal_app_detects_known_terminals() {
|
||||
assert!(is_terminal_app(Some("iTerm2")));
|
||||
assert!(is_terminal_app(Some("Terminal")));
|
||||
assert!(is_terminal_app(Some("WezTerm")));
|
||||
assert!(is_terminal_app(Some("Alacritty")));
|
||||
assert!(is_terminal_app(Some("kitty")));
|
||||
assert!(is_terminal_app(Some("Warp")));
|
||||
assert!(is_terminal_app(Some("Ghostty")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_terminal_app_rejects_non_terminals() {
|
||||
assert!(!is_terminal_app(Some("Safari")));
|
||||
assert!(!is_terminal_app(Some("Slack")));
|
||||
assert!(!is_terminal_app(None));
|
||||
assert!(!is_terminal_app(Some("")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn looks_like_terminal_buffer_detects_shell_prompts() {
|
||||
let buffer = "line1\nline2\nline3\nline4\n$ cargo build\nCompiling...\n";
|
||||
assert!(looks_like_terminal_buffer(buffer));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn looks_like_terminal_buffer_rejects_short_text() {
|
||||
assert!(!looks_like_terminal_buffer("hello"));
|
||||
assert!(!looks_like_terminal_buffer("$ cmd"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn looks_like_terminal_buffer_detects_git_status() {
|
||||
let buffer = "line1\nline2\nline3\nline4\ngit status\nOn branch main\n";
|
||||
assert!(looks_like_terminal_buffer(buffer));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_terminal_input_context_finds_prompt_line() {
|
||||
let text = "old output\n$ ls -la\ntotal 42\nfile1.txt\nfile2.txt\n";
|
||||
let ctx = extract_terminal_input_context(text);
|
||||
assert!(ctx.contains("$ ls"), "expected prompt line, got: {ctx}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_terminal_input_context_skips_noise() {
|
||||
let text = "actual content\n• bullet point\n└── tree branch\n│ pipe\n";
|
||||
let ctx = extract_terminal_input_context(text);
|
||||
assert_eq!(ctx, "actual content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_terminal_input_context_empty_returns_empty() {
|
||||
assert!(extract_terminal_input_context("").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_terminal_input_context_all_noise_returns_empty() {
|
||||
let text = "\n\n\n";
|
||||
assert!(extract_terminal_input_context(text).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_names_is_nonempty() {
|
||||
assert!(!TERMINAL_NAMES.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_terminal_noise_line_detects_noise() {
|
||||
assert!(is_terminal_noise_line(""));
|
||||
assert!(is_terminal_noise_line(" "));
|
||||
assert!(is_terminal_noise_line("• item"));
|
||||
assert!(is_terminal_noise_line("└── branch"));
|
||||
assert!(is_terminal_noise_line("─────"));
|
||||
assert!(is_terminal_noise_line("│ pipe"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_terminal_noise_line_passes_normal_text() {
|
||||
assert!(!is_terminal_noise_line("hello world"));
|
||||
assert!(!is_terminal_noise_line("$ command"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,3 +83,39 @@ pub(crate) fn compute_max_in_flight_messages(channel_count: usize) -> usize {
|
||||
CHANNEL_MAX_IN_FLIGHT_MESSAGES,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn compute_max_in_flight_messages_zero_channels() {
|
||||
let result = compute_max_in_flight_messages(0);
|
||||
assert_eq!(result, CHANNEL_MIN_IN_FLIGHT_MESSAGES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_max_in_flight_messages_one_channel() {
|
||||
let result = compute_max_in_flight_messages(1);
|
||||
assert!(result >= CHANNEL_MIN_IN_FLIGHT_MESSAGES);
|
||||
assert!(result <= CHANNEL_MAX_IN_FLIGHT_MESSAGES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_max_in_flight_messages_many_channels() {
|
||||
let result = compute_max_in_flight_messages(100);
|
||||
assert_eq!(result, CHANNEL_MAX_IN_FLIGHT_MESSAGES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_max_in_flight_messages_clamps_to_min() {
|
||||
let result = compute_max_in_flight_messages(0);
|
||||
assert!(result >= CHANNEL_MIN_IN_FLIGHT_MESSAGES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_max_in_flight_messages_clamps_to_max() {
|
||||
let result = compute_max_in_flight_messages(usize::MAX);
|
||||
assert!(result <= CHANNEL_MAX_IN_FLIGHT_MESSAGES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,4 +278,58 @@ mod tests {
|
||||
assert_eq!(history.entries[1].metadata_id, "id-1");
|
||||
assert!(PathBuf::from(&history.current_day_file).exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_recent_with_limit_one() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let workspace = temp.path().join("workspace");
|
||||
fs::create_dir_all(&workspace).expect("workspace dir");
|
||||
|
||||
let store = ComposioTriggerHistoryStore::new(&workspace).expect("store");
|
||||
store
|
||||
.record_trigger("gmail", "NEW_MSG", "id-1", "uuid-1", &serde_json::json!({}))
|
||||
.expect("record");
|
||||
store
|
||||
.record_trigger("slack", "NEW_MSG", "id-2", "uuid-2", &serde_json::json!({}))
|
||||
.expect("record");
|
||||
|
||||
let history = store.list_recent(1).expect("list");
|
||||
assert_eq!(history.entries.len(), 1);
|
||||
assert_eq!(history.entries[0].metadata_id, "id-2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_recent_empty_store() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let workspace = temp.path().join("workspace");
|
||||
fs::create_dir_all(&workspace).expect("workspace dir");
|
||||
|
||||
let store = ComposioTriggerHistoryStore::new(&workspace).expect("store");
|
||||
let history = store.list_recent(10).expect("list");
|
||||
assert!(history.entries.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_trigger_returns_entry_with_correct_fields() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let workspace = temp.path().join("workspace");
|
||||
fs::create_dir_all(&workspace).expect("workspace dir");
|
||||
|
||||
let store = ComposioTriggerHistoryStore::new(&workspace).expect("store");
|
||||
let entry = store
|
||||
.record_trigger(
|
||||
"github",
|
||||
"PR_OPENED",
|
||||
"pr-42",
|
||||
"uuid-42",
|
||||
&serde_json::json!({"pr": 42}),
|
||||
)
|
||||
.expect("record");
|
||||
|
||||
assert_eq!(entry.toolkit, "github");
|
||||
assert_eq!(entry.trigger, "PR_OPENED");
|
||||
assert_eq!(entry.metadata_id, "pr-42");
|
||||
assert_eq!(entry.metadata_uuid, "uuid-42");
|
||||
assert!(entry.received_at_ms > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,3 +99,80 @@ fn read_optional<T: DeserializeOwned>(
|
||||
fn to_json<T: serde::Serialize>(outcome: RpcOutcome<T>) -> Result<Value, String> {
|
||||
outcome.into_cli_compatible_json()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_schemas_returns_two() {
|
||||
assert_eq!(all_controller_schemas().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_controllers_returns_two() {
|
||||
assert_eq!(all_registered_controllers().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_schema() {
|
||||
let s = schemas("report");
|
||||
assert_eq!(s.namespace, "doctor");
|
||||
assert_eq!(s.function, "report");
|
||||
assert!(s.inputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn models_schema_has_optional_use_cache() {
|
||||
let s = schemas("models");
|
||||
assert_eq!(s.function, "models");
|
||||
let use_cache = s.inputs.iter().find(|f| f.name == "use_cache");
|
||||
assert!(use_cache.is_some_and(|f| !f.required));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_function_returns_unknown() {
|
||||
let s = schemas("nonexistent");
|
||||
assert_eq!(s.function, "unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schemas_and_controllers_match() {
|
||||
let s = all_controller_schemas();
|
||||
let c = all_registered_controllers();
|
||||
for (schema, ctrl) in s.iter().zip(c.iter()) {
|
||||
assert_eq!(schema.function, ctrl.schema.function);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_optional_returns_none_for_missing() {
|
||||
let m = Map::new();
|
||||
let result: Option<bool> = read_optional(&m, "use_cache").unwrap();
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_optional_returns_none_for_null() {
|
||||
let mut m = Map::new();
|
||||
m.insert("use_cache".into(), Value::Null);
|
||||
let result: Option<bool> = read_optional(&m, "use_cache").unwrap();
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_optional_returns_some_for_value() {
|
||||
let mut m = Map::new();
|
||||
m.insert("use_cache".into(), Value::Bool(true));
|
||||
let result: Option<bool> = read_optional(&m, "use_cache").unwrap();
|
||||
assert_eq!(result, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_optional_errors_on_wrong_type() {
|
||||
let mut m = Map::new();
|
||||
m.insert("use_cache".into(), Value::String("yes".into()));
|
||||
let err = read_optional::<bool>(&m, "use_cache").unwrap_err();
|
||||
assert!(err.contains("invalid"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,3 +101,78 @@ fn read_required<T: DeserializeOwned>(params: &Map<String, Value>, key: &str) ->
|
||||
fn to_json<T: serde::Serialize>(outcome: RpcOutcome<T>) -> Result<Value, String> {
|
||||
outcome.into_cli_compatible_json()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_schemas_returns_two() {
|
||||
assert_eq!(all_controller_schemas().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_controllers_returns_two() {
|
||||
assert_eq!(all_registered_controllers().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypt_schema_requires_plaintext() {
|
||||
let s = schemas("encrypt_secret");
|
||||
assert_eq!(s.namespace, "encrypt");
|
||||
assert_eq!(s.function, "secret");
|
||||
assert_eq!(s.inputs.len(), 1);
|
||||
assert!(s.inputs[0].required);
|
||||
assert_eq!(s.inputs[0].name, "plaintext");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_schema_requires_ciphertext() {
|
||||
let s = schemas("decrypt_secret");
|
||||
assert_eq!(s.namespace, "decrypt");
|
||||
assert_eq!(s.function, "secret");
|
||||
assert_eq!(s.inputs.len(), 1);
|
||||
assert!(s.inputs[0].required);
|
||||
assert_eq!(s.inputs[0].name, "ciphertext");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_function_returns_unknown() {
|
||||
let s = schemas("nonexistent");
|
||||
assert_eq!(s.function, "unknown");
|
||||
assert_eq!(s.namespace, "encryption");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schemas_and_controllers_match() {
|
||||
let s = all_controller_schemas();
|
||||
let c = all_registered_controllers();
|
||||
assert_eq!(s.len(), c.len());
|
||||
for (schema, ctrl) in s.iter().zip(c.iter()) {
|
||||
assert_eq!(schema.function, ctrl.schema.function);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_required_parses_string() {
|
||||
let mut m = Map::new();
|
||||
m.insert("key".into(), Value::String("value".into()));
|
||||
let result: String = read_required(&m, "key").unwrap();
|
||||
assert_eq!(result, "value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_required_errors_on_missing_key() {
|
||||
let m = Map::new();
|
||||
let err = read_required::<String>(&m, "key").unwrap_err();
|
||||
assert!(err.contains("missing required param"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_required_errors_on_wrong_type() {
|
||||
let mut m = Map::new();
|
||||
m.insert("key".into(), Value::Bool(true));
|
||||
let err = read_required::<String>(&m, "key").unwrap_err();
|
||||
assert!(err.contains("invalid"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,43 @@ pub fn learning_schemas(function: &str) -> ControllerSchema {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_schemas_returns_one() {
|
||||
assert_eq!(all_learning_controller_schemas().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_controllers_returns_one() {
|
||||
assert_eq!(all_learning_registered_controllers().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linkedin_enrichment_schema() {
|
||||
let s = learning_schemas("learning_linkedin_enrichment");
|
||||
assert_eq!(s.namespace, "learning");
|
||||
assert_eq!(s.function, "linkedin_enrichment");
|
||||
assert!(s.inputs.is_empty());
|
||||
assert!(!s.outputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_function_returns_unknown() {
|
||||
let s = learning_schemas("nonexistent");
|
||||
assert_eq!(s.function, "unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schemas_and_controllers_match() {
|
||||
let s = all_learning_controller_schemas();
|
||||
let c = all_learning_registered_controllers();
|
||||
assert_eq!(s[0].function, c[0].schema.function);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_linkedin_enrichment(_params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
let config = config_rpc::load_config_with_timeout().await?;
|
||||
|
||||
@@ -93,3 +93,29 @@ pub fn create_memory_for_migration(
|
||||
) -> anyhow::Result<Box<dyn Memory>> {
|
||||
anyhow::bail!("memory migration is disabled for the unified namespace memory core")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn effective_memory_backend_name_always_returns_namespace() {
|
||||
assert_eq!(effective_memory_backend_name("sqlite", None), "namespace");
|
||||
assert_eq!(effective_memory_backend_name("anything", None), "namespace");
|
||||
assert_eq!(effective_memory_backend_name("", None), "namespace");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_memory_for_migration_always_errors() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
// Box<dyn Memory> doesn't impl Debug, so we can't use .unwrap_err().
|
||||
// Use match instead.
|
||||
match create_memory_for_migration("any", tmp.path()) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert!(
|
||||
e.to_string().contains("migration is disabled"),
|
||||
"unexpected error: {e}"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,3 +188,238 @@ impl UnifiedMemory {
|
||||
(1.0 / (1.0 + age_hours / 24.0)).clamp(0.0, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::UnifiedMemory;
|
||||
use serde_json::json;
|
||||
|
||||
// ── vec_to_bytes / bytes_to_vec ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn vec_bytes_roundtrip() {
|
||||
let original = vec![1.0_f32, 2.5, -3.0, 0.0];
|
||||
let bytes = UnifiedMemory::vec_to_bytes(&original);
|
||||
assert_eq!(bytes.len(), 16); // 4 floats * 4 bytes
|
||||
let back = UnifiedMemory::bytes_to_vec(&bytes);
|
||||
assert_eq!(back, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vec_to_bytes_empty() {
|
||||
let bytes = UnifiedMemory::vec_to_bytes(&[]);
|
||||
assert!(bytes.is_empty());
|
||||
let back = UnifiedMemory::bytes_to_vec(&bytes);
|
||||
assert!(back.is_empty());
|
||||
}
|
||||
|
||||
// ── cosine_similarity ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn cosine_similarity_identical_vectors() {
|
||||
let v = vec![1.0_f32, 0.0, 0.0];
|
||||
let sim = UnifiedMemory::cosine_similarity(&v, &v);
|
||||
assert!((sim - 1.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cosine_similarity_orthogonal_vectors() {
|
||||
let a = vec![1.0_f32, 0.0];
|
||||
let b = vec![0.0_f32, 1.0];
|
||||
let sim = UnifiedMemory::cosine_similarity(&a, &b);
|
||||
assert!(sim.abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cosine_similarity_different_lengths_returns_zero() {
|
||||
let a = vec![1.0_f32, 0.0];
|
||||
let b = vec![1.0_f32, 0.0, 0.0];
|
||||
assert_eq!(UnifiedMemory::cosine_similarity(&a, &b), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cosine_similarity_empty_vectors_returns_zero() {
|
||||
assert_eq!(UnifiedMemory::cosine_similarity(&[], &[]), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cosine_similarity_zero_vector_returns_zero() {
|
||||
let a = vec![0.0_f32, 0.0];
|
||||
let b = vec![1.0_f32, 0.0];
|
||||
assert_eq!(UnifiedMemory::cosine_similarity(&a, &b), 0.0);
|
||||
}
|
||||
|
||||
// ── collapse_whitespace ──────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn collapse_whitespace_normalizes() {
|
||||
assert_eq!(
|
||||
UnifiedMemory::collapse_whitespace(" hello world "),
|
||||
"hello world"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collapse_whitespace_empty() {
|
||||
assert_eq!(UnifiedMemory::collapse_whitespace(""), "");
|
||||
}
|
||||
|
||||
// ── normalize_search_text ────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn normalize_search_text_lowercases_and_strips_special() {
|
||||
let result = UnifiedMemory::normalize_search_text("Hello, World! @#$ test");
|
||||
assert_eq!(result, "hello world test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_search_text_preserves_separators() {
|
||||
let result = UnifiedMemory::normalize_search_text("path/to_file-name.txt");
|
||||
assert_eq!(result, "path to file name txt");
|
||||
}
|
||||
|
||||
// ── tokenize_search_terms ────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn tokenize_search_terms_splits_correctly() {
|
||||
let terms = UnifiedMemory::tokenize_search_terms("Hello World");
|
||||
assert_eq!(terms, vec!["hello", "world"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokenize_search_terms_empty() {
|
||||
assert!(UnifiedMemory::tokenize_search_terms("").is_empty());
|
||||
assert!(UnifiedMemory::tokenize_search_terms(" @#$ ").is_empty());
|
||||
}
|
||||
|
||||
// ── normalize_graph_entity / predicate ───────────────────────────
|
||||
|
||||
#[test]
|
||||
fn normalize_graph_entity_uppercases() {
|
||||
assert_eq!(
|
||||
UnifiedMemory::normalize_graph_entity(" rust language "),
|
||||
"RUST LANGUAGE"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_graph_predicate_underscores_separators() {
|
||||
assert_eq!(
|
||||
UnifiedMemory::normalize_graph_predicate("is written in"),
|
||||
"IS_WRITTEN_IN"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_graph_predicate_strips_trailing_underscores() {
|
||||
assert_eq!(UnifiedMemory::normalize_graph_predicate(" has -- "), "HAS");
|
||||
}
|
||||
|
||||
// ── json_string_array ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn json_string_array_from_array_and_singular() {
|
||||
let val = json!({"tags": ["a", "b"], "tag": "c"});
|
||||
let result = UnifiedMemory::json_string_array(&val, "tags", "tag");
|
||||
assert_eq!(result, vec!["a", "b", "c"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_string_array_deduplicates() {
|
||||
let val = json!({"tags": ["a", "a"], "tag": "a"});
|
||||
let result = UnifiedMemory::json_string_array(&val, "tags", "tag");
|
||||
assert_eq!(result, vec!["a"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_string_array_empty_when_missing() {
|
||||
let val = json!({});
|
||||
let result = UnifiedMemory::json_string_array(&val, "tags", "tag");
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_string_array_filters_empty_strings() {
|
||||
let val = json!({"tags": ["", " ", "valid"]});
|
||||
let result = UnifiedMemory::json_string_array(&val, "tags", "tag");
|
||||
assert_eq!(result, vec!["valid"]);
|
||||
}
|
||||
|
||||
// ── merge_unique_string_arrays ───────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn merge_unique_string_arrays_combines_and_deduplicates() {
|
||||
let a = json!({"tags": ["x", "y"]});
|
||||
let b = json!({"tags": ["y", "z"]});
|
||||
let merged = UnifiedMemory::merge_unique_string_arrays(&a, &b, "tags", "tag");
|
||||
assert_eq!(merged, vec!["x", "y", "z"]);
|
||||
}
|
||||
|
||||
// ── json_i64 ─────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn json_i64_from_integer() {
|
||||
assert_eq!(UnifiedMemory::json_i64(&json!({"n": 42}), "n"), Some(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_i64_from_float() {
|
||||
assert_eq!(UnifiedMemory::json_i64(&json!({"n": 3.9}), "n"), Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_i64_missing_key() {
|
||||
assert_eq!(UnifiedMemory::json_i64(&json!({}), "n"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_i64_from_string_returns_none() {
|
||||
assert_eq!(UnifiedMemory::json_i64(&json!({"n": "42"}), "n"), None);
|
||||
}
|
||||
|
||||
// ── recency_score ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn recency_score_current_time_is_one() {
|
||||
let now = 1_700_000_000.0;
|
||||
let score = UnifiedMemory::recency_score(now, now);
|
||||
assert!((score - 1.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recency_score_old_document_is_lower() {
|
||||
let now = 1_700_000_000.0;
|
||||
let one_day_ago = now - 86400.0;
|
||||
let score = UnifiedMemory::recency_score(one_day_ago, now);
|
||||
assert!(score < 1.0);
|
||||
assert!(score > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recency_score_future_clamped_to_one() {
|
||||
let now = 1_700_000_000.0;
|
||||
let future = now + 86400.0;
|
||||
let score = UnifiedMemory::recency_score(future, now);
|
||||
assert!((score - 1.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
// ── chunk_document_content ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn chunk_document_content_returns_nonempty_for_content() {
|
||||
let chunks = UnifiedMemory::chunk_document_content("Hello world. This is a test.", 100);
|
||||
assert!(!chunks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_document_content_empty_input_returns_empty() {
|
||||
let chunks = UnifiedMemory::chunk_document_content("", 100);
|
||||
assert!(chunks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_document_content_whitespace_only_returns_empty() {
|
||||
let chunks = UnifiedMemory::chunk_document_content(" \n \t ", 100);
|
||||
assert!(chunks.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,4 +143,81 @@ mod tests {
|
||||
// Should return some sandbox (at least NoopSandbox)
|
||||
assert!(sandbox.is_available());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_via_enabled_false_returns_noop() {
|
||||
let config = SecurityConfig {
|
||||
sandbox: SandboxConfig {
|
||||
enabled: Some(false),
|
||||
backend: SandboxBackend::Auto,
|
||||
firejail_args: Vec::new(),
|
||||
},
|
||||
..SecurityConfig::default()
|
||||
};
|
||||
let sandbox = create_sandbox(&config);
|
||||
assert_eq!(sandbox.name(), "none");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn landlock_backend_on_non_linux_falls_back() {
|
||||
// On macOS/Windows, Landlock isn't available — should fall back to Noop
|
||||
let config = SecurityConfig {
|
||||
sandbox: SandboxConfig {
|
||||
enabled: None,
|
||||
backend: SandboxBackend::Landlock,
|
||||
firejail_args: Vec::new(),
|
||||
},
|
||||
..SecurityConfig::default()
|
||||
};
|
||||
let sandbox = create_sandbox(&config);
|
||||
if std::env::consts::OS != "linux" {
|
||||
assert_eq!(sandbox.name(), "none");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firejail_backend_on_non_linux_falls_back() {
|
||||
let config = SecurityConfig {
|
||||
sandbox: SandboxConfig {
|
||||
enabled: None,
|
||||
backend: SandboxBackend::Firejail,
|
||||
firejail_args: Vec::new(),
|
||||
},
|
||||
..SecurityConfig::default()
|
||||
};
|
||||
let sandbox = create_sandbox(&config);
|
||||
if std::env::consts::OS != "linux" {
|
||||
assert_eq!(sandbox.name(), "none");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bubblewrap_backend_falls_back_when_unavailable() {
|
||||
let config = SecurityConfig {
|
||||
sandbox: SandboxConfig {
|
||||
enabled: None,
|
||||
backend: SandboxBackend::Bubblewrap,
|
||||
firejail_args: Vec::new(),
|
||||
},
|
||||
..SecurityConfig::default()
|
||||
};
|
||||
let sandbox = create_sandbox(&config);
|
||||
// Bubblewrap probably isn't installed on CI/dev — expect fallback
|
||||
assert!(sandbox.is_available());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn docker_backend_falls_back_when_unavailable() {
|
||||
let config = SecurityConfig {
|
||||
sandbox: SandboxConfig {
|
||||
enabled: None,
|
||||
backend: SandboxBackend::Docker,
|
||||
firejail_args: Vec::new(),
|
||||
},
|
||||
..SecurityConfig::default()
|
||||
};
|
||||
let sandbox = create_sandbox(&config);
|
||||
// Docker may or may not be available
|
||||
assert!(sandbox.is_available());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,4 +205,55 @@ mod tests {
|
||||
|
||||
handle.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn service_restart_defaults_source_and_reason() {
|
||||
event_bus::init_global(event_bus::DEFAULT_CAPACITY);
|
||||
let outcome = service_restart(None, None)
|
||||
.await
|
||||
.expect("restart should succeed");
|
||||
assert!(outcome.value.accepted);
|
||||
assert_eq!(outcome.value.source, "jsonrpc");
|
||||
assert_eq!(outcome.value.reason, "service.restart");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn service_restart_trims_whitespace() {
|
||||
event_bus::init_global(event_bus::DEFAULT_CAPACITY);
|
||||
let outcome = service_restart(Some(" ui ".into()), Some(" user request ".into()))
|
||||
.await
|
||||
.expect("restart should succeed");
|
||||
assert_eq!(outcome.value.source, "ui");
|
||||
assert_eq!(outcome.value.reason, "user request");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn service_restart_empty_strings_use_defaults() {
|
||||
event_bus::init_global(event_bus::DEFAULT_CAPACITY);
|
||||
let outcome = service_restart(Some("".into()), Some(" ".into()))
|
||||
.await
|
||||
.expect("restart should succeed");
|
||||
assert_eq!(outcome.value.source, "jsonrpc");
|
||||
assert_eq!(outcome.value.reason, "service.restart");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restart_status_serializes() {
|
||||
let status = RestartStatus {
|
||||
accepted: true,
|
||||
source: "test".into(),
|
||||
reason: "testing".into(),
|
||||
};
|
||||
let json = serde_json::to_string(&status).unwrap();
|
||||
assert!(json.contains("\"accepted\":true"));
|
||||
assert!(json.contains("\"source\":\"test\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_startup_restart_delay_from_env_noop_when_unset() {
|
||||
// Ensure the env var is not set, then call — should not block
|
||||
let _prev = std::env::var(RESTART_DELAY_ENV).ok();
|
||||
std::env::remove_var(RESTART_DELAY_ENV);
|
||||
apply_startup_restart_delay_from_env(); // should return immediately
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,3 +254,100 @@ fn handle_daemon_host_set(params: Map<String, Value>) -> ControllerFuture {
|
||||
fn to_json<T: serde::Serialize>(outcome: RpcOutcome<T>) -> Result<Value, String> {
|
||||
outcome.into_cli_compatible_json()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_schemas_returns_eight() {
|
||||
assert_eq!(all_controller_schemas().len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_controllers_returns_eight() {
|
||||
assert_eq!(all_registered_controllers().len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_schemas_use_service_namespace() {
|
||||
for s in all_controller_schemas() {
|
||||
assert_eq!(s.namespace, "service");
|
||||
assert!(!s.description.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lifecycle_schemas_have_no_inputs_except_restart() {
|
||||
for fn_name in ["install", "start", "stop", "status", "uninstall"] {
|
||||
let s = schemas(fn_name);
|
||||
assert!(
|
||||
s.inputs.is_empty(),
|
||||
"{fn_name} should have no inputs, got {}",
|
||||
s.inputs.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restart_schema_has_optional_source_and_reason() {
|
||||
let s = schemas("restart");
|
||||
assert_eq!(s.function, "restart");
|
||||
assert_eq!(s.inputs.len(), 2);
|
||||
for input in &s.inputs {
|
||||
assert!(
|
||||
!input.required,
|
||||
"restart input '{}' should be optional",
|
||||
input.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn daemon_host_get_schema_has_no_inputs() {
|
||||
let s = schemas("daemon_host_get");
|
||||
assert!(s.inputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn daemon_host_set_requires_show_tray() {
|
||||
let s = schemas("daemon_host_set");
|
||||
assert_eq!(s.inputs.len(), 1);
|
||||
assert_eq!(s.inputs[0].name, "show_tray");
|
||||
assert!(s.inputs[0].required);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_function_returns_unknown() {
|
||||
let s = schemas("nonexistent");
|
||||
assert_eq!(s.function, "unknown");
|
||||
assert_eq!(s.namespace, "service");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schemas_and_controllers_match() {
|
||||
let s = all_controller_schemas();
|
||||
let c = all_registered_controllers();
|
||||
assert_eq!(s.len(), c.len());
|
||||
for (schema, ctrl) in s.iter().zip(c.iter()) {
|
||||
assert_eq!(schema.function, ctrl.schema.function);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_functions_resolve_correctly() {
|
||||
for fn_name in [
|
||||
"install",
|
||||
"restart",
|
||||
"start",
|
||||
"stop",
|
||||
"status",
|
||||
"uninstall",
|
||||
"daemon_host_get",
|
||||
"daemon_host_set",
|
||||
] {
|
||||
let s = schemas(fn_name);
|
||||
assert_ne!(s.function, "unknown", "{fn_name} fell through");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,3 +68,102 @@ pub enum ToolContent {
|
||||
Text { text: String },
|
||||
Json { data: serde_json::Value },
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn tool_result_success() {
|
||||
let r = ToolResult::success("done");
|
||||
assert!(!r.is_error);
|
||||
assert_eq!(r.text(), "done");
|
||||
assert_eq!(r.output(), "done");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_result_error() {
|
||||
let r = ToolResult::error("failed");
|
||||
assert!(r.is_error);
|
||||
assert_eq!(r.text(), "failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_result_json() {
|
||||
let r = ToolResult::json(json!({"key": "value"}));
|
||||
assert!(!r.is_error);
|
||||
assert!(r.text().is_empty()); // text() skips JSON blocks
|
||||
assert!(r.output().contains("key"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_result_mixed_content() {
|
||||
let r = ToolResult {
|
||||
content: vec![
|
||||
ToolContent::Text {
|
||||
text: "line1".into(),
|
||||
},
|
||||
ToolContent::Json {
|
||||
data: json!({"a": 1}),
|
||||
},
|
||||
ToolContent::Text {
|
||||
text: "line2".into(),
|
||||
},
|
||||
],
|
||||
is_error: false,
|
||||
};
|
||||
assert_eq!(r.text(), "line1\nline2");
|
||||
let output = r.output();
|
||||
assert!(output.contains("line1"));
|
||||
assert!(output.contains("line2"));
|
||||
assert!(output.contains("\"a\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_result_serde_roundtrip() {
|
||||
let r = ToolResult::success("hello");
|
||||
let json = serde_json::to_string(&r).unwrap();
|
||||
let back: ToolResult = serde_json::from_str(&json).unwrap();
|
||||
assert!(!back.is_error);
|
||||
assert_eq!(back.text(), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_content_text_serde() {
|
||||
let c = ToolContent::Text {
|
||||
text: "test".into(),
|
||||
};
|
||||
let json = serde_json::to_string(&c).unwrap();
|
||||
assert!(json.contains("\"type\":\"text\""));
|
||||
let back: ToolContent = serde_json::from_str(&json).unwrap();
|
||||
match back {
|
||||
ToolContent::Text { text } => assert_eq!(text, "test"),
|
||||
_ => panic!("expected Text variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_content_json_serde() {
|
||||
let c = ToolContent::Json {
|
||||
data: json!({"x": 1}),
|
||||
};
|
||||
let json = serde_json::to_string(&c).unwrap();
|
||||
assert!(json.contains("\"type\":\"json\""));
|
||||
let back: ToolContent = serde_json::from_str(&json).unwrap();
|
||||
match back {
|
||||
ToolContent::Json { data } => assert_eq!(data["x"], 1),
|
||||
_ => panic!("expected Json variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_result_empty_content() {
|
||||
let r = ToolResult {
|
||||
content: vec![],
|
||||
is_error: false,
|
||||
};
|
||||
assert!(r.text().is_empty());
|
||||
assert!(r.output().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,3 +479,163 @@ fn field_opt(name: &'static str, ty: TypeSchema, comment: &'static str) -> Field
|
||||
fn to_json<T: serde::Serialize>(outcome: RpcOutcome<T>) -> Result<Value, String> {
|
||||
outcome.into_cli_compatible_json()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_schemas_returns_ten() {
|
||||
assert_eq!(all_controller_schemas().len(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_controllers_returns_ten() {
|
||||
assert_eq!(all_registered_controllers().len(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_use_subconscious_namespace() {
|
||||
for s in all_controller_schemas() {
|
||||
assert_eq!(s.namespace, "subconscious");
|
||||
assert!(!s.description.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schemas_and_controllers_match() {
|
||||
let s = all_controller_schemas();
|
||||
let c = all_registered_controllers();
|
||||
for (schema, ctrl) in s.iter().zip(c.iter()) {
|
||||
assert_eq!(schema.function, ctrl.schema.function);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_functions_resolve() {
|
||||
for fn_name in [
|
||||
"status",
|
||||
"trigger",
|
||||
"tasks_list",
|
||||
"tasks_add",
|
||||
"tasks_update",
|
||||
"tasks_remove",
|
||||
"log_list",
|
||||
"escalations_list",
|
||||
"escalations_approve",
|
||||
"escalations_dismiss",
|
||||
] {
|
||||
let s = schemas(fn_name);
|
||||
assert_ne!(s.function, "unknown", "{fn_name} fell through");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_function_returns_unknown() {
|
||||
let s = schemas("nonexistent");
|
||||
assert_eq!(s.function, "unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_schema_has_no_inputs() {
|
||||
assert!(schemas("status").inputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trigger_schema_has_no_inputs() {
|
||||
assert!(schemas("trigger").inputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tasks_add_requires_title() {
|
||||
let s = schemas("tasks_add");
|
||||
let required: Vec<&str> = s
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|f| f.required)
|
||||
.map(|f| f.name)
|
||||
.collect();
|
||||
assert!(required.contains(&"title"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tasks_update_requires_task_id() {
|
||||
let s = schemas("tasks_update");
|
||||
let required: Vec<&str> = s
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|f| f.required)
|
||||
.map(|f| f.name)
|
||||
.collect();
|
||||
assert!(required.contains(&"task_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tasks_remove_requires_task_id() {
|
||||
let s = schemas("tasks_remove");
|
||||
let required: Vec<&str> = s
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|f| f.required)
|
||||
.map(|f| f.name)
|
||||
.collect();
|
||||
assert!(required.contains(&"task_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escalations_approve_requires_escalation_id() {
|
||||
let s = schemas("escalations_approve");
|
||||
assert!(s
|
||||
.inputs
|
||||
.iter()
|
||||
.any(|f| f.name == "escalation_id" && f.required));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escalations_dismiss_requires_escalation_id() {
|
||||
let s = schemas("escalations_dismiss");
|
||||
assert!(s
|
||||
.inputs
|
||||
.iter()
|
||||
.any(|f| f.name == "escalation_id" && f.required));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_list_has_optional_inputs() {
|
||||
let s = schemas("log_list");
|
||||
for input in &s.inputs {
|
||||
assert!(
|
||||
!input.required,
|
||||
"log_list input '{}' should be optional",
|
||||
input.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tasks_list_has_optional_enabled_only() {
|
||||
let s = schemas("tasks_list");
|
||||
let enabled = s.inputs.iter().find(|f| f.name == "enabled_only");
|
||||
assert!(enabled.is_some_and(|f| !f.required));
|
||||
}
|
||||
|
||||
// ── Field helpers ──────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn field_helper_is_required() {
|
||||
let f = field("name", TypeSchema::String, "desc");
|
||||
assert!(f.required);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_req_helper_is_required() {
|
||||
let f = field_req("name", TypeSchema::String, "desc");
|
||||
assert!(f.required);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_opt_helper_is_not_required() {
|
||||
let f = field_opt("name", TypeSchema::String, "desc");
|
||||
assert!(!f.required);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,4 +508,229 @@ mod tests {
|
||||
assert!(parsed["proxy"]["http_proxy"].is_null());
|
||||
assert!(parsed["runtime_proxy"]["http_proxy"].is_null());
|
||||
}
|
||||
|
||||
// ── parse_scope ──────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn parse_scope_known_values() {
|
||||
assert_eq!(
|
||||
ProxyConfigTool::parse_scope("environment"),
|
||||
Some(ProxyScope::Environment)
|
||||
);
|
||||
assert_eq!(
|
||||
ProxyConfigTool::parse_scope("env"),
|
||||
Some(ProxyScope::Environment)
|
||||
);
|
||||
assert_eq!(
|
||||
ProxyConfigTool::parse_scope("openhuman"),
|
||||
Some(ProxyScope::OpenHuman)
|
||||
);
|
||||
assert_eq!(
|
||||
ProxyConfigTool::parse_scope("internal"),
|
||||
Some(ProxyScope::OpenHuman)
|
||||
);
|
||||
assert_eq!(
|
||||
ProxyConfigTool::parse_scope("core"),
|
||||
Some(ProxyScope::OpenHuman)
|
||||
);
|
||||
assert_eq!(
|
||||
ProxyConfigTool::parse_scope("services"),
|
||||
Some(ProxyScope::Services)
|
||||
);
|
||||
assert_eq!(
|
||||
ProxyConfigTool::parse_scope("service"),
|
||||
Some(ProxyScope::Services)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_scope_case_insensitive() {
|
||||
assert_eq!(
|
||||
ProxyConfigTool::parse_scope("SERVICES"),
|
||||
Some(ProxyScope::Services)
|
||||
);
|
||||
assert_eq!(
|
||||
ProxyConfigTool::parse_scope(" ENV "),
|
||||
Some(ProxyScope::Environment)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_scope_unknown_returns_none() {
|
||||
assert!(ProxyConfigTool::parse_scope("unknown").is_none());
|
||||
assert!(ProxyConfigTool::parse_scope("").is_none());
|
||||
}
|
||||
|
||||
// ── parse_string_list ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn parse_string_list_from_csv() {
|
||||
let result =
|
||||
ProxyConfigTool::parse_string_list(&json!("provider.openai,tool.browser"), "services")
|
||||
.unwrap();
|
||||
assert_eq!(result, vec!["provider.openai", "tool.browser"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_string_list_from_array() {
|
||||
let result = ProxyConfigTool::parse_string_list(
|
||||
&json!(["provider.openai", "tool.browser"]),
|
||||
"services",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result, vec!["provider.openai", "tool.browser"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_string_list_trims_and_filters_empty() {
|
||||
let result = ProxyConfigTool::parse_string_list(&json!(" a , , b "), "services").unwrap();
|
||||
assert_eq!(result, vec!["a", "b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_string_list_rejects_non_string_array_elements() {
|
||||
let result = ProxyConfigTool::parse_string_list(&json!([1, 2, 3]), "services");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_string_list_rejects_object() {
|
||||
let result = ProxyConfigTool::parse_string_list(&json!({}), "services");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ── parse_optional_string_update ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn parse_optional_string_update_unset() {
|
||||
let result =
|
||||
ProxyConfigTool::parse_optional_string_update(&json!({}), "http_proxy").unwrap();
|
||||
assert!(matches!(result, MaybeSet::Unset));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_optional_string_update_null() {
|
||||
let result = ProxyConfigTool::parse_optional_string_update(
|
||||
&json!({"http_proxy": null}),
|
||||
"http_proxy",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(result, MaybeSet::Null));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_optional_string_update_empty_string_is_null() {
|
||||
let result =
|
||||
ProxyConfigTool::parse_optional_string_update(&json!({"http_proxy": ""}), "http_proxy")
|
||||
.unwrap();
|
||||
assert!(matches!(result, MaybeSet::Null));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_optional_string_update_set() {
|
||||
let result = ProxyConfigTool::parse_optional_string_update(
|
||||
&json!({"http_proxy": "http://proxy:8080"}),
|
||||
"http_proxy",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(result, MaybeSet::Set(ref v) if v == "http://proxy:8080"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_optional_string_update_rejects_non_string() {
|
||||
let result =
|
||||
ProxyConfigTool::parse_optional_string_update(&json!({"http_proxy": 42}), "http_proxy");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ── env_snapshot ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn env_snapshot_returns_object() {
|
||||
let snap = ProxyConfigTool::env_snapshot();
|
||||
assert!(snap.is_object());
|
||||
assert!(snap.get("HTTP_PROXY").is_some());
|
||||
assert!(snap.get("HTTPS_PROXY").is_some());
|
||||
}
|
||||
|
||||
// ── proxy_json ───────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn proxy_json_returns_object_with_expected_fields() {
|
||||
let config = ProxyConfig::default();
|
||||
let json = ProxyConfigTool::proxy_json(&config);
|
||||
assert!(json.get("enabled").is_some());
|
||||
assert!(json.get("scope").is_some());
|
||||
assert!(json.get("http_proxy").is_some());
|
||||
}
|
||||
|
||||
// ── tool metadata ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn tool_name_and_description() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let tool = ProxyConfigTool::new(
|
||||
Arc::new(Config {
|
||||
workspace_dir: tmp.path().to_path_buf(),
|
||||
config_path: tmp.path().join("config.toml"),
|
||||
..Config::default()
|
||||
}),
|
||||
test_security(),
|
||||
);
|
||||
assert_eq!(tool.name(), "proxy_config");
|
||||
assert!(!tool.description().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn parameters_schema_is_valid() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let tool = ProxyConfigTool::new(test_config(&tmp).await, test_security());
|
||||
let schema = tool.parameters_schema();
|
||||
assert!(schema.is_object());
|
||||
assert!(schema.get("properties").is_some() || schema.get("type").is_some());
|
||||
}
|
||||
|
||||
// ── require_write_access ─────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn blocks_set_in_readonly_mode() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let readonly = Arc::new(SecurityPolicy {
|
||||
autonomy: AutonomyLevel::ReadOnly,
|
||||
..SecurityPolicy::default()
|
||||
});
|
||||
let tool = ProxyConfigTool::new(test_config(&tmp).await, readonly);
|
||||
let result = tool
|
||||
.execute(json!({"action": "set", "enabled": true}))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(result.is_error);
|
||||
assert!(result.output().contains("read-only"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_action_returns_error() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let tool = ProxyConfigTool::new(test_config(&tmp).await, test_security());
|
||||
let result = tool.execute(json!({})).await;
|
||||
// Missing action may return Err or ToolResult::error
|
||||
match result {
|
||||
Err(_) => {}
|
||||
Ok(r) => {
|
||||
// Some implementations return success with help text; just verify it ran
|
||||
let _ = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unknown_action_returns_error() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let tool = ProxyConfigTool::new(test_config(&tmp).await, test_security());
|
||||
let result = tool.execute(json!({"action": "delete"})).await;
|
||||
match result {
|
||||
Err(e) => assert!(e.to_string().contains("Unknown action")),
|
||||
Ok(r) => assert!(r.is_error, "expected error for unknown action"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,3 +289,171 @@ fn type_name(value: &Value) -> &'static str {
|
||||
Value::Object(_) => "object",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn all_schemas_returns_five() {
|
||||
assert_eq!(all_controller_schemas().len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_controllers_returns_five() {
|
||||
assert_eq!(all_registered_controllers().len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_use_tree_summarizer_namespace() {
|
||||
for s in all_controller_schemas() {
|
||||
assert_eq!(s.namespace, "tree_summarizer");
|
||||
assert!(!s.description.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schemas_and_controllers_match() {
|
||||
let s = all_controller_schemas();
|
||||
let c = all_registered_controllers();
|
||||
for (schema, ctrl) in s.iter().zip(c.iter()) {
|
||||
assert_eq!(schema.function, ctrl.schema.function);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_functions_resolve() {
|
||||
for fn_name in ["ingest", "run", "query", "status", "rebuild"] {
|
||||
let s = schemas(fn_name);
|
||||
assert_ne!(s.function, "unknown", "{fn_name} fell through");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_function_returns_unknown() {
|
||||
let s = schemas("nonexistent");
|
||||
assert_eq!(s.function, "unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ingest_requires_namespace_and_content() {
|
||||
let s = schemas("ingest");
|
||||
let required: Vec<&str> = s
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|f| f.required)
|
||||
.map(|f| f.name)
|
||||
.collect();
|
||||
assert!(required.contains(&"namespace"));
|
||||
assert!(required.contains(&"content"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_requires_namespace() {
|
||||
let s = schemas("query");
|
||||
let required: Vec<&str> = s
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|f| f.required)
|
||||
.map(|f| f.name)
|
||||
.collect();
|
||||
assert!(required.contains(&"namespace"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_requires_namespace() {
|
||||
let s = schemas("status");
|
||||
assert!(s.inputs.iter().any(|f| f.name == "namespace" && f.required));
|
||||
}
|
||||
|
||||
// ── Param helper tests ──────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn read_required_parses_string() {
|
||||
let mut m = Map::new();
|
||||
m.insert("key".into(), Value::String("val".into()));
|
||||
let result: String = read_required(&m, "key").unwrap();
|
||||
assert_eq!(result, "val");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_required_errors_on_missing() {
|
||||
let m = Map::new();
|
||||
let err = read_required::<String>(&m, "key").unwrap_err();
|
||||
assert!(err.contains("missing required"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_optional_returns_none_for_missing() {
|
||||
let m = Map::new();
|
||||
let result: Option<String> = read_optional(&m, "key").unwrap();
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_optional_returns_none_for_null() {
|
||||
let mut m = Map::new();
|
||||
m.insert("key".into(), Value::Null);
|
||||
let result: Option<String> = read_optional(&m, "key").unwrap();
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_optional_returns_some_for_value() {
|
||||
let mut m = Map::new();
|
||||
m.insert("key".into(), Value::String("val".into()));
|
||||
let result: Option<String> = read_optional(&m, "key").unwrap();
|
||||
assert_eq!(result, Some("val".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_optional_timestamp_valid_rfc3339() {
|
||||
let mut m = Map::new();
|
||||
m.insert("ts".into(), Value::String("2026-04-17T12:00:00Z".into()));
|
||||
let result = read_optional_timestamp(&m, "ts").unwrap();
|
||||
assert!(result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_optional_timestamp_invalid_format() {
|
||||
let mut m = Map::new();
|
||||
m.insert("ts".into(), Value::String("not-a-date".into()));
|
||||
assert!(read_optional_timestamp(&m, "ts").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_optional_timestamp_non_string() {
|
||||
let mut m = Map::new();
|
||||
m.insert("ts".into(), json!(12345));
|
||||
assert!(read_optional_timestamp(&m, "ts").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_optional_timestamp_none_for_missing() {
|
||||
let m = Map::new();
|
||||
assert!(read_optional_timestamp(&m, "ts").unwrap().is_none());
|
||||
}
|
||||
|
||||
// ── type_name ───────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn type_name_covers_all_variants() {
|
||||
assert_eq!(type_name(&Value::Null), "null");
|
||||
assert_eq!(type_name(&Value::Bool(true)), "bool");
|
||||
assert_eq!(type_name(&json!(42)), "number");
|
||||
assert_eq!(type_name(&json!("s")), "string");
|
||||
assert_eq!(type_name(&json!([1])), "array");
|
||||
assert_eq!(type_name(&json!({})), "object");
|
||||
}
|
||||
|
||||
// ── namespace_input helper ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn namespace_input_is_required_string() {
|
||||
let f = namespace_input("test");
|
||||
assert_eq!(f.name, "namespace");
|
||||
assert!(f.required);
|
||||
assert!(matches!(f.ty, TypeSchema::String));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,3 +114,64 @@ fn handle_apply(params: Map<String, Value>) -> ControllerFuture {
|
||||
fn to_json<T: serde::Serialize>(outcome: RpcOutcome<T>) -> Result<Value, String> {
|
||||
outcome.into_cli_compatible_json()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_schemas_returns_two() {
|
||||
assert_eq!(all_controller_schemas().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_controllers_returns_two() {
|
||||
assert_eq!(all_registered_controllers().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_schema() {
|
||||
let s = schemas("check");
|
||||
assert_eq!(s.namespace, "update");
|
||||
assert_eq!(s.function, "check");
|
||||
assert!(s.inputs.is_empty());
|
||||
assert!(!s.outputs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_schema_requires_download_url_and_asset_name() {
|
||||
let s = schemas("apply");
|
||||
assert_eq!(s.function, "apply");
|
||||
let required: Vec<&str> = s
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|f| f.required)
|
||||
.map(|f| f.name)
|
||||
.collect();
|
||||
assert!(required.contains(&"download_url"));
|
||||
assert!(required.contains(&"asset_name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_schema_has_optional_staging_dir() {
|
||||
let s = schemas("apply");
|
||||
let staging = s.inputs.iter().find(|f| f.name == "staging_dir");
|
||||
assert!(staging.is_some_and(|f| !f.required));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_function_returns_unknown() {
|
||||
let s = schemas("nonexistent");
|
||||
assert_eq!(s.function, "unknown");
|
||||
assert_eq!(s.namespace, "update");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schemas_and_controllers_match() {
|
||||
let s = all_controller_schemas();
|
||||
let c = all_registered_controllers();
|
||||
for (schema, ctrl) in s.iter().zip(c.iter()) {
|
||||
assert_eq!(schema.function, ctrl.schema.function);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user