use super::*;
#[test]
fn compaction_policy_instructions_extend_by_default() {
let policy = CompactionPolicy {
instructions: Some("Keep the failing test names.".to_string()),
..Default::default()
};
let archived = [serde_json::json!({"role": "user", "content": "old context"})];
let retained = [serde_json::json!({"role": "tool", "content": "new evidence"})];
let prompt = render_llm_compaction_prompt(None, &archived, &retained, 1, &policy)
.expect("prompt renders");
assert_eq!(policy.instruction_mode(), "extend");
assert!(prompt.contains("Preserve goals, constraints"));
assert!(prompt.contains("Additional compaction instructions"));
assert!(prompt.contains("Keep the failing test names."));
assert!(prompt.contains("TOOL: new evidence"));
}
#[test]
fn compaction_policy_can_replace_default_instructions() {
let policy = CompactionPolicy {
instructions: Some("Only keep repro steps.".to_string()),
extend_default_instructions: Some(false),
..Default::default()
};
let archived = [serde_json::json!({"role": "user", "content": "old context"})];
let retained = [serde_json::json!({"role": "tool", "content": "new evidence"})];
let prompt = render_llm_compaction_prompt(None, &archived, &retained, 1, &policy)
.expect("prompt renders");
assert_eq!(policy.instruction_mode(), "replace");
assert!(prompt.contains("according to these instructions"));
assert!(prompt.contains("Only keep repro steps."));
assert!(!prompt.contains("Preserve goals, constraints"));
assert!(prompt.contains("newer than every archived message"));
assert!(prompt.contains("TOOL: new evidence"));
}
#[test]
fn auto_compact_preserves_reasoning_tool_suffix() {
let mut messages = vec![
serde_json::json!({"role": "user", "content": "old task"}),
serde_json::json!({"role": "assistant", "content": "old reply"}),
serde_json::json!({"role": "user", "content": "new task"}),
serde_json::json!({
"role": "assistant",
"content": "",
"reasoning": "think first",
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {"name": "read", "arguments": "{\"path\":\"foo.rs\"}"}
}],
}),
serde_json::json!({"role": "tool", "tool_call_id": "call_1", "content": "file"}),
];
let config = AutoCompactConfig {
token_threshold: 1,
keep_last: 2,
..Default::default()
};
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
let summary = runtime
.block_on(auto_compact_messages(&mut messages, &config, None))
.expect("compaction succeeds");
assert!(summary.is_some());
assert_eq!(messages[1]["role"], "user");
assert_eq!(messages[2]["role"], "assistant");
assert_eq!(messages[2]["tool_calls"][0]["id"], "call_1");
assert_eq!(messages[3]["role"], "tool");
assert_eq!(messages[3]["tool_call_id"], "call_1");
}
#[test]
fn auto_compact_never_splits_assistant_tool_use_from_its_result() {
let tool_call = |id: &str| {
serde_json::json!({
"id": id,
"type": "function",
"function": {"name": "run", "arguments": "{}"}
})
};
let mut messages = vec![
serde_json::json!({"role": "user", "content": "task"}),
serde_json::json!({"role": "assistant", "content": "", "tool_calls": [tool_call("c0")]}),
serde_json::json!({"role": "tool", "tool_call_id": "c0", "content": "r0"}),
serde_json::json!({"role": "assistant", "content": "", "tool_calls": [tool_call("c1")]}),
serde_json::json!({"role": "tool", "tool_call_id": "c1", "content": "r1"}),
serde_json::json!({"role": "assistant", "content": "", "tool_calls": [tool_call("c2")]}),
serde_json::json!({"role": "tool", "tool_call_id": "c2", "content": "r2"}),
];
let config = AutoCompactConfig {
token_threshold: 1,
keep_first: 0,
keep_last: 3,
..Default::default()
};
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
let summary = runtime
.block_on(auto_compact_messages(&mut messages, &config, None))
.expect("compaction succeeds");
assert!(summary.is_some(), "compaction should trigger");
assert_eq!(messages[0]["role"], "user", "summary head");
assert_eq!(messages[1]["role"], "assistant");
assert_eq!(messages[1]["tool_calls"][0]["id"], "c1");
assert_eq!(messages[2]["role"], "tool");
assert_eq!(messages[2]["tool_call_id"], "c1");
assert_eq!(messages[3]["tool_calls"][0]["id"], "c2");
assert_eq!(messages[4]["tool_call_id"], "c2");
for (idx, message) in messages.iter().enumerate() {
if message["role"] == "tool" {
let id = message["tool_call_id"].as_str().expect("tool_call_id");
let paired = messages[..idx].iter().any(|prev| {
prev["tool_calls"]
.as_array()
.is_some_and(|calls| calls.iter().any(|call| call["id"] == id))
});
assert!(paired, "tool_result {id} orphaned in kept window");
}
}
}
#[test]
fn auto_compact_clamps_oversized_tool_output_to_max_chars() {
let big = "x".repeat(4000);
let big_len = big.len();
let mut messages = vec![
serde_json::json!({"role": "user", "content": "old task"}),
serde_json::json!({"role": "assistant", "content": "old reply"}),
serde_json::json!({"role": "user", "content": "new task"}),
serde_json::json!({"role": "assistant", "content": "calling tool"}),
serde_json::json!({"role": "tool", "tool_call_id": "call_1", "content": big}),
];
let config = AutoCompactConfig {
token_threshold: 1,
keep_last: 2,
tool_output_max_chars: 500,
..Default::default()
};
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
let result = runtime
.block_on(auto_compact_messages(&mut messages, &config, None))
.expect("compaction succeeds");
assert!(result.is_some(), "compaction should trigger");
let tool_msg = messages
.iter()
.find(|message| message["role"] == "tool")
.expect("tool message kept in window");
assert_eq!(tool_msg["tool_call_id"], "call_1");
let content = tool_msg["content"].as_str().expect("string content");
assert!(
content.len() < big_len,
"tool output should be clamped: {} vs {}",
content.len(),
big_len
);
assert!(content.len() < 2000, "clamped near tool_output_max_chars");
}
#[test]
fn clamp_exempts_pinned_tool_output() {
let pinned_big = format!(
"## Exact current file text {}\n{}",
NO_COMPACT_MARKER,
"x".repeat(4000)
);
let pinned_len = pinned_big.len();
let unpinned_big = "y".repeat(4000);
let unpinned_len = unpinned_big.len();
let mut messages = vec![
serde_json::json!({"role": "user", "content": "old task"}),
serde_json::json!({"role": "assistant", "content": "reply"}),
serde_json::json!({"role": "user", "content": "new task"}),
serde_json::json!({"role": "assistant", "content": "calling tools"}),
serde_json::json!({"role": "tool", "tool_call_id": "c0", "content": unpinned_big}),
serde_json::json!({"role": "tool", "tool_call_id": "c1", "content": pinned_big}),
serde_json::json!({"role": "user", "content": "continue"}),
];
let config = AutoCompactConfig {
token_threshold: 1,
keep_last: 4,
tool_output_max_chars: 500,
..Default::default()
};
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
runtime
.block_on(auto_compact_messages(&mut messages, &config, None))
.expect("compaction succeeds");
let pinned_msg = messages
.iter()
.find(|m| m["tool_call_id"] == "c1")
.expect("pinned tool message kept");
assert_eq!(
pinned_msg["content"].as_str().map(str::len),
Some(pinned_len),
"pinned output must be intact (unclamped)"
);
let unpinned_msg = messages
.iter()
.find(|m| m["tool_call_id"] == "c0")
.expect("unpinned tool message kept");
assert!(
unpinned_msg["content"].as_str().map(str::len).unwrap() < unpinned_len,
"unpinned output of the same size must be clamped"
);
}
#[test]
fn pin_bound_keeps_only_latest_segments() {
let make = |gen: usize| {
let body = (0..40)
.map(|i| format!("marker-gen-{gen} body line {i}"))
.collect::<Vec<_>>()
.join("\n");
serde_json::json!({
"role": "user",
"content": format!(
"## Edited region now reads (gen {gen}) {}\n{}",
NO_COMPACT_MARKER, body
),
})
};
let archived: Vec<_> = (0..6).map(make).collect();
let pinned = latest_pinned_indices(archived.iter(), |m| {
m.get("content").and_then(|c| c.as_str())
});
assert_eq!(
pinned.len(),
MAX_PINNED_SEGMENTS,
"only the latest MAX_PINNED_SEGMENTS are pinned"
);
assert!(pinned.contains(&5) && pinned.contains(&4) && pinned.contains(&3));
assert!(!pinned.contains(&0) && !pinned.contains(&1) && !pinned.contains(&2));
let summary = observation_mask_compaction(&archived, archived.len());
assert!(
summary.contains("marker-gen-5")
&& summary.contains("marker-gen-4")
&& summary.contains("marker-gen-3"),
"latest {MAX_PINNED_SEGMENTS} pinned snapshots survive verbatim: {summary}"
);
assert!(
!summary.contains("marker-gen-0")
&& !summary.contains("marker-gen-1")
&& !summary.contains("marker-gen-2"),
"older pinned snapshots are masked (bound enforced)"
);
assert!(summary.contains("masked]"), "older snapshots were masked");
}
#[test]
fn no_pins_preserves_prior_clamp_behavior() {
let big = "x".repeat(4000);
let big_len = big.len();
let mut messages = vec![
serde_json::json!({"role": "user", "content": "old task"}),
serde_json::json!({"role": "assistant", "content": "old reply"}),
serde_json::json!({"role": "user", "content": "new task"}),
serde_json::json!({"role": "assistant", "content": "calling tool"}),
serde_json::json!({"role": "tool", "tool_call_id": "call_1", "content": big}),
];
let config = AutoCompactConfig {
token_threshold: 1,
keep_last: 2,
tool_output_max_chars: 500,
..Default::default()
};
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
let result = runtime
.block_on(auto_compact_messages(&mut messages, &config, None))
.expect("compaction succeeds");
assert!(result.is_some());
let tool_msg = messages
.iter()
.find(|m| m["role"] == "tool")
.expect("tool kept");
let content = tool_msg["content"].as_str().expect("string content");
assert!(content.len() < big_len, "unpinned output clamped as before");
assert!(content.len() < 2000, "clamped near tool_output_max_chars");
}