mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 22:23:01 +00:00
+16




![github-actions[bot] <github-actions[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)




Mega Mind
GitHub
YellowSnnowmann
Steven Enamakel
github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Cyrus Gray
Horst1993
Cursor
James Gentes
Sam
Sami Rusani
oxoxDev
Muhammad Ismail
Claude Fable 5
nb213
binyangzhu000-sudo
Steven Enamakel
CodeGhost21
sanil-23
M3gA-Mind
oxoxDev
mysma-9403
mwakidenis
NgoQuocViet2001
viet.ngo
Maciej Myszkiewicz
a40fbb79d2
Co-authored-by: YellowSnnowmann <167776381+YellowSnnowmann@users.noreply.github.com> Co-authored-by: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Cyrus Gray <144336577+graycyrus@users.noreply.github.com> Co-authored-by: Horst1993 <horst.w@gmicloud.ai> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: James Gentes <jgentes@users.noreply.github.com> Co-authored-by: Sam <samrusani@users.noreply.github.com> Co-authored-by: Sami Rusani <14844597+samrusani@users.noreply.github.com> Co-authored-by: oxoxDev <164490987+oxoxDev@users.noreply.github.com> Co-authored-by: Muhammad Ismail <78064250+myi1@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: nb213 <binyangzhu000@gmail.com> Co-authored-by: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> Co-authored-by: CodeGhost21 <164498022+CodeGhost21@users.noreply.github.com> Co-authored-by: sanil-23 <sanil@tinyhumans.ai> Co-authored-by: M3gA-Mind <elvin@mahadao.com> Co-authored-by: oxoxDev <oxoxdev@users.noreply.github.com> Co-authored-by: mysma-9403 <64923976+mysma-9403@users.noreply.github.com> Co-authored-by: mwakidenis <mwakidenice@gmail.com> Co-authored-by: NgoQuocViet2001 <123613986+NgoQuocViet2001@users.noreply.github.com> Co-authored-by: viet.ngo <viet.ngo@sotatek.com> Co-authored-by: Maciej Myszkiewicz <mmyszkiewicz@bwcoders.com>
1261 lines
44 KiB
Rust
1261 lines
44 KiB
Rust
use serde_json::Map;
|
|
|
|
use super::*;
|
|
use crate::core::{ControllerSchema, FieldSchema, TypeSchema};
|
|
|
|
fn schema(
|
|
namespace: &'static str,
|
|
function: &'static str,
|
|
inputs: Vec<FieldSchema>,
|
|
) -> ControllerSchema {
|
|
ControllerSchema {
|
|
namespace,
|
|
function,
|
|
description: "test",
|
|
inputs,
|
|
outputs: vec![],
|
|
}
|
|
}
|
|
|
|
fn noop_handler(_params: Map<String, Value>) -> ControllerFuture {
|
|
Box::pin(async { Ok(Value::Null) })
|
|
}
|
|
|
|
/// Wrap raw controllers as [`GroupedController`]s (all `Platform`) so the
|
|
/// `validate_registry` unit tests — which build hand-made `RegisteredController`
|
|
/// lists — can feed the grouped-registry signature (#4796). The group is
|
|
/// irrelevant to `validate_registry`, which only inspects `.controller.schema`.
|
|
fn grouped(controllers: Vec<RegisteredController>) -> Vec<GroupedController> {
|
|
controllers
|
|
.into_iter()
|
|
.map(|controller| GroupedController {
|
|
group: DomainGroup::Platform,
|
|
controller,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn validate_registry_rejects_duplicate_namespace_function() {
|
|
let declared = vec![schema("dup", "fn", vec![]), schema("dup", "fn", vec![])];
|
|
let registered = vec![
|
|
RegisteredController {
|
|
schema: declared[0].clone(),
|
|
handler: noop_handler,
|
|
},
|
|
RegisteredController {
|
|
schema: declared[1].clone(),
|
|
handler: noop_handler,
|
|
},
|
|
];
|
|
|
|
let err = validate_registry(&grouped(registered)).expect_err("expected duplicate error");
|
|
assert!(err.contains("duplicate registered controller `dup.fn`"));
|
|
}
|
|
|
|
#[test]
|
|
fn validate_registry_rejects_duplicate_required_inputs() {
|
|
let declared = vec![schema(
|
|
"doctor",
|
|
"models",
|
|
vec![
|
|
FieldSchema {
|
|
name: "use_cache",
|
|
ty: TypeSchema::Bool,
|
|
comment: "x",
|
|
required: true,
|
|
},
|
|
FieldSchema {
|
|
name: "use_cache",
|
|
ty: TypeSchema::Bool,
|
|
comment: "x",
|
|
required: true,
|
|
},
|
|
],
|
|
)];
|
|
let registered = vec![RegisteredController {
|
|
schema: declared[0].clone(),
|
|
handler: noop_handler,
|
|
}];
|
|
|
|
let err = validate_registry(&grouped(registered)).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(&grouped(registered)).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("memory_tree").is_some());
|
|
assert!(namespace_description("billing").is_some());
|
|
assert!(namespace_description("config").is_some());
|
|
assert!(namespace_description("health").is_some());
|
|
assert!(namespace_description("security").is_some());
|
|
assert!(namespace_description("tool_registry").is_some());
|
|
assert!(namespace_description("voice").is_some());
|
|
assert!(namespace_description("webhooks").is_some());
|
|
assert!(namespace_description("notification").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());
|
|
}
|
|
|
|
/// With the `voice` feature on (the default), the voice + audio_toolkit
|
|
/// controllers are compiled in and registered — the desktop build is
|
|
/// byte-identical.
|
|
#[test]
|
|
#[cfg(feature = "voice")]
|
|
fn voice_and_audio_controllers_registered_when_feature_on() {
|
|
let schemas = all_controller_schemas();
|
|
assert!(
|
|
schemas.iter().any(|s| s.namespace == "voice"),
|
|
"voice controllers must be registered when the `voice` feature is on"
|
|
);
|
|
assert!(
|
|
schemas.iter().any(|s| s.namespace == "audio_toolkit"),
|
|
"audio_toolkit controllers must be registered when the `voice` feature is on"
|
|
);
|
|
}
|
|
|
|
/// With the `voice` feature off, both domains are compiled out: their
|
|
/// controllers never enter the registry, so voice/audio RPC methods are
|
|
/// unknown-method and absent from `/schema`. This is the compile-time
|
|
/// stub-facade correctness gate (see `openhuman::voice::stub`).
|
|
#[test]
|
|
#[cfg(not(feature = "voice"))]
|
|
fn voice_and_audio_controllers_absent_when_feature_off() {
|
|
let schemas = all_controller_schemas();
|
|
assert!(
|
|
!schemas
|
|
.iter()
|
|
.any(|s| s.namespace == "voice" || s.namespace == "audio_toolkit"),
|
|
"voice/audio_toolkit controllers must be compiled out when the `voice` feature is off"
|
|
);
|
|
}
|
|
|
|
/// With the `inference` feature on (the default), the in-process whisper STT
|
|
/// engine is compiled in — `INFERENCE_COMPILED_IN` reflects that, and
|
|
/// `whisper-rs` + `cpal` are linked (dependency shed proven separately by
|
|
/// `cargo tree -i whisper-rs` / `cargo tree -i cpal`).
|
|
#[test]
|
|
#[cfg(feature = "inference")]
|
|
fn inference_engine_compiled_in_when_feature_on() {
|
|
assert!(crate::openhuman::inference::INFERENCE_COMPILED_IN);
|
|
}
|
|
|
|
/// With the `inference` feature off, the whisper engine is compiled out: the
|
|
/// marker flips and the always-compiled `whisper_engine` facade resolves to the
|
|
/// disabled stub — every transcription call returns the disabled error, while
|
|
/// the local-AI service still reaches Ollama / LM Studio over HTTP. This is the
|
|
/// compile-time stub-facade correctness gate (see
|
|
/// `inference::local::service::whisper_engine::stub`); `whisper-rs` + `cpal`
|
|
/// leave the dependency graph.
|
|
#[test]
|
|
#[cfg(not(feature = "inference"))]
|
|
fn inference_engine_compiled_out_when_feature_off() {
|
|
use crate::openhuman::inference::local::service::whisper_engine;
|
|
assert!(!crate::openhuman::inference::INFERENCE_COMPILED_IN);
|
|
let handle = whisper_engine::new_handle();
|
|
assert!(!whisper_engine::is_loaded(&handle));
|
|
let err = whisper_engine::transcribe_pcm_f32(&handle, &[0.0; 16], None, None)
|
|
.expect_err("in-process STT must be disabled when `inference` is off");
|
|
assert!(
|
|
err.contains("inference"),
|
|
"disabled error should name the gate: {err}"
|
|
);
|
|
}
|
|
|
|
/// With the `skills` feature on (the default), all three skill domains are
|
|
/// compiled in and registered — the desktop build is byte-identical.
|
|
#[test]
|
|
#[cfg(feature = "skills")]
|
|
fn skill_controllers_registered_when_feature_on() {
|
|
let schemas = all_controller_schemas();
|
|
for ns in ["skills", "skill_runtime", "skill_registry"] {
|
|
assert!(
|
|
schemas.iter().any(|s| s.namespace == ns),
|
|
"`{ns}` controllers must be registered when the `skills` feature is on"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// With the `skills` feature off, all three domains are compiled out: their
|
|
/// controllers never enter the registry, so skills RPC methods are
|
|
/// unknown-method and absent from `/schema`. This is the compile-time
|
|
/// stub-facade correctness gate (see `openhuman::skills::stub`).
|
|
///
|
|
/// Note this does NOT cover `skills::types` / `skills::ops_types`: those stay
|
|
/// compiled in both directions (the type carve-out — `tools::traits` re-exports
|
|
/// `ToolResult`/`ToolContent` out of them), but they expose no controllers, so
|
|
/// the namespaces are absent either way.
|
|
#[test]
|
|
#[cfg(not(feature = "skills"))]
|
|
fn skill_controllers_absent_when_feature_off() {
|
|
let schemas = all_controller_schemas();
|
|
assert!(
|
|
!schemas.iter().any(|s| s.namespace == "skills"
|
|
|| s.namespace == "skill_runtime"
|
|
|| s.namespace == "skill_registry"),
|
|
"skills/skill_runtime/skill_registry controllers must be compiled out \
|
|
when the `skills` feature is off"
|
|
);
|
|
}
|
|
|
|
/// With the `web3` feature on (the default), the wallet + web3 + x402
|
|
/// controllers are compiled in and registered, and the high-level web3 agent
|
|
/// tools (swap/bridge/dapp) are present — the desktop build is byte-identical.
|
|
#[test]
|
|
#[cfg(feature = "web3")]
|
|
fn wallet_web3_x402_controllers_registered_when_feature_on() {
|
|
let schemas = all_controller_schemas();
|
|
assert!(
|
|
schemas.iter().any(|s| s.namespace == "wallet"),
|
|
"wallet controllers must be registered when the `web3` feature is on"
|
|
);
|
|
assert!(
|
|
schemas.iter().any(|s| s.namespace.starts_with("web3_")),
|
|
"web3 (swap/bridge/dapp) controllers must be registered when the `web3` feature is on"
|
|
);
|
|
assert!(
|
|
schemas.iter().any(|s| s.namespace == "x402"),
|
|
"x402 controllers must be registered when the `web3` feature is on"
|
|
);
|
|
assert!(
|
|
!crate::openhuman::web3::all_web3_agent_tools().is_empty(),
|
|
"web3 agent tools must be present when the `web3` feature is on"
|
|
);
|
|
}
|
|
|
|
/// With the `web3` feature off, all three domains are compiled out: their
|
|
/// controllers never enter the registry (wallet/web3/x402 RPC methods are
|
|
/// unknown-method and absent from `/schema`) and the web3 agent tools are
|
|
/// gone. This is the compile-time stub-facade correctness gate (see
|
|
/// `openhuman::{wallet,web3,x402}::stub`).
|
|
#[test]
|
|
#[cfg(not(feature = "web3"))]
|
|
fn wallet_web3_x402_controllers_absent_when_feature_off() {
|
|
let schemas = all_controller_schemas();
|
|
assert!(
|
|
!schemas.iter().any(|s| s.namespace == "wallet"
|
|
|| s.namespace.starts_with("web3_")
|
|
|| s.namespace == "x402"),
|
|
"wallet/web3/x402 controllers must be compiled out when the `web3` feature is off"
|
|
);
|
|
assert!(
|
|
crate::openhuman::web3::all_web3_agent_tools().is_empty(),
|
|
"web3 agent tools must be gone when the `web3` feature is off"
|
|
);
|
|
}
|
|
|
|
#[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_finds_security_policy_info() {
|
|
let schema = schema_for_rpc_method("openhuman.security_policy_info");
|
|
assert!(schema.is_some(), "security.policy_info should be findable");
|
|
let s = schema.unwrap();
|
|
assert_eq!(s.namespace, "security");
|
|
assert_eq!(s.function, "policy_info");
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "mcp")]
|
|
fn schema_for_rpc_method_finds_internal_mcp_audit_list() {
|
|
let schema = schema_for_rpc_method("openhuman.mcp_audit_list");
|
|
assert!(
|
|
schema.is_some(),
|
|
"mcp_audit.list should be internally routable"
|
|
);
|
|
let s = schema.unwrap();
|
|
assert_eq!(s.namespace, "mcp_audit");
|
|
assert_eq!(s.function, "list");
|
|
}
|
|
|
|
#[test]
|
|
fn schema_for_rpc_method_finds_internal_orchestration_pairing_link_session() {
|
|
let schema = schema_for_rpc_method("openhuman.orchestration_pairing_link_session");
|
|
assert!(
|
|
schema.is_some(),
|
|
"orchestration_pairing.link_session should be internally routable"
|
|
);
|
|
let s = schema.unwrap();
|
|
assert_eq!(s.namespace, "orchestration_pairing");
|
|
assert_eq!(s.function, "link_session");
|
|
}
|
|
|
|
#[test]
|
|
fn rpc_method_from_parts_does_not_expose_internal_mcp_audit_list() {
|
|
assert!(
|
|
rpc_method_from_parts("mcp_audit", "list").is_none(),
|
|
"internal MCP audit RPC must not appear in the public controller registry"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rpc_method_from_parts_does_not_expose_internal_orchestration_pairing() {
|
|
assert!(
|
|
rpc_method_from_parts("orchestration_pairing", "link_session").is_none(),
|
|
"pairing write RPCs must not appear in the public controller registry"
|
|
);
|
|
}
|
|
|
|
#[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"
|
|
);
|
|
}
|
|
|
|
// --- validate_params edge cases -----------------------------------------
|
|
|
|
#[test]
|
|
fn validate_params_accepts_missing_optional_param() {
|
|
let s = schema(
|
|
"test",
|
|
"fn",
|
|
vec![FieldSchema {
|
|
name: "filter",
|
|
ty: TypeSchema::String,
|
|
comment: "optional filter",
|
|
required: false,
|
|
}],
|
|
);
|
|
assert!(validate_params(&s, &Map::new()).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn validate_params_accepts_optional_param_when_present() {
|
|
let s = schema(
|
|
"test",
|
|
"fn",
|
|
vec![FieldSchema {
|
|
name: "filter",
|
|
ty: TypeSchema::String,
|
|
comment: "",
|
|
required: false,
|
|
}],
|
|
);
|
|
let mut p = Map::new();
|
|
p.insert("filter".into(), Value::String("abc".into()));
|
|
assert!(validate_params(&s, &p).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn validate_params_missing_required_error_includes_comment() {
|
|
// The comment text helps callers (esp. the CLI/UI) understand what
|
|
// the missing field is for — lock this in so error messages don't
|
|
// regress to bare field names.
|
|
let s = schema(
|
|
"memory",
|
|
"doc_put",
|
|
vec![FieldSchema {
|
|
name: "namespace",
|
|
ty: TypeSchema::String,
|
|
comment: "namespace to write into",
|
|
required: true,
|
|
}],
|
|
);
|
|
let err = validate_params(&s, &Map::new()).unwrap_err();
|
|
assert!(err.contains("missing required param 'namespace'"));
|
|
assert!(err.contains("namespace to write into"));
|
|
}
|
|
|
|
#[test]
|
|
fn validate_params_unknown_error_includes_namespace_and_function() {
|
|
let s = schema("billing", "top_up", vec![]);
|
|
let mut p = Map::new();
|
|
p.insert("typo".into(), Value::Null);
|
|
let err = validate_params(&s, &p).unwrap_err();
|
|
assert!(err.contains("unknown param 'typo'"));
|
|
assert!(err.contains("billing.top_up"));
|
|
}
|
|
|
|
#[test]
|
|
fn validate_params_reports_missing_required_before_unknown() {
|
|
// If a call both omits a required param AND has an unknown one,
|
|
// the missing-required error fires first (it's strictly more
|
|
// actionable for callers).
|
|
let s = schema(
|
|
"test",
|
|
"fn",
|
|
vec![FieldSchema {
|
|
name: "key",
|
|
ty: TypeSchema::String,
|
|
comment: "",
|
|
required: true,
|
|
}],
|
|
);
|
|
let mut p = Map::new();
|
|
p.insert("unknown".into(), Value::Null);
|
|
let err = validate_params(&s, &p).unwrap_err();
|
|
assert!(err.contains("missing required param 'key'"), "got: {err}");
|
|
}
|
|
|
|
#[test]
|
|
fn validate_params_null_for_required_is_acceptable() {
|
|
// JSON-RPC semantics: `null` is a valid value for an optional field
|
|
// sent explicitly. For a required field, presence (not value) is
|
|
// what we check — null does satisfy the "key present" check.
|
|
// Handlers enforce stronger type contracts downstream.
|
|
let s = schema(
|
|
"test",
|
|
"fn",
|
|
vec![FieldSchema {
|
|
name: "key",
|
|
ty: TypeSchema::String,
|
|
comment: "",
|
|
required: true,
|
|
}],
|
|
);
|
|
let mut p = Map::new();
|
|
p.insert("key".into(), Value::Null);
|
|
assert!(validate_params(&s, &p).is_ok());
|
|
}
|
|
|
|
// --- validate_params type checking (C12) --------------------------------
|
|
|
|
#[test]
|
|
fn validate_params_rejects_wrong_scalar_type() {
|
|
let s = schema(
|
|
"test",
|
|
"fn",
|
|
vec![FieldSchema {
|
|
name: "count",
|
|
ty: TypeSchema::U64,
|
|
comment: "",
|
|
required: true,
|
|
}],
|
|
);
|
|
let mut p = Map::new();
|
|
p.insert("count".into(), Value::String("nope".into()));
|
|
let err = validate_params(&s, &p).unwrap_err();
|
|
assert!(err.contains("invalid type for param 'count'"), "got: {err}");
|
|
assert!(err.contains("expected unsigned integer"), "got: {err}");
|
|
}
|
|
|
|
#[test]
|
|
fn validate_params_accepts_correct_scalar_type() {
|
|
let s = schema(
|
|
"test",
|
|
"fn",
|
|
vec![FieldSchema {
|
|
name: "flag",
|
|
ty: TypeSchema::Bool,
|
|
comment: "",
|
|
required: true,
|
|
}],
|
|
);
|
|
let mut p = Map::new();
|
|
p.insert("flag".into(), Value::Bool(true));
|
|
assert!(validate_params(&s, &p).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn validate_params_validates_array_element_types() {
|
|
let s = schema(
|
|
"test",
|
|
"fn",
|
|
vec![FieldSchema {
|
|
name: "ids",
|
|
ty: TypeSchema::Array(Box::new(TypeSchema::String)),
|
|
comment: "",
|
|
required: true,
|
|
}],
|
|
);
|
|
let mut ok = Map::new();
|
|
ok.insert(
|
|
"ids".into(),
|
|
Value::Array(vec![Value::String("a".into()), Value::String("b".into())]),
|
|
);
|
|
assert!(validate_params(&s, &ok).is_ok());
|
|
|
|
let mut bad = Map::new();
|
|
bad.insert(
|
|
"ids".into(),
|
|
Value::Array(vec![Value::String("a".into()), Value::Bool(true)]),
|
|
);
|
|
let err = validate_params(&s, &bad).unwrap_err();
|
|
assert!(err.contains("invalid type for param 'ids'"), "got: {err}");
|
|
}
|
|
|
|
#[test]
|
|
fn validate_params_enforces_enum_variants() {
|
|
let s = schema(
|
|
"test",
|
|
"fn",
|
|
vec![FieldSchema {
|
|
name: "mode",
|
|
ty: TypeSchema::Enum {
|
|
variants: vec!["read", "write"],
|
|
},
|
|
comment: "",
|
|
required: true,
|
|
}],
|
|
);
|
|
let mut ok = Map::new();
|
|
ok.insert("mode".into(), Value::String("read".into()));
|
|
assert!(validate_params(&s, &ok).is_ok());
|
|
|
|
let mut bad = Map::new();
|
|
bad.insert("mode".into(), Value::String("delete".into()));
|
|
let err = validate_params(&s, &bad).unwrap_err();
|
|
assert!(err.contains("enum variants"), "got: {err}");
|
|
}
|
|
|
|
#[test]
|
|
fn validate_params_option_accepts_null_and_inner_type() {
|
|
let s = schema(
|
|
"test",
|
|
"fn",
|
|
vec![FieldSchema {
|
|
name: "limit",
|
|
ty: TypeSchema::Option(Box::new(TypeSchema::U64)),
|
|
comment: "",
|
|
required: false,
|
|
}],
|
|
);
|
|
let mut null_p = Map::new();
|
|
null_p.insert("limit".into(), Value::Null);
|
|
assert!(validate_params(&s, &null_p).is_ok());
|
|
|
|
let mut val_p = Map::new();
|
|
val_p.insert("limit".into(), Value::Number(5.into()));
|
|
assert!(validate_params(&s, &val_p).is_ok());
|
|
|
|
let mut bad_p = Map::new();
|
|
bad_p.insert("limit".into(), Value::String("x".into()));
|
|
assert!(validate_params(&s, &bad_p).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn validate_params_json_type_accepts_anything() {
|
|
let s = schema(
|
|
"test",
|
|
"fn",
|
|
vec![FieldSchema {
|
|
name: "payload",
|
|
ty: TypeSchema::Json,
|
|
comment: "",
|
|
required: true,
|
|
}],
|
|
);
|
|
let mut p = Map::new();
|
|
p.insert("payload".into(), Value::Array(vec![Value::Bool(true)]));
|
|
assert!(validate_params(&s, &p).is_ok());
|
|
}
|
|
|
|
// --- validate_registry edge cases ---------------------------------------
|
|
|
|
#[test]
|
|
fn validate_registry_rejects_empty_namespace() {
|
|
let declared = vec![schema("", "fn", vec![])];
|
|
let registered = vec![RegisteredController {
|
|
schema: declared[0].clone(),
|
|
handler: noop_handler,
|
|
}];
|
|
let err = validate_registry(&grouped(registered)).unwrap_err();
|
|
assert!(err.contains("namespace must not be empty"));
|
|
}
|
|
|
|
#[test]
|
|
fn validate_registry_rejects_empty_function() {
|
|
let declared = vec![schema("ns", "", vec![])];
|
|
let registered = vec![RegisteredController {
|
|
schema: declared[0].clone(),
|
|
handler: noop_handler,
|
|
}];
|
|
let err = validate_registry(&grouped(registered)).unwrap_err();
|
|
assert!(err.contains("function must not be empty"));
|
|
}
|
|
|
|
#[test]
|
|
fn validate_registry_rejects_whitespace_only_namespace() {
|
|
// `trim().is_empty()` is the invariant — a namespace of " " must
|
|
// be rejected to prevent `openhuman. _fn` nonsense RPC method names.
|
|
let declared = vec![schema(" ", "fn", vec![])];
|
|
let registered = vec![RegisteredController {
|
|
schema: declared[0].clone(),
|
|
handler: noop_handler,
|
|
}];
|
|
let err = validate_registry(&grouped(registered)).unwrap_err();
|
|
assert!(err.contains("namespace must not be empty"));
|
|
}
|
|
|
|
// Note: the previous `declared_without_registered` / `registered_without_declared`
|
|
// drift tests were removed with the registry collapse (Phase 2) — schemas are now
|
|
// derived from the registered controllers, so the two lists cannot drift.
|
|
|
|
#[test]
|
|
fn validate_registry_rejects_duplicate_registered_controllers() {
|
|
let s = schema("a", "b", vec![]);
|
|
let registered = vec![
|
|
RegisteredController {
|
|
schema: s.clone(),
|
|
handler: noop_handler,
|
|
},
|
|
RegisteredController {
|
|
schema: s,
|
|
handler: noop_handler,
|
|
},
|
|
];
|
|
let err = validate_registry(&grouped(registered)).unwrap_err();
|
|
assert!(err.contains("duplicate registered controller `a.b`"));
|
|
}
|
|
|
|
// --- try_invoke_registered_rpc routing ---------------------------------
|
|
|
|
#[tokio::test]
|
|
async fn try_invoke_registered_rpc_returns_none_for_unknown_method() {
|
|
let out = try_invoke_registered_rpc("openhuman.not_a_real_method_xyz_123", Map::new()).await;
|
|
assert!(out.is_none(), "unknown methods must return None");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn try_invoke_registered_rpc_returns_some_for_known_method() {
|
|
// `openhuman.health_snapshot` is registered at startup and takes no
|
|
// required params — it must route and produce Some(_).
|
|
let out = try_invoke_registered_rpc("openhuman.health_snapshot", Map::new()).await;
|
|
assert!(out.is_some(), "known method must route");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn try_invoke_registered_rpc_routes_security_policy_info() {
|
|
let out = try_invoke_registered_rpc("openhuman.security_policy_info", Map::new())
|
|
.await
|
|
.expect("security policy info should be registered")
|
|
.expect("security policy info should succeed");
|
|
|
|
assert!(
|
|
out.get("result").is_some() || out.get("autonomy").is_some(),
|
|
"security policy info should return policy payload: {out}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rpc_method_name_handles_multi_underscore_function() {
|
|
// Functions often contain underscores — the RPC method name must
|
|
// preserve them verbatim, separated from the namespace with `_`.
|
|
let s = schema("team", "change_member_role", vec![]);
|
|
assert_eq!(rpc_method_name(&s), "openhuman.team_change_member_role");
|
|
}
|
|
|
|
#[test]
|
|
fn every_registered_controller_has_matching_declared_schema() {
|
|
// Global invariant: the registry is consistent by construction.
|
|
// This test re-asserts the contract to catch drift.
|
|
use std::collections::BTreeSet;
|
|
let registered: BTreeSet<String> = all_registered_controllers()
|
|
.into_iter()
|
|
.map(|c| format!("{}.{}", c.schema.namespace, c.schema.function))
|
|
.collect();
|
|
let declared: BTreeSet<String> = all_controller_schemas()
|
|
.into_iter()
|
|
.map(|s| format!("{}.{}", s.namespace, s.function))
|
|
.collect();
|
|
assert_eq!(
|
|
registered, declared,
|
|
"registry/schema sets must be identical"
|
|
);
|
|
}
|
|
|
|
// --- DomainSet registration filter (#4796) ------------------------------
|
|
|
|
use crate::core::runtime::context::CoreContext;
|
|
use crate::core::runtime::DomainSet;
|
|
|
|
/// The [`DomainGroup`] a registered controller (agent-facing OR internal) is
|
|
/// tagged with, looked up by its namespace. Test-only helper over the private
|
|
/// grouped registry.
|
|
fn group_for_namespace(ns: &str) -> Option<DomainGroup> {
|
|
registry()
|
|
.iter()
|
|
.chain(internal_registry().iter())
|
|
.find(|g| g.controller.schema.namespace == ns)
|
|
.map(|g| g.group)
|
|
}
|
|
|
|
#[test]
|
|
fn full_registration_is_byte_identical() {
|
|
// With no ambient CoreContext (⇒ full, no filter), the public
|
|
// `all_registered_controllers()` must equal the raw grouped registry — same
|
|
// length AND same rpc-method-name sequence IN ORDER. This is the DoD (1)
|
|
// proof that wrapping every entry in a `GroupedController` + filtering by the
|
|
// ambient DomainSet changes neither the membership nor the ordering of the
|
|
// full() surface.
|
|
//
|
|
// The baseline is the raw `registry()` view rather than a checked-in method
|
|
// snapshot (a #4808 review suggestion): `all_registered_controllers()` and
|
|
// `registry()` are DIFFERENT code paths — the former exercises the ambient
|
|
// filter (`group_allowed`) and re-collects, the latter is the unfiltered
|
|
// source — so this asserts the filter is an order-preserving identity under
|
|
// full(). A frozen snapshot would instead ossify the controller list and
|
|
// force churn on every legitimate new controller; git history is the
|
|
// authoritative pre-#4796 baseline for "did the raw list itself change".
|
|
let filtered_methods: Vec<String> = all_registered_controllers()
|
|
.iter()
|
|
.map(|c| c.rpc_method_name())
|
|
.collect();
|
|
let raw_methods: Vec<String> = registry()
|
|
.iter()
|
|
.map(|g| g.controller.rpc_method_name())
|
|
.collect();
|
|
|
|
assert_eq!(
|
|
filtered_methods.len(),
|
|
raw_methods.len(),
|
|
"unfiltered all_registered_controllers() must equal raw registry length"
|
|
);
|
|
// Ordered comparison — NOT sorted. A reordering (or a drop/add) under full()
|
|
// would change dispatch/schema iteration order and must fail here.
|
|
assert_eq!(
|
|
filtered_methods, raw_methods,
|
|
"unfiltered rpc-method sequence must be byte-identical (order + membership) to the raw registry"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn harness_excludes_gated_namespaces() {
|
|
use std::collections::BTreeSet;
|
|
|
|
// Baseline (full, no scope) — every family present.
|
|
let full_ns: BTreeSet<&str> = all_controller_schemas()
|
|
.iter()
|
|
.map(|s| s.namespace)
|
|
.collect();
|
|
#[cfg(feature = "flows")]
|
|
assert!(full_ns.contains("flows"), "full() must expose flows");
|
|
// `voice` was the pathfinder gate (#4803) and predates this per-assert cfg
|
|
// convention; gate it like its siblings so the disabled build passes (#5022).
|
|
#[cfg(feature = "voice")]
|
|
assert!(full_ns.contains("voice"), "full() must expose voice");
|
|
#[cfg(feature = "channels")]
|
|
assert!(full_ns.contains("channels"), "full() must expose channels");
|
|
|
|
let ctx = CoreContext::for_test(DomainSet::harness(), None);
|
|
let harness_ns: BTreeSet<&'static str> =
|
|
CoreContext::scope(ctx, async { all_controller_schemas() })
|
|
.await
|
|
.iter()
|
|
.map(|s| s.namespace)
|
|
.collect();
|
|
|
|
// Harness families remain.
|
|
for present in ["memory", "threads", "config", "security", "agent"] {
|
|
assert!(
|
|
harness_ns.contains(present),
|
|
"harness() must keep the `{present}` namespace"
|
|
);
|
|
}
|
|
// Gate families + platform-only namespaces are gone.
|
|
for absent in [
|
|
"flows",
|
|
"voice",
|
|
"skills",
|
|
"wallet",
|
|
"meet",
|
|
"channels",
|
|
"mcp_clients",
|
|
"health",
|
|
] {
|
|
assert!(
|
|
!harness_ns.contains(absent),
|
|
"harness() must omit the gated/platform `{absent}` namespace"
|
|
);
|
|
}
|
|
assert!(
|
|
harness_ns.len() < full_ns.len(),
|
|
"harness() must expose strictly fewer namespaces than full()"
|
|
);
|
|
}
|
|
|
|
// Uses a `flows.*` method as its gated-family vehicle, so the whole test is
|
|
// `#[cfg(feature = "flows")]`: without the feature there is no flows controller
|
|
// in the registry at all and the `.expect()` below would panic. The runtime
|
|
// gating this proves is orthogonal to the compile-time gate, and CI runs the
|
|
// test suite on default features (flows ON), so no coverage is lost there.
|
|
#[cfg(feature = "flows")]
|
|
#[tokio::test]
|
|
async fn dispatch_returns_none_for_gated_method() {
|
|
// A method whose group is gated OFF under the ambient DomainSet must
|
|
// dispatch as an unknown method (None) — indistinguishable from absent.
|
|
let gated_method = all_registered_controllers()
|
|
.into_iter()
|
|
.find(|c| c.schema.namespace == "flows")
|
|
.map(|c| c.rpc_method_name())
|
|
.expect("a flows.* method exists in the full registry");
|
|
|
|
let ctx = CoreContext::for_test(DomainSet::harness(), None);
|
|
let out = CoreContext::scope(ctx, try_invoke_registered_rpc(&gated_method, Map::new())).await;
|
|
assert!(
|
|
out.is_none(),
|
|
"gated method `{gated_method}` must dispatch as None under harness()"
|
|
);
|
|
|
|
// A harness-family method still routes (Some) — security.policy_info needs
|
|
// no workspace, so it is a clean positive control.
|
|
let ctx = CoreContext::for_test(DomainSet::harness(), None);
|
|
let out = CoreContext::scope(
|
|
ctx,
|
|
try_invoke_registered_rpc("openhuman.security_policy_info", Map::new()),
|
|
)
|
|
.await;
|
|
assert!(
|
|
out.is_some(),
|
|
"harness-family security.policy_info must still route under harness()"
|
|
);
|
|
}
|
|
|
|
// Same flows-vehicle reasoning as `dispatch_returns_none_for_gated_method`.
|
|
#[cfg(feature = "flows")]
|
|
#[tokio::test]
|
|
async fn schema_lookup_is_gated_in_lockstep_with_dispatch() {
|
|
// #4808 review: `schema_for_rpc_method` must gate identically to
|
|
// `try_invoke_registered_rpc`, otherwise `invoke_method_inner` validates a
|
|
// gated method's params BEFORE the dispatch gate fires — returning the
|
|
// controller's validation error instead of method-not-found and leaking the
|
|
// hidden RPC surface. Prove the schema lookup returns None for a gated
|
|
// method under harness() (so no validation runs) while a harness-family
|
|
// method still resolves.
|
|
let gated_method = all_registered_controllers()
|
|
.into_iter()
|
|
.find(|c| c.schema.namespace == "flows")
|
|
.map(|c| c.rpc_method_name())
|
|
.expect("a flows.* method exists in the full registry");
|
|
|
|
// Full (no scope): the gated method's schema IS visible — proves the None
|
|
// below is the gate, not a missing method.
|
|
assert!(
|
|
schema_for_rpc_method(&gated_method).is_some(),
|
|
"under full() the schema for `{gated_method}` must resolve"
|
|
);
|
|
|
|
let ctx = CoreContext::for_test(DomainSet::harness(), None);
|
|
let gated_schema =
|
|
CoreContext::scope(ctx, async { schema_for_rpc_method(&gated_method) }).await;
|
|
assert!(
|
|
gated_schema.is_none(),
|
|
"schema lookup for gated `{gated_method}` must be None under harness() (no param validation, no surface leak)"
|
|
);
|
|
|
|
let ctx = CoreContext::for_test(DomainSet::harness(), None);
|
|
let kept_schema = CoreContext::scope(ctx, async {
|
|
schema_for_rpc_method("openhuman.security_policy_info")
|
|
})
|
|
.await;
|
|
assert!(
|
|
kept_schema.is_some(),
|
|
"harness-family security.policy_info schema must still resolve under harness()"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn group_mapping_smoke() {
|
|
// Representative controller from each harness family maps to its group…
|
|
assert_eq!(group_for_namespace("memory"), Some(DomainGroup::Memory));
|
|
assert_eq!(group_for_namespace("threads"), Some(DomainGroup::Threads));
|
|
assert_eq!(group_for_namespace("config"), Some(DomainGroup::Config));
|
|
assert_eq!(group_for_namespace("security"), Some(DomainGroup::Security));
|
|
assert_eq!(group_for_namespace("agent"), Some(DomainGroup::Agent));
|
|
// …and a representative gated one maps to its gate group. `group_for_namespace`
|
|
// reads the real controller registry, so a compile-time-gated family has no
|
|
// entry to map when its feature is off.
|
|
#[cfg(feature = "flows")]
|
|
assert_eq!(group_for_namespace("flows"), Some(DomainGroup::Flows));
|
|
// `group_for_namespace` is registry-derived, so a compile-time-gated domain
|
|
// has no controller to map. Skip when its Cargo feature is off.
|
|
#[cfg(feature = "skills")]
|
|
assert_eq!(group_for_namespace("skills"), Some(DomainGroup::Skills));
|
|
// `voice` predates the per-assert cfg convention (#4803); registry-derived, so
|
|
// it has no entry to map when the feature is off. Gate like its siblings (#5022).
|
|
#[cfg(feature = "voice")]
|
|
assert_eq!(group_for_namespace("voice"), Some(DomainGroup::Voice));
|
|
#[cfg(feature = "web3")]
|
|
assert_eq!(group_for_namespace("wallet"), Some(DomainGroup::Web3));
|
|
// `meet` is compiled out under `--no-default-features`, so the registry has
|
|
// no entry to map (#4800).
|
|
#[cfg(feature = "meet")]
|
|
assert_eq!(group_for_namespace("meet"), Some(DomainGroup::Meet));
|
|
// Internal-only registry is grouped too (mcp_audit → Mcp).
|
|
// Compiled out with the `mcp` feature: `group_for_namespace` reads the LIVE
|
|
// registry, and the gate unregisters the mcp_audit controller entirely.
|
|
#[cfg(feature = "mcp")]
|
|
assert_eq!(group_for_namespace("mcp_audit"), Some(DomainGroup::Mcp));
|
|
}
|
|
|
|
// --- `mcp` compile-time gate (#4799) ------------------------------------
|
|
|
|
/// With the `mcp` feature ON (the default / shipped desktop build), both MCP
|
|
/// namespaces are registered: `mcp_clients` (the dynamic Smithery registry,
|
|
/// agent-facing) and `mcp_audit` (the write-audit log, internal-only).
|
|
///
|
|
/// Paired with `mcp_namespaces_absent_when_gate_off` below so the gate is
|
|
/// pinned in BOTH directions — an assert that only ever runs in one build
|
|
/// configuration cannot prove a gate works.
|
|
#[test]
|
|
#[cfg(feature = "mcp")]
|
|
fn mcp_namespaces_registered_when_gate_on() {
|
|
assert_eq!(
|
|
group_for_namespace("mcp_clients"),
|
|
Some(DomainGroup::Mcp),
|
|
"with `mcp` compiled in, the dynamic registry's `mcp_clients` \
|
|
namespace must be registered"
|
|
);
|
|
assert_eq!(
|
|
group_for_namespace("mcp_audit"),
|
|
Some(DomainGroup::Mcp),
|
|
"with `mcp` compiled in, the internal `mcp_audit` namespace must be \
|
|
registered"
|
|
);
|
|
}
|
|
|
|
/// With the `mcp` feature OFF, both MCP namespaces are gone from the live
|
|
/// registry — every `openhuman.mcp_clients_*` / `openhuman.mcp_audit_*` method
|
|
/// is an unknown method over `/rpc` and absent from `/schema`.
|
|
///
|
|
/// This is the compile-time analogue of the runtime `DomainSet::mcp` filter:
|
|
/// `DomainSet` can hide these namespaces at runtime, this feature removes the
|
|
/// code that backs them altogether. Note the stubs make this work with NO
|
|
/// `#[cfg]` in `src/core/all.rs` — the aggregators simply return empty vecs.
|
|
#[test]
|
|
#[cfg(not(feature = "mcp"))]
|
|
fn mcp_namespaces_absent_when_gate_off() {
|
|
assert_eq!(
|
|
group_for_namespace("mcp_clients"),
|
|
None,
|
|
"with `mcp` compiled out, the `mcp_clients` namespace must not be \
|
|
registered — the stub aggregator returns an empty vec"
|
|
);
|
|
assert_eq!(
|
|
group_for_namespace("mcp_audit"),
|
|
None,
|
|
"with `mcp` compiled out, the internal `mcp_audit` namespace must not \
|
|
be registered — the stub aggregator returns an empty vec"
|
|
);
|
|
}
|
|
|
|
// --- #4797: `flows` compile-time gate (directional proof) -------------------
|
|
//
|
|
// One namespace, not three: `tinyflows` registers no controllers, and
|
|
// `rhai_workflows` is `scope() = AgentOnly` (no controller schemas in v1), so
|
|
// `flows` is the gate's entire controller surface.
|
|
|
|
#[cfg(feature = "flows")]
|
|
#[test]
|
|
fn flows_controllers_registered_when_feature_on() {
|
|
let namespaces: Vec<&str> = all_controller_schemas()
|
|
.iter()
|
|
.map(|s| s.namespace)
|
|
.collect();
|
|
assert!(
|
|
namespaces.contains(&"flows"),
|
|
"with the `flows` feature ON the flows controllers must be registered"
|
|
);
|
|
}
|
|
|
|
#[cfg(not(feature = "flows"))]
|
|
#[test]
|
|
fn flows_controllers_absent_when_feature_off() {
|
|
let namespaces: Vec<&str> = all_controller_schemas()
|
|
.iter()
|
|
.map(|s| s.namespace)
|
|
.collect();
|
|
assert!(
|
|
!namespaces.contains(&"flows"),
|
|
"with the `flows` feature OFF the flows controllers must be absent \
|
|
(unknown-method over /rpc, omitted from /schema)"
|
|
);
|
|
}
|
|
|
|
/// All three Meet namespaces register when the `meet` feature is on (#4800).
|
|
///
|
|
/// Paired with `meet_controllers_absent_when_feature_off` below: together they
|
|
/// pin *both* directions of the compile-time gate. The negative half is the one
|
|
/// that actually proves the gate does something — a gate that never removes
|
|
/// anything would still pass this positive test.
|
|
#[cfg(feature = "meet")]
|
|
#[test]
|
|
fn meet_controllers_registered_when_feature_on() {
|
|
for ns in ["meet", "agent_meetings", "meet_agent"] {
|
|
assert_eq!(
|
|
group_for_namespace(ns),
|
|
Some(DomainGroup::Meet),
|
|
"`{ns}` must register under DomainGroup::Meet when the `meet` feature is on"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// No Meet namespace registers when the `meet` feature is off (#4800).
|
|
///
|
|
/// This is the half that proves the gate: with `meet` compiled out the three
|
|
/// domains must leave zero trace in either the public or the internal registry.
|
|
#[cfg(not(feature = "meet"))]
|
|
#[test]
|
|
fn meet_controllers_absent_when_feature_off() {
|
|
for ns in ["meet", "agent_meetings", "meet_agent"] {
|
|
assert_eq!(
|
|
group_for_namespace(ns),
|
|
None,
|
|
"`{ns}` must not register when the `meet` feature is off"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The external-channel namespace registers when the `channels` feature is on
|
|
/// (#4801).
|
|
///
|
|
/// Paired with `channels_controllers_absent_when_feature_off` below to pin both
|
|
/// directions of the compile-time gate. The webview API/notification bridges
|
|
/// and WhatsApp store have moved to the Tauri shell and expose no core
|
|
/// controllers.
|
|
#[cfg(feature = "channels")]
|
|
#[test]
|
|
fn channels_controllers_registered_when_feature_on() {
|
|
let namespaces: Vec<&str> = all_controller_schemas()
|
|
.iter()
|
|
.map(|s| s.namespace)
|
|
.collect();
|
|
assert!(
|
|
namespaces.contains(&"channels"),
|
|
"with the `channels` feature ON the `channels` controllers must be registered"
|
|
);
|
|
}
|
|
|
|
/// With `channels` compiled out the channel + webview-bridge domains leave zero
|
|
/// trace in the registry (#4801) — while the in-app web chat (`channel`
|
|
/// namespace) stays present, pinning the #5002 decoupling: turning off external
|
|
/// messaging must NOT take down core in-app chat.
|
|
///
|
|
/// This is the half that proves the gate does something. The 3 `whatsapp_data`
|
|
/// agent tools are pinned separately in `tools::ops_tests` (that module has the
|
|
/// full-tool-list machinery); here we assert the controller surface.
|
|
#[cfg(not(feature = "channels"))]
|
|
#[test]
|
|
fn channels_controllers_absent_when_feature_off() {
|
|
let namespaces: Vec<&str> = all_controller_schemas()
|
|
.iter()
|
|
.map(|s| s.namespace)
|
|
.collect();
|
|
assert!(
|
|
!namespaces.contains(&"channels"),
|
|
"with the `channels` feature OFF the `channels` controllers must be absent \
|
|
(unknown-method over /rpc, omitted from /schema)"
|
|
);
|
|
// #5002 decoupling: the in-app web chat controllers (RPC namespace `channel`)
|
|
// are core product surface and must survive the `channels` gate being off.
|
|
assert!(
|
|
namespaces.contains(&"channel"),
|
|
"the in-app web_chat controllers (`channel` namespace) must stay registered \
|
|
even with the `channels` feature OFF (#5002 decoupling)"
|
|
);
|
|
}
|
|
|
|
/// With the `http-server` feature on (the default), the HTTP + Socket.IO
|
|
/// transport is compiled in — `HTTP_SERVER_COMPILED_IN` reflects that, and
|
|
/// `socketioxide` is linked. `socketioxide` is the only dependency this gate
|
|
/// actually sheds (proven by `cargo tree -i socketioxide`); `axum` stays in the
|
|
/// graph either way because `tinychannels` pulls it transitively.
|
|
#[test]
|
|
#[cfg(feature = "http-server")]
|
|
fn http_server_compiled_in_when_feature_on() {
|
|
assert!(crate::core::http_server_status::HTTP_SERVER_COMPILED_IN);
|
|
}
|
|
|
|
/// With the `http-server` feature off, the transport is compiled out: the
|
|
/// marker flips, `serve()` returns without binding a listener, and the
|
|
/// exclusive `socketioxide` dependency leaves the graph (`axum` remains, pulled
|
|
/// transitively by `tinychannels`). The desktop shell's compile-time assert on
|
|
/// this marker (`app/src-tauri/src/lib.rs`) turns a silent listener-less core
|
|
/// into a build failure (cf. voice #4901).
|
|
#[test]
|
|
#[cfg(not(feature = "http-server"))]
|
|
fn http_server_compiled_out_when_feature_off() {
|
|
assert!(!crate::core::http_server_status::HTTP_SERVER_COMPILED_IN);
|
|
}
|
|
|
|
/// With `http-server` on, the `http_host` static-directory server registers its
|
|
/// controllers, so the `http_host.*` RPC surface is present in `/schema`.
|
|
#[test]
|
|
#[cfg(feature = "http-server")]
|
|
fn http_host_controllers_registered_when_http_server_on() {
|
|
let schemas = all_controller_schemas();
|
|
assert!(
|
|
schemas.iter().any(|s| s.namespace == "http_host"),
|
|
"`http_host` controllers must be registered when the `http-server` feature is on"
|
|
);
|
|
}
|
|
|
|
/// With `http-server` off, the whole `http_host` axum domain is compiled out and
|
|
/// its controller-registration push in `core::all` is gated in lockstep, so the
|
|
/// `http_host` namespace never enters the registry (unknown-method over `/rpc`,
|
|
/// absent from `/schema`). This is the negative half that proves the gate
|
|
/// removes the surface.
|
|
#[test]
|
|
#[cfg(not(feature = "http-server"))]
|
|
fn http_host_controllers_absent_when_http_server_off() {
|
|
let schemas = all_controller_schemas();
|
|
assert!(
|
|
!schemas.iter().any(|s| s.namespace == "http_host"),
|
|
"`http_host` controllers must be compiled out when the `http-server` feature is off"
|
|
);
|
|
}
|
|
|
|
/// The `medulla` namespace registers under `DomainGroup::Medulla` when the
|
|
/// `medulla` feature is on.
|
|
///
|
|
/// Paired with the negative below. On its own this proves nothing about the
|
|
/// gate — a gate that removed nothing would still pass it.
|
|
#[cfg(feature = "medulla")]
|
|
#[test]
|
|
fn medulla_controllers_registered_when_feature_on() {
|
|
assert_eq!(
|
|
group_for_namespace("medulla"),
|
|
Some(DomainGroup::Medulla),
|
|
"`medulla` must register under DomainGroup::Medulla when the feature is on"
|
|
);
|
|
}
|
|
|
|
/// The `medulla` namespace leaves no trace when the feature is off.
|
|
///
|
|
/// This is the half that proves the gate removes something. It also pins the
|
|
/// intended off-state: **absence**, so a host sees unknown-method and hides the
|
|
/// surface, rather than a registered controller that fails at call time.
|
|
#[cfg(not(feature = "medulla"))]
|
|
#[test]
|
|
fn medulla_controllers_absent_when_feature_off() {
|
|
assert_eq!(
|
|
group_for_namespace("medulla"),
|
|
None,
|
|
"`medulla` must not register when the feature is off"
|
|
);
|
|
}
|