//! Tests for the registry, dispatch, the result cap and the fake tools. Do not edit. mod support; use loopd::tools::{Dispatch, FakeTools, Registry, ToolPort, cap_result, dispatch}; use proto::{CallId, SessionId, ToolRequest, ToolResponse}; fn req(tool: &str, arguments: &str) -> ToolRequest { ToolRequest { session: SessionId::new("s").unwrap(), call: CallId(1), tool: tool.to_string(), arguments: arguments.to_string(), } } #[test] fn the_core_schemas_are_the_fixed_tools_array() { let names: Vec = Registry::m2b() .core_schemas() .into_iter() .map(|s| s.name) .collect(); assert_eq!(names, ["clock", "find_tool", "call_tool"]); let find = &Registry::m2b().core_schemas()[1]; assert_eq!(find.parameters["required"], serde_json::json!(["query"])); let call = &Registry::m2b().core_schemas()[2]; assert_eq!( call.parameters["required"], serde_json::json!(["name", "arguments"]) ); } #[test] fn find_matches_name_or_description_case_insensitively() { let r = Registry::m2b(); let names = |q: &str| r.find(q).iter().map(|s| s.name.clone()).collect::>(); assert_eq!(names("echo"), ["echo"]); assert_eq!(names("ECHO"), ["echo"]); assert_eq!(names("unchanged"), ["echo"], "matches the description too"); assert_eq!(names("time"), ["clock"]); assert!(names("weather").is_empty()); assert!( names("").is_empty(), "an empty query matches nothing, not everything" ); assert!(names(" ").is_empty()); } #[test] fn find_tool_is_answered_locally() { let r = Registry::m2b(); match dispatch(&r, "find_tool", r#"{"query":"echo"}"#) { Dispatch::Local(text) => { assert!(text.starts_with("1 tool(s) match:\n"), "{text}"); assert!(text.contains("\"name\":\"echo\""), "{text}"); assert!( text.contains("\"parameters\""), "the schema is included: {text}" ); assert!(text.ends_with("Call it with call_tool."), "{text}"); } other => panic!("{other:?}"), } assert_eq!( dispatch(&r, "find_tool", r#"{"query":"weather"}"#), Dispatch::Local("No tool matches.".to_string()) ); assert!( matches!(dispatch(&r, "find_tool", "not json"), Dispatch::Local(t) if t == "No tool matches.") ); } #[test] fn call_tool_unwraps_a_known_non_core_tool_and_nothing_else() { let r = Registry::m2b(); let args = r#"{"name":"echo","arguments":{"text": "box"}}"#; match dispatch(&r, "call_tool", args) { Dispatch::Port { tool, arguments } => { assert_eq!(tool, "echo"); assert_eq!( serde_json::from_str::(&arguments).unwrap(), serde_json::json!({"text": "box"}) ); } other => panic!("{other:?}"), } let cases = [ ( r#"{"name":"weather","arguments":{}}"#, "No tool named \"weather\"", ), (r#"{"name":"clock","arguments":{}}"#, "core tool"), (r#"{"arguments":{}}"#, "No tool named \"\""), ("not json", "needs a JSON object"), ]; for (args, want) in cases { match dispatch(&r, "call_tool", args) { Dispatch::Local(text) => assert!(text.contains(want), "{args}: {text}"), other => panic!("{args}: {other:?}, must never reach the port"), } } } #[test] fn any_other_tool_goes_to_the_port_as_it_is() { let r = Registry::m2b(); let want = Dispatch::Port { tool: "clock".to_string(), arguments: "{}".to_string(), }; assert_eq!(dispatch(&r, "clock", "{}"), want); // Even one the registry does not know: the port (brokerd) decides, not loopd. let want = Dispatch::Port { tool: "read_file".to_string(), arguments: r#"{"path":"/x"}"#.to_string(), }; assert_eq!(dispatch(&r, "read_file", r#"{"path":"/x"}"#), want); } #[test] fn results_are_cut_on_a_character_boundary_and_marked() { assert_eq!(cap_result("short", 100), ("short".to_string(), false)); assert_eq!(cap_result("exactly", 7), ("exactly".to_string(), false)); let (text, truncated) = cap_result("abcdefghij", 4); assert_eq!(text, "abcd\n[truncated]"); assert!(truncated); // 3 ASCII bytes, then 2-byte characters: byte 4 is inside a character. let (text, truncated) = cap_result("abc\u{e9}\u{e9}\u{e9}", 4); assert_eq!(text, "abc\n[truncated]"); assert!(truncated); assert_eq!(cap_result("", 0), (String::new(), false)); assert_eq!(cap_result("x", 0), ("\n[truncated]".to_string(), true)); } #[test] fn fake_tools_answer_clock_and_echo_and_record_calls() { let fake = FakeTools::new(); match fake.call(&req("clock", "{}")) { ToolResponse::Result { content, class, untrusted, truncated, } => { assert!( proto::Timestamp::parse(&content).is_ok(), "an RFC 3339 time: {content}" ); assert_eq!(class, proto::DataClass::Public); assert!(!untrusted && !truncated); } other => panic!("{other:?}"), } match fake.call(&req("echo", r#"{"text":"box"}"#)) { ToolResponse::Result { content, .. } => assert_eq!(content, "box"), other => panic!("{other:?}"), } assert!(matches!( fake.call(&req("echo", r#"{"tex":"box"}"#)), ToolResponse::Failed { .. } )); assert!(matches!( fake.call(&req("weather", "{}")), ToolResponse::Denied { .. } )); assert_eq!(fake.calls().len(), 4); assert_eq!(fake.calls()[1].tool, "echo"); }