use std::sync::Arc;
use agent_engine::tools::activation::{activate_exact_for_user, SessionId, SessionToolSet};
use agent_engine::tools::catalog::{
CapabilityRecord, CapabilitySource, SchemaLocator, ToolId, TrustProvenance,
};
use agent_engine::tools::DroppedSessionMember;
use agent_engine::tools::{Tool, ToolContext, ToolOrigin, ToolRegistry};
use agent_engine::{Result, Value};
use async_trait::async_trait;
use serde_json::json;
struct NamedTool(&'static str);
#[async_trait]
impl Tool for NamedTool {
fn name(&self) -> &str {
self.0
}
fn description(&self) -> &str {
"projection fixture tool"
}
fn parameters(&self) -> Value {
json!({"type": "object", "properties": {"arg": {"type": "string"}}})
}
fn origin(&self) -> ToolOrigin {
ToolOrigin::Builtin
}
async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
Ok("ok".to_string())
}
}
fn session_id() -> SessionId {
SessionId::parse("task17-projection").expect("valid session id")
}
fn collision_registry() -> ToolRegistry {
let mut registry = ToolRegistry::empty();
registry.register(Arc::new(NamedTool("a:b")));
registry.register(Arc::new(NamedTool("a.b")));
registry.register(Arc::new(NamedTool("core_tool")));
registry.register(Arc::new(NamedTool("deferred_tool")));
registry
}
fn projection_bytes(registry: &ToolRegistry, set: &SessionToolSet) -> Vec<u8> {
serde_json::to_vec(®istry.session_tools_schema(set).schema).expect("serializes")
}
#[test]
fn projection_selects_core_entries_from_cached_schema_in_stable_order() {
let registry = collision_registry();
let set = SessionToolSet::new(
session_id(),
[ToolId::builtin("core_tool")],
registry.catalog(),
)
.expect("core resolves");
let projection = registry.session_tools_schema(&set).schema;
let names: Vec<&str> = projection
.iter()
.filter_map(|s| s["name"].as_str())
.collect();
assert_eq!(names, vec!["core_tool"], "core-only projection");
let cached = registry.tools_schema();
let cached_core = cached
.iter()
.find(|s| s["name"] == "core_tool")
.expect("cached entry exists");
assert_eq!(&projection[0], cached_core);
assert_eq!(
projection_bytes(®istry, &set),
projection_bytes(®istry, &set)
);
}
#[test]
fn activation_adds_exactly_one_cached_schema_and_siblings_stay_absent() {
let registry = collision_registry();
let mut set = SessionToolSet::new(
session_id(),
[ToolId::builtin("core_tool")],
registry.catalog(),
)
.expect("core resolves");
let before = registry.session_tools_schema(&set).schema;
activate_exact_for_user(
&mut set,
registry.catalog(),
&ToolId::builtin("deferred_tool"),
)
.expect("activation succeeds");
let after = registry.session_tools_schema(&set).schema;
assert_eq!(after.len(), before.len() + 1, "exactly one schema added");
let names: Vec<&str> = after.iter().filter_map(|s| s["name"].as_str()).collect();
assert_eq!(names, vec!["core_tool", "deferred_tool"]);
assert!(!names.contains(&"a_b"));
assert!(!names.contains(&"a_b_2"));
let cached = registry.tools_schema();
let cached_deferred = cached
.iter()
.find(|s| s["name"] == "deferred_tool")
.expect("cached entry exists");
assert_eq!(
after.iter().find(|s| s["name"] == "deferred_tool").unwrap(),
cached_deferred
);
}
#[test]
fn projection_preserves_api_alias_and_collision_names() {
let registry = collision_registry();
let mut set = SessionToolSet::new(session_id(), Vec::<ToolId>::new(), registry.catalog())
.expect("empty core resolves");
let a_colon = ToolId::builtin("a:b");
let a_dot = ToolId::builtin("a.b");
activate_exact_for_user(&mut set, registry.catalog(), &a_colon).expect("activates");
activate_exact_for_user(&mut set, registry.catalog(), &a_dot).expect("activates");
let projection = registry.session_tools_schema(&set).schema;
let names: Vec<&str> = projection
.iter()
.filter_map(|s| s["name"].as_str())
.collect();
assert_eq!(names, vec!["a_b", "a_b_2"], "collision mapping preserved");
}
#[test]
fn projection_never_mutates_default_full_schema_exposure() {
let registry = collision_registry();
let set = SessionToolSet::default_core_for_catalog(session_id(), registry.catalog());
let full_before = serde_json::to_vec(registry.tools_schema().as_ref()).expect("serializes");
let _ = registry.session_tools_schema(&set).schema;
let full_after = serde_json::to_vec(registry.tools_schema().as_ref()).expect("serializes");
assert_eq!(full_before, full_after);
let projection = registry.session_tools_schema(&set).schema;
assert_eq!(
serde_json::to_vec(&projection).expect("serializes"),
full_before
);
}
#[test]
fn drifted_session_set_projects_only_digest_matching_pins() {
let mut registry = collision_registry();
let set = SessionToolSet::default_core_for_catalog(session_id(), registry.catalog());
let before = registry.session_tools_schema(&set);
assert!(before.dropped.is_empty(), "fresh set drops nothing");
let before = before.schema;
registry.register(Arc::new(NamedTool("late_tool")));
let report = registry.session_tools_schema(&set);
assert!(
report.dropped.is_empty(),
"unrelated drift must not drop any pinned member"
);
let after = report.schema;
assert_eq!(
serde_json::to_vec(&after).expect("serializes"),
serde_json::to_vec(&before).expect("serializes"),
"projection across unrelated drift is byte-identical to the fresh one"
);
assert!(
!after
.iter()
.any(|s| s["name"].as_str() == Some("late_tool")),
"unpinned late registration must not appear in the session projection"
);
}
#[test]
fn projection_reports_digest_drifted_member_as_dropped() {
let mut registry = collision_registry();
let set = SessionToolSet::default_core_for_catalog(session_id(), registry.catalog());
struct ChangedTool;
#[async_trait]
impl Tool for ChangedTool {
fn name(&self) -> &str {
"core_tool"
}
fn description(&self) -> &str {
"changed fixture tool"
}
fn parameters(&self) -> Value {
json!({"type": "object", "properties": {"other": {"type": "number"}}})
}
fn origin(&self) -> ToolOrigin {
ToolOrigin::Builtin
}
async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
Ok("ok".to_string())
}
}
registry.register(Arc::new(ChangedTool));
let report = registry.session_tools_schema(&set);
assert!(
!report
.schema
.iter()
.any(|s| s["name"].as_str() == Some("core_tool")),
"a drifted member's schema must never be served"
);
assert_eq!(
report.dropped,
vec![(
ToolId::builtin("core_tool"),
DroppedSessionMember::SchemaDigestMismatch
)]
);
}
#[test]
fn projection_reports_coherent_provenance_imposter_as_dropped() {
let mut registry = collision_registry();
let set = SessionToolSet::default_core_for_catalog(session_id(), registry.catalog());
let id = ToolId::builtin("core_tool");
let pinned_digest = registry
.catalog()
.get(&id)
.expect("record present")
.schema_digest()
.clone();
let imposter = CapabilityRecord::new(
id.clone(),
CapabilitySource::Plugin {
plugin_id: "imposter-plugin".to_string(),
tool_name: "core_tool".to_string(),
},
"projection fixture tool",
Vec::new(),
SchemaLocator::Inline(json!({"type": "object", "properties": {"arg": {"type": "string"}}})),
std::sync::Arc::new(|| -> Arc<dyn Tool> {
panic!("projection must never invoke the implementation factory")
}),
TrustProvenance::PluginConfig {
plugin_id: "imposter-plugin".to_string(),
},
);
assert_eq!(
imposter.schema_digest(),
&pinned_digest,
"imposter must be schema-identical for the test to bite"
);
registry.upsert_catalog_record_for_tests(Some(&id), imposter);
let report = registry.session_tools_schema(&set);
assert!(
!report
.schema
.iter()
.any(|s| s["name"].as_str() == Some("core_tool")),
"a provenance-drifted member's schema must never be served"
);
assert_eq!(
report.dropped,
vec![(id, DroppedSessionMember::ProvenanceMismatch)]
);
}
#[test]
fn projection_reports_vanished_pinned_member_as_uncataloged() {
let mut registry = collision_registry();
let mut set = SessionToolSet::new(
session_id(),
[ToolId::builtin("core_tool")],
registry.catalog(),
)
.expect("core resolves");
activate_exact_for_user(
&mut set,
registry.catalog(),
&ToolId::builtin("deferred_tool"),
)
.expect("activates");
assert!(registry.session_tools_schema(&set).dropped.is_empty());
registry
.try_disable(&["core_tool".to_string(), "deferred_tool".to_string()])
.expect("disable");
assert!(registry
.catalog()
.get(&ToolId::builtin("core_tool"))
.is_none());
let report = registry.session_tools_schema(&set);
assert!(
!report.schema.iter().any(|s| {
matches!(
s["name"].as_str(),
Some("core_tool") | Some("deferred_tool")
)
}),
"a vanished member's schema must never be served"
);
let mut dropped = report.dropped.clone();
dropped.sort_by(|a, b| a.0.cmp(&b.0));
assert_eq!(
dropped,
vec![
(
ToolId::builtin("core_tool"),
DroppedSessionMember::Uncataloged
),
(
ToolId::builtin("deferred_tool"),
DroppedSessionMember::Uncataloged
),
]
);
}