f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
270 lines
8.9 KiB
Rust
270 lines
8.9 KiB
Rust
// Acceptance tests for context compression (all three compaction levels).
|
|
//
|
|
// TC-A2-01 and TC-A2-03 are purely local (no LLM call).
|
|
// TC-A2-02 makes a real LLM call and is skipped when OPENAI_API_KEY is absent.
|
|
|
|
use nomi_agent::compact::auto::{BOUNDARY_PREFIX, autocompact, should_autocompact};
|
|
use nomi_agent::compact::emergency::is_at_emergency_limit;
|
|
use nomi_agent::compact::micro::{CLEARED_TOOL_RESULT, microcompact};
|
|
use nomi_agent::compact::state::CompactState;
|
|
use nomi_config::compact::CompactConfig;
|
|
use nomi_types::message::{ContentBlock, Message, Role};
|
|
use serde_json::json;
|
|
|
|
use crate::helpers;
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
fn tool_use_block(id: &str, name: &str) -> ContentBlock {
|
|
ContentBlock::ToolUse {
|
|
id: id.to_string(),
|
|
name: name.to_string(),
|
|
input: json!({}),
|
|
extra: None,
|
|
}
|
|
}
|
|
|
|
fn tool_result_block(id: &str, content: &str) -> ContentBlock {
|
|
ContentBlock::ToolResult {
|
|
tool_use_id: id.to_string(),
|
|
content: content.to_string(),
|
|
is_error: false,
|
|
images: Vec::new(),
|
|
}
|
|
}
|
|
|
|
// ── TC-A2-01: Microcompact clears old tool results (LOCAL) ─────────────────
|
|
|
|
/// Construct a message history with more than `micro_keep_recent * 2`
|
|
/// compactable tool results (each with a matching ToolUse block), run
|
|
/// microcompact, and verify that old results are cleared while the most
|
|
/// recent `micro_keep_recent` are preserved.
|
|
#[test]
|
|
fn microcompact_clears_old_tool_results() {
|
|
let keep_recent: usize = 3;
|
|
// We need MORE than keep_recent * 2 = 6 compactable results, so use 8.
|
|
let total_results: usize = 8;
|
|
|
|
let config = CompactConfig {
|
|
micro_keep_recent: keep_recent,
|
|
compactable_tools: vec!["Read".to_string()],
|
|
..CompactConfig::default()
|
|
};
|
|
|
|
// Build messages: alternating ToolUse (assistant) and ToolResult (user)
|
|
let mut messages: Vec<Message> = Vec::with_capacity(total_results * 2);
|
|
for i in 0..total_results {
|
|
let id = format!("tool_{i}");
|
|
messages.push(Message::new(
|
|
Role::Assistant,
|
|
vec![tool_use_block(&id, "Read")],
|
|
));
|
|
messages.push(Message::new(
|
|
Role::User,
|
|
vec![tool_result_block(&id, &format!("content of file {i}"))],
|
|
));
|
|
}
|
|
|
|
let result = microcompact(&mut messages, &config);
|
|
|
|
// Verify cleared count is positive
|
|
assert!(
|
|
result.cleared_count > 0,
|
|
"microcompact should clear at least one tool result, got cleared_count=0"
|
|
);
|
|
|
|
// Exactly total_results - keep_recent should be cleared
|
|
let expected_cleared = total_results - keep_recent;
|
|
assert_eq!(
|
|
result.cleared_count, expected_cleared,
|
|
"expected {expected_cleared} cleared, got {}",
|
|
result.cleared_count
|
|
);
|
|
|
|
// Verify old results (first `expected_cleared`) are replaced with placeholder
|
|
for i in 0..expected_cleared {
|
|
let user_msg_idx = i * 2 + 1; // user messages are at odd indices
|
|
match &messages[user_msg_idx].content[0] {
|
|
ContentBlock::ToolResult { content, .. } => {
|
|
assert_eq!(
|
|
content, CLEARED_TOOL_RESULT,
|
|
"tool result at index {i} should be cleared"
|
|
);
|
|
}
|
|
other => panic!("expected ToolResult at index {user_msg_idx}, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
// Verify most recent `keep_recent` results are preserved
|
|
for i in expected_cleared..total_results {
|
|
let user_msg_idx = i * 2 + 1;
|
|
match &messages[user_msg_idx].content[0] {
|
|
ContentBlock::ToolResult { content, .. } => {
|
|
let expected = format!("content of file {i}");
|
|
assert_eq!(
|
|
content, &expected,
|
|
"tool result at index {i} should be preserved with original content"
|
|
);
|
|
}
|
|
other => panic!("expected ToolResult at index {user_msg_idx}, got {other:?}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── TC-A2-02: Autocompact triggers LLM summary (REAL API CALL) ────────────
|
|
|
|
/// Set a very low autocompact threshold, verify should_autocompact triggers,
|
|
/// then call autocompact with a real LLM provider and verify the result
|
|
/// contains the boundary prefix marker.
|
|
#[tokio::test]
|
|
async fn autocompact_triggers_llm_summary() {
|
|
let api_key = match helpers::openai_api_key() {
|
|
Some(k) => k,
|
|
None => {
|
|
eprintln!("[acceptance] OPENAI_API_KEY not set — skipping");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Use gpt-4.1-mini which supports up to 32768 output tokens.
|
|
// The autocompact function requests COMPACT_MAX_OUTPUT_TOKENS (20000),
|
|
// which exceeds gpt-4o-mini's 16384 limit.
|
|
let config = {
|
|
let base = helpers::openai_config(&api_key);
|
|
nomi_config::config::Config {
|
|
model: "gpt-4.1-mini".to_string(),
|
|
..base
|
|
}
|
|
};
|
|
|
|
let compact_config = CompactConfig {
|
|
context_window: 1000,
|
|
output_reserve: 100,
|
|
autocompact_buffer: 100,
|
|
// threshold = 1000 - 100 - 100 = 800
|
|
..CompactConfig::default()
|
|
};
|
|
|
|
// Verify should_autocompact detects the threshold is exceeded
|
|
assert!(
|
|
should_autocompact(900, &compact_config),
|
|
"900 tokens should exceed the threshold of 800"
|
|
);
|
|
assert!(
|
|
!should_autocompact(700, &compact_config),
|
|
"700 tokens should be below the threshold of 800"
|
|
);
|
|
|
|
// Build a simple conversation
|
|
let messages = vec![
|
|
Message::new(
|
|
Role::User,
|
|
vec![ContentBlock::Text {
|
|
text: "Hello".to_string(),
|
|
}],
|
|
),
|
|
Message::new(
|
|
Role::Assistant,
|
|
vec![ContentBlock::Text {
|
|
text: "Hi there!".to_string(),
|
|
}],
|
|
),
|
|
Message::new(
|
|
Role::User,
|
|
vec![ContentBlock::Text {
|
|
text: "What is 2+2?".to_string(),
|
|
}],
|
|
),
|
|
Message::new(
|
|
Role::Assistant,
|
|
vec![ContentBlock::Text {
|
|
text: "4".to_string(),
|
|
}],
|
|
),
|
|
];
|
|
|
|
// Create a real provider and run autocompact
|
|
let provider = nomi_providers::create_provider(&config);
|
|
|
|
let state = CompactState {
|
|
last_input_tokens: 900, // above the threshold of 800
|
|
..CompactState::default()
|
|
};
|
|
// autocompact takes &mut state for recording success/failure
|
|
let mut state = state;
|
|
|
|
let result = autocompact(
|
|
provider.as_ref(),
|
|
&messages,
|
|
&config.model,
|
|
&compact_config,
|
|
&mut state,
|
|
)
|
|
.await;
|
|
|
|
let compact_result = result.expect("autocompact should succeed with a real LLM");
|
|
|
|
// Verify the result messages contain the boundary prefix
|
|
let has_boundary = compact_result.messages.iter().any(|msg| {
|
|
msg.content.iter().any(|block| {
|
|
if let ContentBlock::Text { text } = block {
|
|
text.starts_with(BOUNDARY_PREFIX)
|
|
} else {
|
|
false
|
|
}
|
|
})
|
|
});
|
|
assert!(
|
|
has_boundary,
|
|
"autocompact result should contain a message with the boundary prefix"
|
|
);
|
|
|
|
// Verify metadata
|
|
assert_eq!(compact_result.messages_summarized, messages.len());
|
|
assert_eq!(compact_result.pre_compact_tokens, 900);
|
|
}
|
|
|
|
// ── TC-A2-03: Emergency truncation detection (LOCAL) ───────────────────────
|
|
|
|
/// Verify that is_at_emergency_limit correctly detects when the token count
|
|
/// is within the emergency buffer of the context window, and that it works
|
|
/// even when compact is disabled.
|
|
#[test]
|
|
fn emergency_truncation_detection() {
|
|
let config = CompactConfig {
|
|
context_window: 1000,
|
|
emergency_buffer: 100,
|
|
// limit = 1000 - 100 = 900
|
|
..CompactConfig::default()
|
|
};
|
|
|
|
// 950 >= 900 → true (at emergency limit)
|
|
assert!(
|
|
is_at_emergency_limit(950, &config),
|
|
"950 tokens should be at the emergency limit (threshold = 900)"
|
|
);
|
|
|
|
// 800 < 900 → false (below emergency limit)
|
|
assert!(
|
|
!is_at_emergency_limit(800, &config),
|
|
"800 tokens should be below the emergency limit (threshold = 900)"
|
|
);
|
|
|
|
// Verify emergency check works even when config.enabled = false
|
|
let disabled_config = CompactConfig {
|
|
context_window: 1000,
|
|
emergency_buffer: 100,
|
|
enabled: false,
|
|
..CompactConfig::default()
|
|
};
|
|
|
|
assert!(
|
|
is_at_emergency_limit(950, &disabled_config),
|
|
"emergency limit should apply even when compact is disabled"
|
|
);
|
|
assert!(
|
|
!is_at_emergency_limit(800, &disabled_config),
|
|
"below-limit should still return false when compact is disabled"
|
|
);
|
|
}
|