focus/crates/focus-harness/tests/compaction_tests.rs

136 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 上下文压缩(方案 D+E的单元测试。
//! Unit tests for context compaction (schemes D+E).
use focus_core::model::*;
use focus_harness::compaction::{
apply_summary, estimate_messages, estimate_tokens, plan_compaction, DEFAULT_KEEP_RATIO,
DEFAULT_THRESHOLD_RATIO,
};
use focus_json::JsonValue;
fn long_user_message(text: &str) -> Message {
Message::user_text(text)
}
#[test]
fn estimate_is_reasonable() {
// 80 个 ASCII 字符 ≈ 20 token。
// 80 ASCII chars ≈ 20 tokens.
let text = "a".repeat(80);
assert_eq!(estimate_tokens(&text), 20);
// 中文按字符计。
// CJK counted per char.
assert_eq!(estimate_tokens("中文"), 2);
// 消息开销计入。
// Per-message overhead is included.
assert!(estimate_messages(&[long_user_message("hi")]) > estimate_tokens("hi"));
}
#[test]
fn below_threshold_returns_none() {
let messages: Vec<Message> = (0..5)
.map(|i| long_user_message(&format!("msg {}", i)))
.collect();
// 窗口极大 → 不触发。
// Huge window → no trigger.
assert!(plan_compaction(
&messages,
1_000_000,
DEFAULT_THRESHOLD_RATIO,
DEFAULT_KEEP_RATIO
)
.is_none());
}
#[test]
fn above_threshold_plans_summarize_and_keep() {
// 每条消息约 50+ token"x"*200 → 50 token + 4 开销10 条约 540 token。
// Each message ≈ 50+ tokens; 10 of them ≈ 540 tokens.
let messages: Vec<Message> = (0..10)
.map(|_i| long_user_message(&"x".repeat(200)))
.collect();
let plan =
plan_compaction(&messages, 200, DEFAULT_THRESHOLD_RATIO, DEFAULT_KEEP_RATIO).expect("plan");
assert!(!plan.summarize.is_empty());
assert!(!plan.keep.is_empty());
assert!(plan.summarize.len() + plan.keep.len() == messages.len());
// 摘要指令包含两个部分。
// The summary instruction has both sections.
assert!(plan.summary_instruction.contains("<summary>"));
assert!(plan.summary_instruction.contains("<key-facts>"));
// 保存的 token 为正。
// Saved tokens are positive.
assert!(plan.estimated_saved_tokens > 0);
}
#[test]
fn keeps_tool_result_pairs_intact() {
// 构造user → assistant(tool_call) → toolResult → assistant(text)
// 其中 tool 部分恰好落在边界附近。
// Build: user → assistant(tool_call) → toolResult → assistant(text),
// with the tool part landing near the boundary.
let mut args = JsonValue::obj();
args.insert("x", "1".into()).ok();
let mut messages = vec![long_user_message(&"a".repeat(300))];
messages.push(Message::Assistant(AssistantMessage {
content: vec![ContentBlock::ToolCall(ToolCall {
id: "c1".into(),
name: "read".into(),
arguments: args,
})],
model: "m".into(),
usage: Usage::default(),
stop_reason: StopReason::ToolUse,
error_message: None,
timestamp: 0,
}));
messages.push(Message::ToolResult(ToolResultMessage {
tool_call_id: "c1".into(),
tool_name: "read".into(),
content: vec![ContentBlock::text("r".repeat(300))],
details: JsonValue::obj(),
is_error: false,
timestamp: 0,
}));
messages.push(Message::Assistant(AssistantMessage {
content: vec![ContentBlock::text("final")],
model: "m".into(),
usage: Usage::default(),
stop_reason: StopReason::Stop,
error_message: None,
timestamp: 0,
}));
let plan =
plan_compaction(&messages, 100, DEFAULT_THRESHOLD_RATIO, DEFAULT_KEEP_RATIO).expect("plan");
// 若 tool_result 在 keep 中,其 assistant 调用消息必须也在 keep 中。
// If the tool_result is kept, its assistant call message must be kept too.
let keep: Vec<&Message> = plan.keep.iter().collect();
if keep.iter().any(|m| matches!(m, Message::ToolResult(_))) {
assert!(keep.iter().any(|m| {
matches!(m, Message::Assistant(a)
if a.content.iter().any(|c| c.as_tool_call().is_some()))
}));
}
}
#[test]
fn apply_summary_prepends_summary_message() {
let messages: Vec<Message> = (0..6)
.map(|_i| long_user_message(&"x".repeat(200)))
.collect();
let plan = plan_compaction(&messages, 100, 0.8, 0.6).expect("plan");
let out = apply_summary("SUMMARY TEXT", &plan);
assert_eq!(out.len(), plan.keep.len() + 1);
match &out[0] {
Message::User(u) => {
let text = u.content[0].as_text().unwrap().text.clone();
assert!(text.contains("SUMMARY TEXT"));
}
other => panic!("expected user summary, got {:?}", other),
}
// 保留部分原样在后。
// The kept part follows verbatim.
assert_eq!(out[1..], plan.keep[..]);
}