use super::compact::{BUILTIN_EXEMPT_TOOLS, is_exempt_tool, micro_compact};
use super::plan_state::PLAN_MODE_WHITELIST;
use super::policy::{ContextTier, RetentionPolicy, is_key_tool, policy_for, tier_for};
use super::window::select_messages;
use crate::command::chat::storage::{ChatMessage, MessageRole, ToolCallItem};
fn user(content: &str) -> ChatMessage {
ChatMessage::text(MessageRole::User, content)
}
fn assistant(content: &str) -> ChatMessage {
ChatMessage::text(MessageRole::Assistant, content)
}
fn tool_call_with_id(id: &str, name: &str) -> ChatMessage {
ChatMessage {
role: MessageRole::Assistant,
content: String::new(),
tool_calls: Some(vec![ToolCallItem {
id: id.to_string(),
name: name.to_string(),
arguments: "{}".to_string(),
}]),
tool_call_id: None,
images: None,
}
}
fn tool_call(names: &[&str]) -> ChatMessage {
ChatMessage {
role: MessageRole::Assistant,
content: String::new(),
tool_calls: Some(
names
.iter()
.enumerate()
.map(|(i, name)| ToolCallItem {
id: format!("call_{}", i),
name: name.to_string(),
arguments: "{}".to_string(),
})
.collect(),
),
tool_call_id: None,
images: None,
}
}
fn tool_result(call_id: &str, content: &str) -> ChatMessage {
ChatMessage {
role: MessageRole::Tool,
content: content.to_string(),
tool_calls: None,
tool_call_id: Some(call_id.to_string()),
images: None,
}
}
#[test]
fn tool_name_constants_match_policy_hardcoded_strings() {
use crate::command::chat::tools::tool_names::*;
assert!(is_key_tool(ENTER_PLAN_MODE), "EnterPlanMode 应为 KeyTool");
assert!(is_key_tool(EXIT_PLAN_MODE), "ExitPlanMode 应为 KeyTool");
assert!(is_key_tool(ENTER_WORKTREE), "EnterWorktree 应为 KeyTool");
assert!(is_key_tool(EXIT_WORKTREE), "ExitWorktree 应为 KeyTool");
assert!(is_key_tool(ASK), "Ask 应为 KeyTool");
assert!(is_key_tool(LOAD_SKILL), "LoadSkill 应为 KeyTool");
assert!(is_key_tool(TASK), "Task 应为 KeyTool");
assert!(is_key_tool(TODO_WRITE), "TodoWrite 应为 KeyTool");
assert!(is_key_tool(TODO_READ), "TodoRead 应为 KeyTool");
assert!(is_key_tool(AGENT), "Agent 应为 KeyTool");
assert!(is_key_tool(AGENT_TEAM), "AgentTeam 应为 KeyTool");
assert!(is_key_tool(SEND_MESSAGE), "SendMessage 应为 KeyTool");
assert!(is_key_tool(CREATE_TEAMMATE), "CreateTeammate 应为 KeyTool");
}
#[test]
fn regular_tool_name_constants_match_policy() {
use crate::command::chat::tools::tool_names::*;
for name in [BASH, READ, WRITE, EDIT, GLOB, GREP, WEB_FETCH, WEB_SEARCH] {
let p = policy_for(name);
assert_eq!(
p.tier,
ContextTier::RegularTool,
"{} 应为 RegularTool",
name
);
assert_eq!(
p.retention,
RetentionPolicy::Placeholder,
"{} 应为 Placeholder",
name
);
}
}
#[test]
fn builtin_exempt_tools_aliases_key_tools() {
for &name in BUILTIN_EXEMPT_TOOLS {
assert!(
is_key_tool(name),
"BUILTIN_EXEMPT_TOOLS 中的 {} 必须是 KeyTool",
name
);
}
use super::policy::KEY_TOOL_NAMES;
assert_eq!(
BUILTIN_EXEMPT_TOOLS.len(),
KEY_TOOL_NAMES.len(),
"BUILTIN_EXEMPT_TOOLS 应与 KEY_TOOL_NAMES 长度一致"
);
}
#[test]
fn is_exempt_tool_without_extra_equals_is_key_tool() {
use crate::command::chat::tools::tool_names::*;
for name in [ENTER_PLAN_MODE, BASH, READ, LOAD_SKILL, ASK, GREP] {
assert_eq!(
is_exempt_tool(name, &[]),
is_key_tool(name),
"is_exempt_tool({}, []) 应等价于 is_key_tool",
name
);
}
}
#[test]
fn user_extra_exempt_tools_override() {
use crate::command::chat::tools::tool_names::BASH;
let extra = vec![BASH.to_string()];
assert!(is_exempt_tool(BASH, &extra), "Bash 被用户扩展豁免应生效");
assert!(
!is_exempt_tool(BASH, &[]),
"Bash 未被扩展豁免时应非豁免(默认 RegularTool)"
);
}
#[test]
fn micro_compact_preserves_key_tool_results() {
use crate::command::chat::tools::tool_names::*;
let big = "x".repeat(5000); let mut msgs = vec![
user("load skill"),
tool_call_with_id("k1", LOAD_SKILL),
tool_result("k1", &big),
user("enter plan mode"),
tool_call_with_id("k2", ENTER_PLAN_MODE),
tool_result("k2", &big),
user("edit worktree"),
tool_call_with_id("k3", ENTER_WORKTREE),
tool_result("k3", &big),
user("query ask"),
tool_call_with_id("k4", ASK),
tool_result("k4", &big),
user("do shell"),
tool_call_with_id("b1", BASH),
tool_result("b1", "ls output"),
tool_call_with_id("b2", BASH),
tool_result("b2", "ls output"),
tool_call_with_id("b3", BASH),
tool_result("b3", "ls output"),
];
micro_compact(&mut msgs, 1, &[]);
let preserved_big_count = msgs.iter().filter(|m| m.content.len() >= 5000).count();
assert_eq!(
preserved_big_count, 4,
"4 个 KeyTool 的大体积 result 必须全部保留,实际保留 {}",
preserved_big_count
);
}
#[test]
fn micro_compact_placeholder_for_regular_tools() {
use crate::command::chat::tools::tool_names::BASH;
let big = "x".repeat(5000);
let mut msgs = vec![
user("q1"),
tool_call_with_id("b1", BASH),
tool_result("b1", &big),
user("q2"),
tool_call_with_id("b2", BASH),
tool_result("b2", &big),
user("q3"),
tool_call_with_id("b3", BASH),
tool_result("b3", &big),
];
micro_compact(&mut msgs, 1, &[]);
let placeholders = msgs
.iter()
.filter(|m| m.role == MessageRole::Tool && m.content.starts_with("[Previous: used Bash]"))
.count();
assert_eq!(placeholders, 2, "最早 2 个 Bash result 应被替换为占位符");
}
#[test]
fn micro_compact_below_threshold_not_replaced() {
use crate::command::chat::tools::tool_names::BASH;
let small = "ok".to_string();
let mut msgs = vec![
user("q1"),
tool_call_with_id("b1", BASH),
tool_result("b1", &small),
user("q2"),
tool_call_with_id("b2", BASH),
tool_result("b2", &small),
user("q3"),
tool_call_with_id("b3", BASH),
tool_result("b3", &small),
];
micro_compact(&mut msgs, 1, &[]);
assert!(
msgs.iter().all(|m| !m.content.contains("Previous: used")),
"小体积 result 不应被替换为占位符"
);
}
#[test]
fn window_preserves_user_mentioned_key_tools_under_tight_budget() {
use crate::command::chat::tools::tool_names::*;
for key_tool in [ENTER_PLAN_MODE, ENTER_WORKTREE, ASK, LOAD_SKILL] {
let msgs = vec![
user("load it"),
tool_call(&[key_tool]),
tool_result(
"call_0",
&format!("{} large content ", key_tool).repeat(300),
),
user("q"),
assistant("a"),
user("q2"),
assistant("a2"),
user("q3"),
];
let result = select_messages(&msgs, 100, 5, 0, &[]);
assert!(
result.iter().any(|m| m.role == MessageRole::Tool),
"{} 的 tool result 必须通过豁免保底保留",
key_tool
);
}
}
#[test]
fn window_always_retains_latest_user() {
use crate::command::chat::tools::tool_names::BASH;
let mut msgs = Vec::new();
for i in 0..50 {
msgs.push(tool_call(&[BASH]));
msgs.push(tool_result("call_0", &"output ".repeat(500)));
msgs.push(user(&format!("q{}", i)));
}
msgs.push(user("LATEST"));
let result = select_messages(&msgs, 3, 1, 0, &[]);
assert!(
result
.iter()
.any(|m| m.role == MessageRole::User && m.content == "LATEST"),
"最新 User 消息必须通过兜底保留"
);
}
#[test]
fn window_stage1_time_fallback() {
use crate::command::chat::tools::tool_names::BASH;
let mut msgs = Vec::new();
for i in 0..20 {
msgs.push(user(&format!("old {}", i).repeat(30)));
}
msgs.push(tool_call(&[BASH]));
msgs.push(tool_result("call_0", "recent bash"));
msgs.push(user("latest"));
let result = select_messages(&msgs, 100, 2, 2, &[]);
assert!(
result
.iter()
.any(|m| m.role == MessageRole::Tool && m.content == "recent bash"),
"最近的 Bash result 应被 Stage 1 时间保底保留"
);
assert!(
result
.iter()
.any(|m| m.role == MessageRole::User && m.content == "latest"),
"最新 User 必须保留"
);
}
#[test]
fn window_merges_adjacent_dropped_tool_groups() {
use crate::command::chat::tools::tool_names::*;
let msgs = vec![
user("run"),
tool_call(&[BASH]),
tool_result("call_0", &"x".repeat(3000)),
tool_call(&[READ]),
tool_result("call_0", &"y".repeat(3000)),
tool_call(&[GREP]),
tool_result("call_0", &"z".repeat(3000)),
user("latest"),
];
let result = select_messages(&msgs, 100, 1, 0, &[]);
let placeholders: Vec<&ChatMessage> = result
.iter()
.filter(|m| m.content.contains("Previous: used"))
.collect();
assert!(
!placeholders.is_empty(),
"应有至少一条占位符消息合并被丢弃的 ToolGroup"
);
let combined = placeholders
.iter()
.map(|m| m.content.clone())
.collect::<Vec<_>>()
.join("\n");
let hit = [BASH, READ, GREP]
.iter()
.filter(|name| combined.contains(*name))
.count();
assert!(
hit >= 2,
"至少两个工具名应出现在合并占位符中,实际: {}",
combined
);
}
#[test]
fn window_preserves_time_order() {
use crate::command::chat::tools::tool_names::BASH;
let msgs = vec![
user("A"),
assistant("a1"),
tool_call(&[BASH]),
tool_result("call_0", "r1"),
user("B"),
assistant("a2"),
user("C"),
];
let result = select_messages(&msgs, 100, 0, 10, &[]);
let pos_a = result.iter().position(|m| m.content == "A").unwrap();
let pos_b = result.iter().position(|m| m.content == "B").unwrap();
let pos_c = result.iter().position(|m| m.content == "C").unwrap();
assert!(pos_a < pos_b && pos_b < pos_c, "User 消息应保持时间顺序");
}
#[test]
fn window_no_truncation_when_within_budget() {
let msgs = vec![user("hello"), assistant("hi")];
let result = select_messages(&msgs, 100, 0, 10, &[]);
assert_eq!(result.len(), 2);
}
#[test]
fn plan_mode_whitelist_recognized_by_policy() {
for name in PLAN_MODE_WHITELIST {
let p = policy_for(name);
let _ = p.tier;
}
}
#[test]
fn plan_mode_decision_tools_are_key_tools() {
use crate::command::chat::tools::tool_names::*;
for name in [ENTER_PLAN_MODE, EXIT_PLAN_MODE, ASK] {
assert!(
PLAN_MODE_WHITELIST.contains(&name),
"{} 应在 Plan mode 白名单",
name
);
assert!(is_key_tool(name), "{} 应为 KeyTool", name);
}
assert!(!PLAN_MODE_WHITELIST.contains(&LOAD_SKILL));
assert!(is_key_tool(LOAD_SKILL));
}
#[test]
fn tier_ordering_matches_user_requirement() {
use crate::command::chat::tools::tool_names::*;
assert!(ContextTier::User < ContextTier::KeyTool);
assert!(ContextTier::KeyTool < ContextTier::Assistant);
assert!(ContextTier::Assistant < ContextTier::RegularTool);
for name in [ENTER_PLAN_MODE, ENTER_WORKTREE, ASK, LOAD_SKILL] {
assert!(
tier_for(name) < tier_for(BASH),
"{} 的 tier 必须严格优于 Bash",
name
);
}
}