From c5648d21af0f6d966369390db7f528c643ba9b12 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Wed, 11 Nov 2026 15:23:41 +0530 Subject: [PATCH] Test that a promoted tool's schema gets the provider preparation A tool found through tool_search is promoted with the deferred catalogue's schema, and the catalogue applies RunPolicy::tool_schemas like the direct set does. Nothing pinned that, so a promoted tool could regress to reaching the wire raw. This adds a test. Part of #322 (item f). Deferred tools come only from the registry, so a toolset's per-step PreparedToolSet transform has no promoted tools to apply to. --- .../src/agent_loop/tool_rules_tests.rs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/crates/tinyagents-harness/src/agent_loop/tool_rules_tests.rs b/crates/tinyagents-harness/src/agent_loop/tool_rules_tests.rs index 82ac6e125..ac77a3453 100644 --- a/crates/tinyagents-harness/src/agent_loop/tool_rules_tests.rs +++ b/crates/tinyagents-harness/src/agent_loop/tool_rules_tests.rs @@ -675,3 +675,70 @@ async fn a_rule_can_withhold_tool_search_itself() { assert!(answer.contains("rule 'no-discovery'"), "{answer}"); assert!(!answer.contains("deferred_open"), "{answer}"); } + +/// A tool with one optional property, so the strict sanitizer has something +/// to rewrite (a bare `{"type": "object"}` passes through unchanged). +struct PropTool(&'static str, ToolExposure); + +#[async_trait] +impl Tool for PropTool { + fn name(&self) -> &str { + self.0 + } + fn description(&self) -> &str { + "deferred lookup" + } + fn parameters_schema(&self) -> Value { + json!({"type": "object", "properties": {"q": {"type": "string"}}}) + } + fn exposure(&self) -> ToolExposure { + self.1 + } + async fn execute(&self, _arguments: Value) -> anyhow::Result { + Ok(ToolResult::success(self.0)) + } +} + +#[tokio::test] +async fn a_promoted_tool_reaches_the_wire_prepared_like_a_direct_one() { + // Promotion serves the catalogue's schema, which takes the run's provider + // preparation, so a found tool is never sent raw. + let model = Arc::new(ScriptedModel::new(vec![ + calls(vec![( + "search", + "tool_search", + json!({"query": "deferred"}), + )]), + ModelResponse::assistant("done"), + ])); + let mut harness: AgentHarness<()> = AgentHarness::new(); + harness.with_model_resolver(Arc::new(crate::host::FixedModelResolver::new( + model.clone() as _, + ))); + harness.with_policy(RunPolicy { + tool_schemas: Some(crate::tool::SchemaPreparation { + strict: true, + ..crate::tool::SchemaPreparation::default() + }), + ..RunPolicy::default() + }); + harness.register_tool(Arc::new(PropTool("open", ToolExposure::Direct))); + harness.register_tool(Arc::new(PropTool("deferred_open", ToolExposure::Deferred))); + + run(&harness, "promoted-prepared").await; + + let requests = model.requests(); + let direct = requests[0] + .tools + .iter() + .find(|tool| tool.name == "open") + .expect("direct tool advertised"); + let promoted = requests[1] + .tools + .iter() + .find(|tool| tool.name == "deferred_open") + .expect("the found tool is promoted"); + assert_eq!(direct.parameters["additionalProperties"], json!(false)); + assert_eq!(direct.parameters["required"], json!(["q"])); + assert_eq!(promoted.parameters, direct.parameters); +}