136 lines
4.7 KiB
Rust
136 lines
4.7 KiB
Rust
//! 上下文压缩(方案 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[..]);
|
||
}
|