#[cfg(test)]
mod tests {
use crate::context::classification::*;
use std::path::PathBuf;
fn test_manager() -> ContextManager {
ContextManager::new()
}
fn test_manager_with_rules(rules: SupersedenceRules) -> ContextManager {
ContextManager::with_rules(rules)
}
#[test]
fn transcript_read_edit_read() {
let mut manager = test_manager();
let read_args_1 = serde_json::json!({"file_path": "/src/main.rs"});
manager
.classifier_mut()
.classify_tool_call("read", &read_args_1, "msg-1", "call-1");
manager.classifier_mut().classify_tool_result(
"read",
r#"{"file_path":"/src/main.rs","content":"fn main() { old_code(); }"}"#,
"msg-2",
"call-1",
);
let edit_args = serde_json::json!({
"file_path": "/src/main.rs",
"old_string": "old_code",
"new_string": "new_code"
});
manager
.classifier_mut()
.classify_tool_call("edit", &edit_args, "msg-3", "call-2");
manager.classifier_mut().classify_tool_result(
"edit",
r#"{"file_path":"/src/main.rs","replacements":1}"#,
"msg-4",
"call-2",
);
let read_args_2 = serde_json::json!({"file_path": "/src/main.rs"});
manager
.classifier_mut()
.classify_tool_call("read", &read_args_2, "msg-5", "call-3");
manager.classifier_mut().classify_tool_result(
"read",
r#"{"file_path":"/src/main.rs","content":"fn main() { new_code(); }"}"#,
"msg-6",
"call-3",
);
let result = manager.compute_optimized_order();
assert!(
result.should_drop("msg-2"),
"First read result should be dropped"
);
assert!(result.should_drop("msg-4"), "Edit result should be dropped");
assert!(
result.should_include("msg-6"),
"Final read should be included"
);
let final_order = result.final_order();
assert!(
final_order.last().map(|k| k.message_id.as_str()) == Some("msg-6")
|| result.move_to_end.iter().any(|k| k.message_id == "msg-6"),
"Latest read should be at end of context"
);
}
#[test]
fn transcript_multiple_files() {
let mut manager = test_manager();
manager.classifier_mut().classify_tool_call(
"read",
&serde_json::json!({"file_path": "/src/file_a.rs"}),
"msg-1",
"call-1",
);
manager.classifier_mut().classify_tool_result(
"read",
r#"{"file_path":"/src/file_a.rs","content":"v1"}"#,
"msg-2",
"call-1",
);
manager.classifier_mut().classify_tool_call(
"read",
&serde_json::json!({"file_path": "/src/file_b.rs"}),
"msg-3",
"call-2",
);
manager.classifier_mut().classify_tool_result(
"read",
r#"{"file_path":"/src/file_b.rs","content":"v1"}"#,
"msg-4",
"call-2",
);
manager.classifier_mut().classify_tool_call(
"edit",
&serde_json::json!({
"file_path": "/src/file_a.rs",
"old_string": "old",
"new_string": "new"
}),
"msg-5",
"call-3",
);
manager.classifier_mut().classify_tool_result(
"edit",
r#"{"file_path":"/src/file_a.rs"}"#,
"msg-6",
"call-3",
);
manager.classifier_mut().classify_tool_call(
"read",
&serde_json::json!({"file_path": "/src/file_a.rs"}),
"msg-7",
"call-4",
);
manager.classifier_mut().classify_tool_result(
"read",
r#"{"file_path":"/src/file_a.rs","content":"v2"}"#,
"msg-8",
"call-4",
);
let result = manager.compute_optimized_order();
assert!(
result.should_drop("msg-2"),
"Old read of file_a should be dropped"
);
assert!(
result.should_include("msg-4"),
"Read of file_b should be kept (no newer read)"
);
assert!(
result.should_include("msg-8"),
"New read of file_a should be kept"
);
}
#[test]
fn transcript_write_supersedes() {
let mut manager = test_manager();
manager.classifier_mut().classify_tool_call(
"read",
&serde_json::json!({"file_path": "/src/file.rs"}),
"msg-1",
"call-1",
);
manager.classifier_mut().classify_tool_result(
"read",
r#"{"file_path":"/src/file.rs","content":"v1"}"#,
"msg-2",
"call-1",
);
manager.classifier_mut().classify_tool_call(
"edit",
&serde_json::json!({
"file_path": "/src/file.rs",
"old_string": "a",
"new_string": "b"
}),
"msg-3",
"call-2",
);
manager.classifier_mut().classify_tool_result(
"edit",
r#"{"file_path":"/src/file.rs"}"#,
"msg-4",
"call-2",
);
manager.classifier_mut().classify_tool_call(
"write",
&serde_json::json!({
"file_path": "/src/file.rs",
"content": "completely new content"
}),
"msg-5",
"call-3",
);
manager.classifier_mut().classify_tool_result(
"write",
r#"{"file_path":"/src/file.rs","bytes_written":22}"#,
"msg-6",
"call-3",
);
let result = manager.compute_optimized_order();
assert!(
result.should_drop("msg-2"),
"Read before write should be dropped"
);
assert!(
result.should_drop("msg-4"),
"Edit before write should be dropped"
);
assert!(
result.should_include("msg-6"),
"Write result should be kept"
);
}
#[test]
fn transcript_partial_reads() {
let mut manager = test_manager();
manager.classifier_mut().classify_tool_call(
"read",
&serde_json::json!({
"file_path": "/src/file.rs",
"offset": 0,
"limit": 50
}),
"msg-1",
"call-1",
);
manager.classifier_mut().classify_tool_result(
"read",
r#"{"file_path":"/src/file.rs","content":"lines 0-50"}"#,
"msg-2",
"call-1",
);
manager.classifier_mut().classify_tool_call(
"read",
&serde_json::json!({
"file_path": "/src/file.rs",
"offset": 100,
"limit": 50
}),
"msg-3",
"call-2",
);
manager.classifier_mut().classify_tool_result(
"read",
r#"{"file_path":"/src/file.rs","content":"lines 100-150"}"#,
"msg-4",
"call-2",
);
manager.classifier_mut().classify_tool_call(
"read",
&serde_json::json!({"file_path": "/src/file.rs"}),
"msg-5",
"call-3",
);
manager.classifier_mut().classify_tool_result(
"read",
r#"{"file_path":"/src/file.rs","content":"full file content"}"#,
"msg-6",
"call-3",
);
let result = manager.compute_optimized_order();
assert!(
result.should_drop("msg-2"),
"Partial read 0-50 should be dropped"
);
assert!(
result.should_drop("msg-4"),
"Partial read 100-150 should be dropped"
);
assert!(result.should_include("msg-6"), "Full read should be kept");
}
#[test]
fn transcript_read_after_write_needed() {
let mut manager = test_manager();
manager.classifier_mut().classify_tool_call(
"write",
&serde_json::json!({
"file_path": "/src/file.rs",
"content": "new content"
}),
"msg-1",
"call-1",
);
manager.classifier_mut().classify_tool_result(
"write",
r#"{"file_path":"/src/file.rs"}"#,
"msg-2",
"call-1",
);
let path = PathBuf::from("/src/file.rs");
assert!(
manager.needs_read_after_write(&path),
"Write without read should need read-after-write"
);
manager.classifier_mut().classify_tool_call(
"read",
&serde_json::json!({"file_path": "/src/file.rs"}),
"msg-3",
"call-2",
);
manager.classifier_mut().classify_tool_result(
"read",
r#"{"file_path":"/src/file.rs","content":"new content"}"#,
"msg-4",
"call-2",
);
assert!(
!manager.needs_read_after_write(&path),
"After read, should not need read-after-write"
);
}
#[test]
fn transcript_preserve_edit_history() {
let rules = SupersedenceRules {
preserve_edit_history: true,
max_edits_per_file: 3,
..Default::default()
};
let mut manager = test_manager_with_rules(rules);
manager.classifier_mut().classify_tool_call(
"read",
&serde_json::json!({"file_path": "/src/file.rs"}),
"msg-1",
"call-1",
);
manager.classifier_mut().classify_tool_result(
"read",
r#"{"file_path":"/src/file.rs","content":"v1"}"#,
"msg-2",
"call-1",
);
for i in 0..4 {
let msg_call = format!("msg-{}", 3 + i * 2);
let msg_result = format!("msg-{}", 4 + i * 2);
let call_id = format!("call-{}", 2 + i);
manager.classifier_mut().classify_tool_call(
"edit",
&serde_json::json!({
"file_path": "/src/file.rs",
"old_string": format!("v{}", i + 1),
"new_string": format!("v{}", i + 2)
}),
&msg_call,
&call_id,
);
manager.classifier_mut().classify_tool_result(
"edit",
&format!(r#"{{"file_path":"/src/file.rs","version":"v{}"}}"#, i + 2),
&msg_result,
&call_id,
);
}
let result = manager.compute_optimized_order();
let kept_edits: Vec<_> = ["msg-4", "msg-6", "msg-8", "msg-10"]
.iter()
.filter(|id| result.should_include(id))
.collect();
assert!(
kept_edits.len() <= 3,
"Should keep at most 3 edits, got {}",
kept_edits.len()
);
}
#[test]
fn transcript_optimization_result_structure() {
let mut manager = test_manager();
manager.classifier_mut().classify_tool_call(
"read",
&serde_json::json!({"file_path": "/src/file.rs"}),
"msg-1",
"call-1",
);
manager.classifier_mut().classify_tool_result(
"read",
r#"{"file_path":"/src/file.rs"}"#,
"msg-2",
"call-1",
);
manager.classifier_mut().classify_tool_call(
"read",
&serde_json::json!({"file_path": "/src/file.rs"}),
"msg-3",
"call-2",
);
manager.classifier_mut().classify_tool_result(
"read",
r#"{"file_path":"/src/file.rs"}"#,
"msg-4",
"call-2",
);
let result = manager.compute_optimized_order();
assert!(
!result.keep.is_empty() || !result.move_to_end.is_empty(),
"Should have some messages to keep"
);
let final_order = result.final_order();
let unique: std::collections::HashSet<_> = final_order.iter().collect();
assert_eq!(
final_order.len(),
unique.len(),
"Final order should not have duplicates"
);
for dropped in &result.drop {
assert!(
!final_order.contains(dropped),
"Dropped classification key {:?} should not be in final order",
dropped
);
}
}
#[test]
fn transcript_non_file_operations() {
let mut manager = test_manager();
manager.classifier_mut().classify_tool_call(
"web_search",
&serde_json::json!({"query": "rust tutorials"}),
"msg-1",
"call-1",
);
manager.classifier_mut().classify_tool_result(
"web_search",
r#"{"results": ["result1", "result2"]}"#,
"msg-2",
"call-1",
);
manager.classifier_mut().classify_tool_call(
"bash",
&serde_json::json!({"command": "ls -la"}),
"msg-3",
"call-2",
);
manager.classifier_mut().classify_tool_result(
"bash",
r#"total 100\ndrwxr-xr-x..."#,
"msg-4",
"call-2",
);
let result = manager.compute_optimized_order();
assert!(
result.should_include("msg-1"),
"Non-file tool calls should be kept"
);
assert!(
result.should_include("msg-2"),
"Non-file tool results should be kept"
);
assert!(
result.should_include("msg-3"),
"Bash tool calls should be kept"
);
assert!(
result.should_include("msg-4"),
"Bash tool results should be kept"
);
}
#[allow(dead_code)]
fn visualize_result(result: &ContextOptimizationResult) -> String {
let mut output = String::new();
output.push_str("=== Context Optimization Result ===\n\n");
output.push_str("KEPT (in place):\n");
for key in &result.keep {
output.push_str(&format!(" - {}\n", key));
}
output.push_str("\nDROPPED:\n");
for key in &result.drop {
output.push_str(&format!(" - {}\n", key));
}
output.push_str("\nMOVED TO END:\n");
for key in &result.move_to_end {
output.push_str(&format!(" - {}\n", key));
}
output.push_str("\nFINAL ORDER:\n");
for (i, key) in result.final_order().iter().enumerate() {
output.push_str(&format!(" {}. {}\n", i + 1, key));
}
output
}
}