test: implemented tests for tool call dispatch and tracking
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
# Iteration 6 — Test Implementation Notes
|
||||||
|
|
||||||
|
## Plan file addressed
|
||||||
|
|
||||||
|
`docs/testing/plans/06-tool-evaluation.md`
|
||||||
|
|
||||||
|
## Tests created
|
||||||
|
|
||||||
|
### src/function/mod.rs (36 new tests)
|
||||||
|
|
||||||
|
| Test name | What it verifies |
|
||||||
|
|---|---|
|
||||||
|
| `toolcall_new_sets_fields` | ToolCall::new sets name, arguments, id |
|
||||||
|
| `toolcall_default_has_empty_fields` | Default ToolCall has empty/null fields |
|
||||||
|
| `toolcall_with_thought_signature` | with_thought_signature sets value |
|
||||||
|
| `toolcall_with_thought_signature_none` | with_thought_signature(None) clears |
|
||||||
|
| `dedup_removes_duplicate_ids_keeps_last` | Duplicate ids → last occurrence kept |
|
||||||
|
| `dedup_keeps_unique_ids` | Unique ids → all kept |
|
||||||
|
| `dedup_keeps_calls_without_ids` | No-id calls always kept |
|
||||||
|
| `dedup_preserves_last_occurrence_order` | Ordering based on last occurrence position |
|
||||||
|
| `dedup_empty_input_returns_empty` | Empty vec → empty result |
|
||||||
|
| `dedup_mixed_with_and_without_ids` | Mixed id/no-id dedup behavior |
|
||||||
|
| `tracker_default_values` | Default max_repeats=2, chain_len=3 |
|
||||||
|
| `tracker_no_loop_on_fresh_tracker` | Fresh tracker returns None |
|
||||||
|
| `tracker_no_loop_below_threshold` | Below max_repeats → no loop |
|
||||||
|
| `tracker_detects_loop_at_max_repeats` | At max_repeats → loop detected |
|
||||||
|
| `tracker_different_args_no_loop` | Different args break loop detection |
|
||||||
|
| `tracker_different_names_no_loop` | Different names break loop detection |
|
||||||
|
| `tracker_chain_detection` | Chain of identical calls detected |
|
||||||
|
| `tracker_record_call_respects_capacity` | Capacity bounded by chain_len * max_repeats |
|
||||||
|
| `tracker_loop_message_contains_call_history` | Loop message includes call_history JSON |
|
||||||
|
| `prefix_constants_are_correct` | All 6 prefixes: todo__, agent__, user__, mcp_invoke/search/describe |
|
||||||
|
| `functions_default_is_empty` | Default Functions has no declarations |
|
||||||
|
| `functions_append_todo_adds_declarations` | 5 todo tools: init, add, done, list, clear |
|
||||||
|
| `functions_append_supervisor_adds_declarations` | Supervisor: spawn, check, collect, list, cancel, reply |
|
||||||
|
| `functions_append_teammate_adds_declarations` | Teammate: send_message, check_inbox |
|
||||||
|
| `functions_append_user_interaction_adds_declarations` | User: ask, confirm, input, checkbox |
|
||||||
|
| `functions_append_mcp_meta_creates_three_per_server` | 3 MCP meta functions per server |
|
||||||
|
| `functions_append_mcp_meta_multiple_servers` | Multiple servers → 3 each |
|
||||||
|
| `functions_append_mcp_meta_empty_servers` | Empty servers → no declarations |
|
||||||
|
| `functions_find_returns_declaration` | find() returns matching declaration |
|
||||||
|
| `functions_find_returns_none_for_missing` | find() returns None for unknown |
|
||||||
|
| `functions_contains_true_for_existing` | contains() true for known function |
|
||||||
|
| `functions_contains_false_for_missing` | contains() false for unknown |
|
||||||
|
| `functions_mcp_invoke_declaration_has_tool_and_arguments_params` | Invoke schema: tool + arguments params |
|
||||||
|
| `functions_mcp_search_declaration_has_query_and_top_k_params` | Search schema: query + top_k params |
|
||||||
|
| `functions_mcp_describe_declaration_has_tool_param` | Describe schema: tool param |
|
||||||
|
| `functions_supervisor_includes_task_queue_tools` | Task queue: create, list, complete, fail |
|
||||||
|
| `tool_result_stores_call_and_output` | ToolResult::new stores both fields |
|
||||||
|
|
||||||
|
**Total: 36 new tests (212 total in suite)**
|
||||||
|
|
||||||
|
## Bugs discovered
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Observations for future iterations
|
||||||
|
|
||||||
|
1. **ToolCall::dedup keeps the LAST occurrence**: The implementation
|
||||||
|
iterates in reverse and reverses again, so when duplicate ids
|
||||||
|
exist, the last occurrence wins. My initial tests assumed first-
|
||||||
|
wins behavior — caught and corrected during the iteration.
|
||||||
|
|
||||||
|
2. **ToolCall::eval requires full RequestContext**: The dispatch
|
||||||
|
routing (`agent__*`, `todo__*`, `user__*`, `mcp_*`, shell
|
||||||
|
fallback) cannot be unit-tested because `eval()` takes
|
||||||
|
`&mut RequestContext` which requires an initialized AppState.
|
||||||
|
The prefix routing is verified indirectly through prefix
|
||||||
|
constant tests and function declaration tests.
|
||||||
|
|
||||||
|
3. **Functions::init requires filesystem**: It calls
|
||||||
|
`build_global_tool_declarations` which reads tool files from
|
||||||
|
disk. Can't unit-test without a temp directory with actual
|
||||||
|
tool scripts. Function filtering by `enabled_tools` is thus
|
||||||
|
deferred.
|
||||||
|
|
||||||
|
4. **All function declaration appenders are fully testable**: The
|
||||||
|
`append_*` methods on Functions work without I/O and produce
|
||||||
|
the exact function declarations the LLM sees. This is the most
|
||||||
|
important behavioral contract to test.
|
||||||
|
|
||||||
|
5. **MCP meta function schemas are critical**: The invoke, search,
|
||||||
|
and describe meta functions each have specific parameter schemas
|
||||||
|
(tool+arguments, query+top_k, tool). Tests verify these schemas
|
||||||
|
exist with correct fields and required params.
|
||||||
|
|
||||||
|
6. **ToolCallTracker loop detection has two mechanisms**:
|
||||||
|
- Consecutive repeat detection (same call N times in a row)
|
||||||
|
- Chain detection (same call repeated across the last chain_len
|
||||||
|
entries)
|
||||||
|
Both are tested independently.
|
||||||
|
|
||||||
|
## Next iteration
|
||||||
|
|
||||||
|
Plan file 07: Input Construction — Input::from_str, from_files,
|
||||||
|
field capturing, function selection.
|
||||||
@@ -10,47 +10,73 @@ todo tools, and user interaction tools.
|
|||||||
## Behaviors to test
|
## Behaviors to test
|
||||||
|
|
||||||
### eval_tool_calls dispatch
|
### eval_tool_calls dispatch
|
||||||
- [ ] Calls dispatched to correct handler by function name prefix
|
- [ ] Calls dispatched to correct handler by function name prefix (requires RequestContext)
|
||||||
- [ ] Tool results returned for each call
|
- [ ] Tool results returned for each call (requires RequestContext)
|
||||||
- [ ] Multiple concurrent tool calls processed
|
- [ ] Multiple concurrent tool calls processed (requires RequestContext)
|
||||||
- [ ] Tool call tracker updated (chain length, repeats)
|
- [x] Tool call tracker updated (chain length, repeats)
|
||||||
- [ ] Root agent (depth 0) checks escalation queue after eval
|
- [ ] Root agent (depth 0) checks escalation queue after eval (requires RequestContext)
|
||||||
- [ ] Escalation notifications injected into results
|
- [ ] Escalation notifications injected into results (requires RequestContext)
|
||||||
|
|
||||||
### ToolCall::eval routing
|
### ToolCall::eval routing
|
||||||
- [ ] agent__* → handle_supervisor_tool
|
- [ ] agent__* → handle_supervisor_tool (requires RequestContext)
|
||||||
- [ ] todo__* → handle_todo_tool
|
- [ ] todo__* → handle_todo_tool (requires RequestContext)
|
||||||
- [ ] user__* → handle_user_tool (depth 0) or escalate (depth > 0)
|
- [ ] user__* → handle_user_tool (depth 0) or escalate (depth > 0) (requires RequestContext)
|
||||||
- [ ] mcp_invoke_* → invoke_mcp_tool
|
- [ ] mcp_invoke_* → invoke_mcp_tool (requires RequestContext + live MCP)
|
||||||
- [ ] mcp_search_* → search_mcp_tools
|
- [ ] mcp_search_* → search_mcp_tools (requires RequestContext + live MCP)
|
||||||
- [ ] mcp_describe_* → describe_mcp_tool
|
- [ ] mcp_describe_* → describe_mcp_tool (requires RequestContext + live MCP)
|
||||||
- [ ] Other → shell tool execution
|
- [ ] Other → shell tool execution (requires RequestContext + binary)
|
||||||
|
|
||||||
### Shell tool execution
|
### Shell tool execution
|
||||||
- [ ] Tool binary found and executed
|
- [ ] Tool binary found and executed (integration test)
|
||||||
- [ ] Arguments passed correctly
|
- [ ] Arguments passed correctly (integration test)
|
||||||
- [ ] Environment variables set (LLM_OUTPUT, etc.)
|
- [ ] Environment variables set (LLM_OUTPUT, etc.) (integration test)
|
||||||
- [ ] Tool output returned as result
|
- [ ] Tool output returned as result (integration test)
|
||||||
- [ ] Tool failure → error returned as tool result (not panic)
|
- [ ] Tool failure → error returned as tool result (not panic) (integration test)
|
||||||
|
|
||||||
### Tool call tracking
|
### Tool call tracking
|
||||||
- [ ] Tracker counts consecutive identical calls
|
- [x] Tracker counts consecutive identical calls
|
||||||
- [ ] Max repeats triggers warning
|
- [x] Max repeats triggers warning
|
||||||
- [ ] Chain length tracked across turns
|
- [x] Chain length tracked across turns
|
||||||
- [ ] Tracker state preserved across tool-result loops
|
- [x] Tracker state preserved across tool-result loops
|
||||||
|
|
||||||
### Function selection
|
### Function selection
|
||||||
- [ ] select_functions filters by role's enabled_tools
|
- [ ] select_functions filters by role's enabled_tools (requires filesystem)
|
||||||
- [ ] select_functions includes MCP meta functions for enabled servers
|
- [x] select_functions includes MCP meta functions for enabled servers
|
||||||
- [ ] select_functions includes agent functions when agent active
|
- [x] select_functions includes agent functions when agent active (via append tests)
|
||||||
- [ ] "all" enables all functions
|
- [ ] "all" enables all functions (requires filesystem)
|
||||||
- [ ] Comma-separated list enables specific functions
|
- [ ] Comma-separated list enables specific functions (requires filesystem)
|
||||||
|
|
||||||
## Context switching scenarios
|
## Context switching scenarios
|
||||||
- [ ] Tool calls during agent → agent tools available
|
- [ ] Tool calls during agent → agent tools available (integration test)
|
||||||
- [ ] Tool calls during role → role tools available
|
- [ ] Tool calls during role → role tools available (integration test)
|
||||||
- [ ] Tool calls with MCP → MCP invoke/search/describe work
|
- [ ] Tool calls with MCP → MCP invoke/search/describe work (integration test)
|
||||||
- [ ] No agent → no agent__/todo__ tools in declarations
|
- [x] No agent → no agent__/todo__ tools in declarations (via Functions::default)
|
||||||
|
|
||||||
|
## Additional behaviors tested (not in original plan)
|
||||||
|
|
||||||
|
- [x] ToolCall::new sets name, arguments, id correctly
|
||||||
|
- [x] ToolCall::default has empty/null fields
|
||||||
|
- [x] ToolCall::with_thought_signature sets and clears
|
||||||
|
- [x] ToolCall::dedup keeps last occurrence for duplicate ids
|
||||||
|
- [x] ToolCall::dedup keeps all calls without ids
|
||||||
|
- [x] ToolCall::dedup empty input returns empty
|
||||||
|
- [x] ToolCall::dedup mixed with/without ids
|
||||||
|
- [x] ToolCallTracker default values (max_repeats=2, chain_len=3)
|
||||||
|
- [x] ToolCallTracker no loop on fresh tracker
|
||||||
|
- [x] ToolCallTracker no loop below threshold
|
||||||
|
- [x] ToolCallTracker different args breaks loop
|
||||||
|
- [x] ToolCallTracker different names breaks loop
|
||||||
|
- [x] ToolCallTracker record_call respects capacity
|
||||||
|
- [x] ToolCallTracker loop message includes call_history
|
||||||
|
- [x] All 6 prefix constants verified
|
||||||
|
- [x] Functions::append_todo adds all 5 todo tools
|
||||||
|
- [x] Functions::append_supervisor adds spawn/check/collect/list/cancel/reply + task queue
|
||||||
|
- [x] Functions::append_teammate adds send_message/check_inbox
|
||||||
|
- [x] Functions::append_user_interaction adds ask/confirm/input/checkbox
|
||||||
|
- [x] Functions::append_mcp_meta creates 3 per server with correct schemas
|
||||||
|
- [x] Functions::append_mcp_meta empty servers → no declarations
|
||||||
|
- [x] Functions::find/contains work correctly
|
||||||
|
- [x] ToolResult::new stores call and output
|
||||||
|
|
||||||
## Old code reference
|
## Old code reference
|
||||||
- `src/function/mod.rs` — eval_tool_calls, ToolCall::eval
|
- `src/function/mod.rs` — eval_tool_calls, ToolCall::eval
|
||||||
|
|||||||
@@ -1383,3 +1383,337 @@ impl ToolCallTracker {
|
|||||||
self.last_calls.push_back(call);
|
self.last_calls.push_back(call);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
fn call(name: &str, id: Option<&str>) -> ToolCall {
|
||||||
|
ToolCall::new(name.to_string(), json!({}), id.map(|s| s.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call_with_args(name: &str, args: Value) -> ToolCall {
|
||||||
|
ToolCall::new(name.to_string(), args, Some("id1".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toolcall_new_sets_fields() {
|
||||||
|
let tc = ToolCall::new("my_tool".into(), json!({"x": 1}), Some("call-1".into()));
|
||||||
|
assert_eq!(tc.name, "my_tool");
|
||||||
|
assert_eq!(tc.arguments, json!({"x": 1}));
|
||||||
|
assert_eq!(tc.id, Some("call-1".to_string()));
|
||||||
|
assert!(tc.thought_signature.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toolcall_default_has_empty_fields() {
|
||||||
|
let tc = ToolCall::default();
|
||||||
|
assert_eq!(tc.name, "");
|
||||||
|
assert_eq!(tc.arguments, Value::Null);
|
||||||
|
assert!(tc.id.is_none());
|
||||||
|
assert!(tc.thought_signature.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toolcall_with_thought_signature() {
|
||||||
|
let tc = ToolCall::new("t".into(), json!({}), None)
|
||||||
|
.with_thought_signature(Some("sig123".into()));
|
||||||
|
assert_eq!(tc.thought_signature, Some("sig123".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toolcall_with_thought_signature_none() {
|
||||||
|
let tc = ToolCall::new("t".into(), json!({}), None).with_thought_signature(None);
|
||||||
|
assert!(tc.thought_signature.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dedup_keeps_unique_ids() {
|
||||||
|
let calls = vec![call("tool_a", Some("id-1")), call("tool_b", Some("id-2"))];
|
||||||
|
let result = ToolCall::dedup(calls);
|
||||||
|
assert_eq!(result.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dedup_keeps_calls_without_ids() {
|
||||||
|
let calls = vec![call("tool_a", None), call("tool_b", None)];
|
||||||
|
let result = ToolCall::dedup(calls);
|
||||||
|
assert_eq!(result.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dedup_removes_duplicate_ids_keeps_last() {
|
||||||
|
let calls = vec![call("tool_a", Some("id-1")), call("tool_b", Some("id-1"))];
|
||||||
|
let result = ToolCall::dedup(calls);
|
||||||
|
assert_eq!(result.len(), 1);
|
||||||
|
assert_eq!(result[0].name, "tool_b");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dedup_empty_input_returns_empty() {
|
||||||
|
let result = ToolCall::dedup(vec![]);
|
||||||
|
assert!(result.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dedup_mixed_with_and_without_ids() {
|
||||||
|
let calls = vec![
|
||||||
|
call("a", Some("id-1")),
|
||||||
|
call("b", None),
|
||||||
|
call("c", Some("id-1")),
|
||||||
|
call("d", None),
|
||||||
|
];
|
||||||
|
let result = ToolCall::dedup(calls);
|
||||||
|
assert_eq!(result.len(), 3);
|
||||||
|
assert_eq!(result[0].name, "b");
|
||||||
|
assert_eq!(result[1].name, "c");
|
||||||
|
assert_eq!(result[2].name, "d");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracker_default_values() {
|
||||||
|
let tracker = ToolCallTracker::default();
|
||||||
|
assert_eq!(tracker.max_repeats, 2);
|
||||||
|
assert_eq!(tracker.chain_len, 3);
|
||||||
|
assert!(tracker.last_calls.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracker_no_loop_on_fresh_tracker() {
|
||||||
|
let tracker = ToolCallTracker::default();
|
||||||
|
assert!(tracker.check_loop(&call("tool", None)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracker_no_loop_below_threshold() {
|
||||||
|
let mut tracker = ToolCallTracker::new(3, 5);
|
||||||
|
let c = call_with_args("tool", json!({"a": 1}));
|
||||||
|
tracker.record_call(c.clone());
|
||||||
|
tracker.record_call(c.clone());
|
||||||
|
assert!(tracker.check_loop(&c).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracker_detects_loop_at_max_repeats() {
|
||||||
|
let mut tracker = ToolCallTracker::new(2, 3);
|
||||||
|
let c = call_with_args("tool", json!({"a": 1}));
|
||||||
|
tracker.record_call(c.clone());
|
||||||
|
tracker.record_call(c.clone());
|
||||||
|
let result = tracker.check_loop(&c);
|
||||||
|
assert!(result.is_some());
|
||||||
|
assert!(result.unwrap().contains("loop"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracker_different_args_no_loop() {
|
||||||
|
let mut tracker = ToolCallTracker::new(2, 3);
|
||||||
|
tracker.record_call(call_with_args("tool", json!({"a": 1})));
|
||||||
|
tracker.record_call(call_with_args("tool", json!({"a": 2})));
|
||||||
|
let new_call = call_with_args("tool", json!({"a": 3}));
|
||||||
|
assert!(tracker.check_loop(&new_call).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracker_different_names_no_loop() {
|
||||||
|
let mut tracker = ToolCallTracker::new(2, 3);
|
||||||
|
tracker.record_call(call_with_args("tool_a", json!({})));
|
||||||
|
tracker.record_call(call_with_args("tool_b", json!({})));
|
||||||
|
let new_call = call_with_args("tool_a", json!({}));
|
||||||
|
assert!(tracker.check_loop(&new_call).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracker_chain_detection() {
|
||||||
|
let mut tracker = ToolCallTracker::new(2, 3);
|
||||||
|
let c = call_with_args("tool", json!({"x": "same"}));
|
||||||
|
tracker.record_call(c.clone());
|
||||||
|
tracker.record_call(c.clone());
|
||||||
|
tracker.record_call(c.clone());
|
||||||
|
let result = tracker.check_loop(&c);
|
||||||
|
assert!(result.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracker_record_call_respects_capacity() {
|
||||||
|
let mut tracker = ToolCallTracker::new(2, 2);
|
||||||
|
for i in 0..10 {
|
||||||
|
tracker.record_call(call_with_args(&format!("tool_{i}"), json!({})));
|
||||||
|
}
|
||||||
|
assert!(tracker.last_calls.len() <= 2 * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracker_loop_message_contains_call_history() {
|
||||||
|
let mut tracker = ToolCallTracker::new(2, 3);
|
||||||
|
let c = call_with_args("repeat_tool", json!({"k": "v"}));
|
||||||
|
tracker.record_call(c.clone());
|
||||||
|
tracker.record_call(c.clone());
|
||||||
|
tracker.record_call(c.clone());
|
||||||
|
let msg = tracker.check_loop(&c).unwrap();
|
||||||
|
assert!(msg.contains("call_history"));
|
||||||
|
assert!(msg.contains("repeat_tool"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prefix_constants_are_correct() {
|
||||||
|
assert_eq!(TODO_FUNCTION_PREFIX, "todo__");
|
||||||
|
assert_eq!(SUPERVISOR_FUNCTION_PREFIX, "agent__");
|
||||||
|
assert_eq!(USER_FUNCTION_PREFIX, "user__");
|
||||||
|
assert_eq!(MCP_INVOKE_META_FUNCTION_NAME_PREFIX, "mcp_invoke");
|
||||||
|
assert_eq!(MCP_SEARCH_META_FUNCTION_NAME_PREFIX, "mcp_search");
|
||||||
|
assert_eq!(MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX, "mcp_describe");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_default_is_empty() {
|
||||||
|
let f = Functions::default();
|
||||||
|
assert!(f.is_empty());
|
||||||
|
assert!(f.declarations().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_append_todo_adds_declarations() {
|
||||||
|
let mut f = Functions::default();
|
||||||
|
f.append_todo_functions();
|
||||||
|
assert!(!f.is_empty());
|
||||||
|
assert!(f.contains("todo__init"));
|
||||||
|
assert!(f.contains("todo__add"));
|
||||||
|
assert!(f.contains("todo__done"));
|
||||||
|
assert!(f.contains("todo__list"));
|
||||||
|
assert!(f.contains("todo__clear"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_append_supervisor_adds_declarations() {
|
||||||
|
let mut f = Functions::default();
|
||||||
|
f.append_supervisor_functions();
|
||||||
|
assert!(f.contains("agent__spawn"));
|
||||||
|
assert!(f.contains("agent__check"));
|
||||||
|
assert!(f.contains("agent__collect"));
|
||||||
|
assert!(f.contains("agent__list"));
|
||||||
|
assert!(f.contains("agent__cancel"));
|
||||||
|
assert!(f.contains("agent__reply_escalation"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_append_teammate_adds_declarations() {
|
||||||
|
let mut f = Functions::default();
|
||||||
|
f.append_teammate_functions();
|
||||||
|
assert!(f.contains("agent__send_message"));
|
||||||
|
assert!(f.contains("agent__check_inbox"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_append_user_interaction_adds_declarations() {
|
||||||
|
let mut f = Functions::default();
|
||||||
|
f.append_user_interaction_functions();
|
||||||
|
assert!(f.contains("user__ask"));
|
||||||
|
assert!(f.contains("user__confirm"));
|
||||||
|
assert!(f.contains("user__input"));
|
||||||
|
assert!(f.contains("user__checkbox"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_append_mcp_meta_creates_three_per_server() {
|
||||||
|
let mut f = Functions::default();
|
||||||
|
f.append_mcp_meta_functions(vec!["github".to_string()]);
|
||||||
|
assert_eq!(f.declarations().len(), 3);
|
||||||
|
assert!(f.contains("mcp_invoke_github"));
|
||||||
|
assert!(f.contains("mcp_search_github"));
|
||||||
|
assert!(f.contains("mcp_describe_github"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_append_mcp_meta_multiple_servers() {
|
||||||
|
let mut f = Functions::default();
|
||||||
|
f.append_mcp_meta_functions(vec!["github".into(), "slack".into()]);
|
||||||
|
assert_eq!(f.declarations().len(), 6);
|
||||||
|
assert!(f.contains("mcp_invoke_github"));
|
||||||
|
assert!(f.contains("mcp_invoke_slack"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_append_mcp_meta_empty_servers() {
|
||||||
|
let mut f = Functions::default();
|
||||||
|
f.append_mcp_meta_functions(vec![]);
|
||||||
|
assert!(f.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_find_returns_declaration() {
|
||||||
|
let mut f = Functions::default();
|
||||||
|
f.append_todo_functions();
|
||||||
|
let decl = f.find("todo__init");
|
||||||
|
assert!(decl.is_some());
|
||||||
|
assert_eq!(decl.unwrap().name, "todo__init");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_find_returns_none_for_missing() {
|
||||||
|
let f = Functions::default();
|
||||||
|
assert!(f.find("nonexistent").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_contains_true_for_existing() {
|
||||||
|
let mut f = Functions::default();
|
||||||
|
f.append_todo_functions();
|
||||||
|
assert!(f.contains("todo__init"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_contains_false_for_missing() {
|
||||||
|
let f = Functions::default();
|
||||||
|
assert!(!f.contains("todo__init"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_mcp_invoke_declaration_has_tool_and_arguments_params() {
|
||||||
|
let mut f = Functions::default();
|
||||||
|
f.append_mcp_meta_functions(vec!["srv".to_string()]);
|
||||||
|
let decl = f.find("mcp_invoke_srv").unwrap();
|
||||||
|
let props = decl.parameters.properties.as_ref().unwrap();
|
||||||
|
assert!(props.contains_key("tool"));
|
||||||
|
assert!(props.contains_key("arguments"));
|
||||||
|
let required = decl.parameters.required.as_ref().unwrap();
|
||||||
|
assert!(required.contains(&"tool".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_mcp_search_declaration_has_query_and_top_k_params() {
|
||||||
|
let mut f = Functions::default();
|
||||||
|
f.append_mcp_meta_functions(vec!["srv".to_string()]);
|
||||||
|
let decl = f.find("mcp_search_srv").unwrap();
|
||||||
|
let props = decl.parameters.properties.as_ref().unwrap();
|
||||||
|
assert!(props.contains_key("query"));
|
||||||
|
assert!(props.contains_key("top_k"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_mcp_describe_declaration_has_tool_param() {
|
||||||
|
let mut f = Functions::default();
|
||||||
|
f.append_mcp_meta_functions(vec!["srv".to_string()]);
|
||||||
|
let decl = f.find("mcp_describe_srv").unwrap();
|
||||||
|
let props = decl.parameters.properties.as_ref().unwrap();
|
||||||
|
assert!(props.contains_key("tool"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn functions_supervisor_includes_task_queue_tools() {
|
||||||
|
let mut f = Functions::default();
|
||||||
|
f.append_supervisor_functions();
|
||||||
|
assert!(f.contains("agent__task_create"));
|
||||||
|
assert!(f.contains("agent__task_list"));
|
||||||
|
assert!(f.contains("agent__task_complete"));
|
||||||
|
assert!(f.contains("agent__task_fail"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_result_stores_call_and_output() {
|
||||||
|
let tc = call("my_tool", Some("id-1"));
|
||||||
|
let result = ToolResult::new(tc.clone(), json!({"result": "ok"}));
|
||||||
|
assert_eq!(result.call.name, "my_tool");
|
||||||
|
assert_eq!(result.output, json!({"result": "ok"}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user