test: pin current tool-eval, guardrail, and truncation behavior ahead of background-jobs work

T0 characterization safety net per plans/background-jobs-design.md §9.3: 43 tests
pinning handle_collect/check/cancel/spawn, the pending-agents guardrail (incl.
ForceTerminate + counter resets), eval_tool_calls partition/re-sort/soft-fail/
loop-alert/truncation, truncate_if_needed's UTF-8 boundary edge, ToolCall::eval
prefix routing, merge_tool_results shape, and cancel_recursive recursion.

Known-buggy behaviors deliberately pinned for visible later diffs: handle_check
consumes finished handles, guardrail ignores finished-but-uncollected agents,
mid-char truncation returns the full string with marker prepended.

Not covered (findings): empty-after-dedup bail is unreachable from non-empty
input; run_child_agent needs a mock LLM client (none exists) — manual case;
over-threshold summarization pinned via deterministic unknown-model failure.
This commit is contained in:
2026-08-25 13:55:15 -06:00
parent 240eaa081a
commit bfc3b7bfea
4 changed files with 808 additions and 2 deletions
+68
View File
@@ -593,6 +593,8 @@ mod tests {
use super::*;
use crate::config::request_context::RequestContext;
use crate::config::{AppState, WorkingMode};
use crate::function::ToolCall;
use serde_json::json;
use std::fs;
use std::sync::Arc;
use std::time::SystemTime;
@@ -973,4 +975,70 @@ mod tests {
));
assert!(result.is_err());
}
fn tool_result(id: &str, output: &str) -> ToolResult {
ToolResult::new(
ToolCall::new("t".into(), json!({}), Some(id.to_string())),
json!(output),
)
}
#[test]
fn merge_tool_results_first_merge_creates_container() {
let ctx = create_test_ctx();
let input = Input::from_str(&ctx, "test", None).unwrap();
let input =
input.merge_tool_results("assistant text".into(), vec![tool_result("id-1", "ok")]);
let tool_calls = input.tool_calls().as_ref().unwrap();
assert_eq!(tool_calls.text, "assistant text");
assert!(!tool_calls.sequence);
assert_eq!(tool_calls.tool_results.len(), 1);
assert!(tool_calls.tool_results[0].text.is_none());
}
#[test]
fn merge_tool_results_second_merge_marks_sequence_and_tags_text() {
let ctx = create_test_ctx();
let input = Input::from_str(&ctx, "test", None)
.unwrap()
.merge_tool_results("assistant text".into(), vec![tool_result("id-1", "ok")]);
let input =
input.merge_tool_results("second text".into(), vec![tool_result("id-2", "ok2")]);
let tool_calls = input.tool_calls().as_ref().unwrap();
assert!(tool_calls.sequence);
assert_eq!(tool_calls.tool_results.len(), 2);
assert_eq!(tool_calls.text, "assistant text");
assert!(tool_calls.tool_results[0].text.is_none());
assert_eq!(
tool_calls.tool_results[1].text,
Some("second text".to_string())
);
}
#[test]
fn build_messages_wraps_tool_results_in_single_assistant_message() {
let ctx = create_test_ctx();
let input = Input::from_str(&ctx, "test", None)
.unwrap()
.merge_tool_results("assistant text".into(), vec![tool_result("id-1", "ok")])
.merge_tool_results("second text".into(), vec![tool_result("id-2", "ok2")]);
let messages = input.build_messages().unwrap();
let tool_call_messages: Vec<_> = messages
.iter()
.filter(|m| matches!(m.content, MessageContent::ToolCalls(_)))
.collect();
assert_eq!(tool_call_messages.len(), 1);
let message = tool_call_messages[0];
assert!(matches!(message.role, MessageRole::Assistant));
let MessageContent::ToolCalls(tool_calls) = &message.content else {
unreachable!();
};
assert_eq!(tool_calls.tool_results.len(), 2);
}
}