use super::*;
use crate::caveats::Caveats;
use crate::{BackendKind, MemMessage};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, Request, Respond, ResponseTemplate};
const TASK: &str =
"ACTIVE TASK GAUNTLET-7f3d9c: read big.txt until told to stop, then restate this marker";
const CANNED_SUMMARY: &str =
"## Active Task\nACTIVE TASK GAUNTLET-7f3d9c (canned summary)\n## Completed Actions\n1. read big.txt";
fn msgs() -> Vec<MemMessage> {
vec![MemMessage::system("you are a test"), MemMessage::user(TASK)]
}
fn ctx<'a>(
server_uri: &'a str,
messages: &'a [MemMessage],
caveats: &'a Caveats,
workspace: &'a str,
) -> ChatCtx<'a> {
ChatCtx {
shell_dialect: crate::agentic::shell_dialect_sentence(false, false),
command_budget: Default::default(),
overflow_retry: Default::default(),
run_allowance: None,
verify_outcomes: false,
round_cap_hit: None,
smart_harness: None,
url: server_uri,
model: "test-model",
kind: BackendKind::Ollama,
api_key: None,
messages,
task: TASK,
workspace,
default_command_cwd: None,
worktree_session: None,
color: false,
markdown: false,
tool_offload: false,
spill_store: None,
disclosure: None,
compaction_store: None,
scratchpad: false,
scratchpad_store: None,
code_search: None,
where_is: None,
nav: None,
exposure: Default::default(),
experience_store: None,
step_ledger: None,
caveats,
persona_tools: None,
cognition: None,
chat_completions_capability: Default::default(),
responses_capability: Default::default(),
openai_api: Default::default(),
output_allowance: None,
attempt_ledger: None,
reasoning_replay_scope: crate::model_card::ReasoningReplayScope::Never,
emits_leading_reasoning: false,
max_tool_rounds: 12,
narration_nudge_cap: 1,
action_nudges: true,
prompt_disposition: PromptDisposition::Act,
prompt_intake: None,
workflow_grace_rounds: 0,
tool_output_lines: 2,
debug: false,
trace: false,
num_ctx: None,
input_ceiling_pct: 80,
low_budget_pct: 15,
connect_timeout_secs: 5,
inference_timeout_secs: 30,
mid_loop_trim_threshold: 40,
compaction_trigger_policy: crate::CompactionTriggerPolicy::HeadroomAware,
mid_loop_trim_tokens: Some(5_000),
max_ok_input: None,
build_check_cmd: None,
safe_context: None,
recover_cw_400: None,
note_sink: None,
note_nudge: None,
recall_source: None,
memory_source: None,
summarizer: None,
compress_state: None,
tool_events: None,
phantom_reaches: None,
end_reason: None,
solve_obs: None,
permission_gate: None,
on_round_usage: None,
estimate_ratio: None,
estimation: crate::tokens::TokenEstimation::default(),
summary_input_cap_floor_chars: 8_192,
rewrites_history: true,
exec_floor: None,
write_ledger: None,
attribution: None,
cancel: None,
live_tool_output: None,
git_tool: None,
crew_runner: None,
operating_mode_control: None,
plan_mode_control: None,
plan_draft_sink: None,
steering: None,
completed_spill_renderer: None,
}
}
fn gauntlet_workspace() -> tempfile::TempDir {
let ws = tempfile::TempDir::new().unwrap();
let line = "the quick brown newt compresses context without discarding it\n";
std::fs::write(ws.path().join("big.txt"), line.repeat(64)).unwrap();
ws
}
fn gauntlet_messages_with_history(workspace: &std::path::Path, copies: usize) -> Vec<MemMessage> {
let earlier_read = std::fs::read_to_string(workspace.join("big.txt")).unwrap();
vec![
MemMessage::system("you are a test"),
MemMessage::user("Read the earlier fixtures and report their contents."),
MemMessage::assistant(format!(
"Completed earlier file review:\n{}",
earlier_read.repeat(copies)
)),
MemMessage::user(TASK),
]
}
fn body_json(req: &Request) -> serde_json::Value {
serde_json::from_slice(&req.body).unwrap_or_default()
}
fn messages_contain(body: &serde_json::Value, needle: &str) -> bool {
body["messages"]
.as_array()
.map(|msgs| {
msgs.iter().any(|m| {
m["content"]
.as_str()
.map(|c| c.contains(needle))
.unwrap_or(false)
})
})
.unwrap_or(false)
}
fn body_message_tokens(body: &serde_json::Value) -> usize {
body["messages"]
.as_array()
.map(|msgs| {
msgs.iter()
.map(|m| {
crate::tokens::TokenEstimation::default()
.tokens_for_chars(m.to_string().chars().count())
})
.sum()
})
.unwrap_or(0)
}
fn body_request_tokens(body: &serde_json::Value) -> usize {
let estimation = crate::tokens::TokenEstimation::default();
body_message_tokens(body)
+ body
.get("tools")
.map(|tools| estimate_value_tokens(tools, estimation))
.unwrap_or(0)
}
fn canned_summarizer(prompts: Arc<Mutex<Vec<String>>>) -> Summarizer {
Box::new(move |prompt: String| {
let prompts = prompts.clone();
Box::pin(async move {
prompts.lock().unwrap().push(prompt);
Ok((CANNED_SUMMARY.to_string(), None))
})
})
}
struct GauntletResponder {
final_answer: String,
log: Arc<Mutex<Vec<(bool, usize, usize)>>>,
task_in_marker_request: Arc<AtomicBool>,
summary_in_marker_request: Arc<AtomicBool>,
old_placeholder_seen: Arc<AtomicBool>,
static_marker_instead: bool,
}
impl Respond for GauntletResponder {
fn respond(&self, req: &Request) -> ResponseTemplate {
let body = body_json(req);
let has_marker = messages_contain(&body, SUMMARY_PREFIX);
if !body["stream"].as_bool().unwrap_or(false) {
self.log.lock().unwrap().push((
has_marker,
body_message_tokens(&body),
body_request_tokens(&body),
));
}
if messages_contain(&body, "earlier tool-call messages omitted") {
self.old_placeholder_seen.store(true, Ordering::SeqCst);
}
if has_marker {
if messages_contain(&body, TASK) {
self.task_in_marker_request.store(true, Ordering::SeqCst);
}
let summary_needle = if self.static_marker_instead {
"Summary generation was unavailable."
} else {
CANNED_SUMMARY
};
if messages_contain(&body, summary_needle) {
self.summary_in_marker_request.store(true, Ordering::SeqCst);
}
return ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": { "content": self.final_answer }
}));
}
if body.get("tools").is_some() {
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": { "content": "", "tool_calls": [{
"function": { "name": "read_file", "arguments": { "path": "big.txt" } }
}]}
}))
} else {
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({ "message": { "content": "cap exit" } }))
}
}
}
#[tokio::test]
async fn active_task_survives_compression() {
let _settings = crate::test_guard::GlobalSettingsGuard::acquire();
crate::initiative::set_initiative_config(Default::default());
let server = MockServer::start().await;
let log = Arc::new(Mutex::new(Vec::new()));
let task_in_marker = Arc::new(AtomicBool::new(false));
let summary_in_marker = Arc::new(AtomicBool::new(false));
let old_placeholder = Arc::new(AtomicBool::new(false));
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(GauntletResponder {
final_answer: "the marker is GAUNTLET-7f3d9c".into(),
log: log.clone(),
task_in_marker_request: task_in_marker.clone(),
summary_in_marker_request: summary_in_marker.clone(),
old_placeholder_seen: old_placeholder.clone(),
static_marker_instead: false,
})
.mount(&server)
.await;
let ws = gauntlet_workspace();
let workspace = ws.path().to_string_lossy().to_string();
let messages = gauntlet_messages_with_history(ws.path(), 4);
let caveats = Caveats::top();
let uri = server.uri();
let prompts = Arc::new(Mutex::new(Vec::new()));
let summarizer = canned_summarizer(prompts.clone());
let mut compress_state = CompressState::new();
let mut c = ctx(&uri, &messages, &caveats, &workspace);
c.action_nudges = false;
c.mid_loop_trim_tokens = Some(
builtin_catalog_tokens(PromptDisposition::Act)
+ prompt_read::response_repository_policy_tokens()
+ 5_600,
);
assert!(
initial_request_budget(&messages, TASK, &workspace).saturating_sub(1)
<= c.mid_loop_trim_tokens.unwrap(),
"completed history must fit initially; fresh reads trigger the compression"
);
c.summarizer = Some(&*summarizer);
c.compress_state = Some(&mut compress_state);
let (reply, _streamed, _usage, hallu) = chat_complete(c, &mut NoMcp)
.await
.expect("chat_complete should succeed");
assert_eq!(
crate::agentic::model_explanation(&reply),
"the marker is GAUNTLET-7f3d9c"
);
assert_eq!(hallu, 0);
let prompts = prompts.lock().unwrap();
assert_eq!(prompts.len(), 1, "one compression, one summary request");
assert!(
prompts[0].contains(TASK),
"summary request must quote the task verbatim"
);
assert!(task_in_marker.load(Ordering::SeqCst), "B5 property");
assert!(summary_in_marker.load(Ordering::SeqCst));
assert!(
!old_placeholder.load(Ordering::SeqCst),
"the old amputation placeholder must never be dispatched"
);
let log = log.lock().unwrap();
let before = log
.iter()
.filter(|(m, ..)| !m)
.map(|&(_, t, _)| t)
.max()
.expect("pre-compression requests were dispatched");
let after = log
.iter()
.find(|(m, ..)| *m)
.map(|&(_, t, _)| t)
.expect("a compressed request was dispatched");
println!("e2e reclaim: ~{before} -> ~{after} est. message tokens");
let fixed = prompt_read::response_repository_policy_tokens();
let reclaimable_before = before.saturating_sub(fixed);
let reclaimable_after = after.saturating_sub(fixed);
assert!(
reclaimable_after < reclaimable_before * 6 / 10,
"compression must reclaim >40% of the non-policy messages here \
(got {before} -> {after}, fixed policy ~{fixed})"
);
}
#[tokio::test]
async fn first_turn_over_num_ctx_ceiling_compresses_before_dispatch() {
let _settings = super::http_loop_tests::default_loop_settings();
let server = MockServer::start().await;
let log = Arc::new(Mutex::new(Vec::new()));
let task_in_marker = Arc::new(AtomicBool::new(false));
let summary_in_marker = Arc::new(AtomicBool::new(false));
let old_placeholder = Arc::new(AtomicBool::new(false));
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(GauntletResponder {
final_answer: "the marker is GAUNTLET-7f3d9c".into(),
log: log.clone(),
task_in_marker_request: task_in_marker.clone(),
summary_in_marker_request: summary_in_marker.clone(),
old_placeholder_seen: old_placeholder.clone(),
static_marker_instead: false,
})
.mount(&server)
.await;
let filler = "the quick brown newt reads three fifty-kilobyte fixtures\n".repeat(50);
let mut messages = vec![MemMessage::system("you are a test"), MemMessage::user(TASK)];
for _ in 0..12 {
messages.push(MemMessage::assistant(format!("file contents: {filler}")));
messages.push(MemMessage::user("continue"));
}
let ws = gauntlet_workspace();
let workspace = ws.path().to_string_lossy().to_string();
let caveats = Caveats::top();
let uri = server.uri();
let prompts = Arc::new(Mutex::new(Vec::new()));
let summarizer = canned_summarizer(prompts.clone());
let mut compress_state = CompressState::new();
let mut c = ctx(&uri, &messages, &caveats, &workspace);
c.max_ok_input = None;
c.safe_context = None;
c.mid_loop_trim_tokens = None;
let input_ceiling = builtin_catalog_tokens(PromptDisposition::Act)
+ prompt_read::response_repository_policy_tokens()
+ 1_130
+ observed_note_tokens(&workspace);
let num_ctx = (input_ceiling * 100).div_ceil(c.input_ceiling_pct as usize) as u32;
let ceiling = num_ctx as usize * c.input_ceiling_pct as usize / 100;
c.num_ctx = Some(num_ctx);
c.summarizer = Some(&*summarizer);
c.compress_state = Some(&mut compress_state);
let (reply, _streamed, _usage, _hallu) = chat_complete(c, &mut NoMcp)
.await
.expect("the first turn must complete");
assert_eq!(
crate::agentic::model_explanation(&reply),
"the marker is GAUNTLET-7f3d9c"
);
assert!(
!prompts.lock().unwrap().is_empty(),
"compression ran before the first dispatch (≥1 bounded summary request)"
);
let log = log.lock().unwrap();
let (first_had_marker, first_message_tokens, first_request_tokens) =
*log.first().expect("at least one request dispatched");
assert!(
first_had_marker,
"B6: the first dispatched request must already be compressed — \
pre-#282 it went out raw at ~9k tokens"
);
assert!(
first_request_tokens <= ceiling,
"first dispatch must fit the num_ctx input ceiling \
(got ~{first_request_tokens} est. full-request tokens, \
including ~{first_message_tokens} message tokens, > {ceiling})"
);
assert!(task_in_marker.load(Ordering::SeqCst), "task survives");
assert!(summary_in_marker.load(Ordering::SeqCst), "summary present");
assert!(!old_placeholder.load(Ordering::SeqCst));
}
#[tokio::test]
async fn summarizer_500_degrades_to_static_marker_and_turn_completes() {
let _settings = crate::test_guard::GlobalSettingsGuard::acquire();
crate::initiative::set_initiative_config(Default::default());
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/summarize"))
.respond_with(ResponseTemplate::new(500).set_body_string("boom"))
.mount(&server)
.await;
let log = Arc::new(Mutex::new(Vec::new()));
let task_in_marker = Arc::new(AtomicBool::new(false));
let static_in_marker = Arc::new(AtomicBool::new(false));
let old_placeholder = Arc::new(AtomicBool::new(false));
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(GauntletResponder {
final_answer: "completed despite summarizer outage".into(),
log: log.clone(),
task_in_marker_request: task_in_marker.clone(),
summary_in_marker_request: static_in_marker.clone(),
old_placeholder_seen: old_placeholder.clone(),
static_marker_instead: true,
})
.mount(&server)
.await;
let attempts = Arc::new(AtomicUsize::new(0));
let summarize_url = format!("{}/summarize", server.uri());
let attempts_in = attempts.clone();
let summarizer: Summarizer = Box::new(move |prompt: String| {
let url = summarize_url.clone();
let attempts = attempts_in.clone();
Box::pin(async move {
attempts.fetch_add(1, Ordering::SeqCst);
let resp = reqwest::Client::new()
.post(&url)
.body(prompt)
.send()
.await?;
if !resp.status().is_success() {
anyhow::bail!("summarizer endpoint {}", resp.status());
}
Ok((resp.text().await?, None))
})
});
let ws = gauntlet_workspace();
let workspace = ws.path().to_string_lossy().to_string();
let messages = gauntlet_messages_with_history(ws.path(), 1);
let caveats = Caveats::top();
let uri = server.uri();
let mut compress_state = CompressState::new();
let mut c = ctx(&uri, &messages, &caveats, &workspace);
c.action_nudges = false;
c.mid_loop_trim_tokens = Some(
builtin_catalog_tokens(PromptDisposition::Act)
+ prompt_read::response_repository_policy_tokens()
+ 1_815
+ observed_note_tokens(&workspace),
);
assert!(
initial_request_budget(&messages, TASK, &workspace).saturating_sub(1)
<= c.mid_loop_trim_tokens.unwrap(),
"completed history must fit initially; a fresh read triggers outage recovery"
);
c.summarizer = Some(&*summarizer);
c.compress_state = Some(&mut compress_state);
let ledger = std::sync::Mutex::new(crate::attempts::AttemptLedger::default());
c.attempt_ledger = Some(&ledger);
let (reply, _, _, _) = chat_complete(c, &mut NoMcp)
.await
.expect("a summarizer failure must never abort the turn");
assert_eq!(
crate::agentic::model_explanation(&reply),
"completed despite summarizer outage"
);
let received = server.received_requests().await.expect("journal");
let mut wire: Vec<_> = received
.iter()
.filter(|r| r.url.path() == "/api/chat")
.map(|r| content_addressable::RawContentId::from_content(&r.body))
.collect();
assert!(
received
.iter()
.all(|r| matches!(r.url.path(), "/api/chat" | "/summarize")),
"no path beyond the rounds and the summarizer"
);
let mut recorded: Vec<_> = ledger
.lock()
.unwrap()
.records()
.map(|r| r.key.request)
.collect();
wire.sort();
recorded.sort();
assert_eq!(recorded, wire, "primary attempts == /api/chat requests");
assert!(
attempts.load(Ordering::SeqCst) >= 1,
"the summarizer endpoint must have been attempted"
);
assert!(
static_in_marker.load(Ordering::SeqCst),
"the static fallback marker must reach the model"
);
assert!(task_in_marker.load(Ordering::SeqCst), "task still anchored");
assert!(!old_placeholder.load(Ordering::SeqCst));
}
#[tokio::test]
async fn optional_continuation_cannot_abort_a_fitting_static_fallback() {
let _settings = crate::test_guard::GlobalSettingsGuard::acquire();
crate::initiative::set_initiative_config(Default::default());
let _initiative = crate::initiative::scoped_effective_initiative(crate::Initiative::Measured);
let server = MockServer::start().await;
let log = Arc::new(Mutex::new(Vec::new()));
let task_in_marker = Arc::new(AtomicBool::new(false));
let static_in_marker = Arc::new(AtomicBool::new(false));
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(GauntletResponder {
final_answer: "completed with action nudges enabled".into(),
log: log.clone(),
task_in_marker_request: task_in_marker.clone(),
summary_in_marker_request: static_in_marker.clone(),
old_placeholder_seen: Arc::new(AtomicBool::new(false)),
static_marker_instead: true,
})
.mount(&server)
.await;
let attempts = Arc::new(AtomicUsize::new(0));
let attempts_in = attempts.clone();
let summarizer: Summarizer = Box::new(move |_| {
attempts_in.fetch_add(1, Ordering::SeqCst);
Box::pin(async { anyhow::bail!("fixture summarizer unavailable") })
});
let ws = gauntlet_workspace();
let workspace = ws.path().to_string_lossy().to_string();
let messages = msgs();
let caveats = Caveats::top();
let uri = server.uri();
let mut state = CompressState::new();
let mut c = ctx(&uri, &messages, &caveats, &workspace);
assert!(
c.action_nudges,
"this regression must exercise the live continuation"
);
let ceiling = builtin_catalog_tokens(PromptDisposition::Act)
+ prompt_read::response_repository_policy_tokens()
+ 1_815
+ observed_note_tokens(&workspace);
c.mid_loop_trim_tokens = Some(ceiling);
c.summarizer = Some(&*summarizer);
c.compress_state = Some(&mut state);
let (reply, _, _, _) = chat_complete(c, &mut NoMcp)
.await
.expect("optional harness guidance must not abort a fitting fallback");
assert_eq!(
crate::agentic::model_explanation(&reply),
"completed with action nudges enabled"
);
assert!(attempts.load(Ordering::SeqCst) > 0);
assert!(task_in_marker.load(Ordering::SeqCst));
assert!(static_in_marker.load(Ordering::SeqCst));
let requests = server.received_requests().await.unwrap();
let fallback = requests
.iter()
.map(body_json)
.find(|body| messages_contain(body, SUMMARY_PREFIX))
.expect("the real provider request must receive the static fallback");
assert!(!messages_contain(&fallback, compress::CONTINUATION_PREFIX));
assert!(requests
.iter()
.all(|request| body_request_tokens(&body_json(request)) <= ceiling));
}
struct OpenAiGauntletResponder {
final_answer: String,
task_in_marker_request: Arc<AtomicBool>,
summary_in_marker_request: Arc<AtomicBool>,
directive_in_marker_request: Arc<AtomicBool>,
}
impl Respond for OpenAiGauntletResponder {
fn respond(&self, req: &Request) -> ResponseTemplate {
let body = body_json(req);
if messages_contain(&body, SUMMARY_PREFIX) {
if messages_contain(&body, TASK) {
self.task_in_marker_request.store(true, Ordering::SeqCst);
}
if messages_contain(&body, CANNED_SUMMARY) {
self.summary_in_marker_request.store(true, Ordering::SeqCst);
}
if messages_contain(&body, compress::CONTINUATION_PREFIX) {
self.directive_in_marker_request
.store(true, Ordering::SeqCst);
}
return ResponseTemplate::new(200).set_body_json(serde_json::json!({
"choices": [{ "message": { "content": self.final_answer } }]
}));
}
if body.get("tools").is_some() {
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"choices": [{ "message": {
"content": null,
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": { "name": "read_file", "arguments": "{\"path\":\"big.txt\"}" }
}]
}}]
}))
} else {
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"choices": [{ "message": { "content": "cap exit" } }]
}))
}
}
}
#[tokio::test]
async fn openai_loop_compresses_with_the_same_pipeline() {
let _settings = super::http_loop_tests::default_loop_settings();
let server = MockServer::start().await;
let task_in_marker = Arc::new(AtomicBool::new(false));
let summary_in_marker = Arc::new(AtomicBool::new(false));
let directive_in_marker = Arc::new(AtomicBool::new(false));
Mock::given(method("POST"))
.and(path("/v1/chat/completions"))
.respond_with(OpenAiGauntletResponder {
final_answer: "openai: marker is GAUNTLET-7f3d9c".into(),
task_in_marker_request: task_in_marker.clone(),
summary_in_marker_request: summary_in_marker.clone(),
directive_in_marker_request: directive_in_marker.clone(),
})
.mount(&server)
.await;
let ws = gauntlet_workspace();
let workspace = ws.path().to_string_lossy().to_string();
let messages = msgs();
let caveats = Caveats::top();
let uri = server.uri();
let prompts = Arc::new(Mutex::new(Vec::new()));
let summarizer = canned_summarizer(prompts.clone());
let mut compress_state = CompressState::new();
let mut c = ctx(&uri, &messages, &caveats, &workspace);
c.kind = BackendKind::Openai;
c.api_key = Some("sk-test");
c.summarizer = Some(&*summarizer);
c.compress_state = Some(&mut compress_state);
c.mid_loop_trim_tokens = None;
c.mid_loop_trim_threshold = 8;
let (reply, _, _, _) = chat_complete(c, &mut NoMcp)
.await
.expect("openai loop should succeed");
assert_eq!(
crate::agentic::model_explanation(&reply),
"openai: marker is GAUNTLET-7f3d9c"
);
assert!(!prompts.lock().unwrap().is_empty(), "summarizer engaged");
assert!(task_in_marker.load(Ordering::SeqCst));
assert!(summary_in_marker.load(Ordering::SeqCst));
assert!(
directive_in_marker.load(Ordering::SeqCst),
"the post-compaction act-now directive must reach the wire"
);
}
type HaulLog = Arc<Mutex<Vec<(usize, Option<String>)>>>;
struct LongHaulResponder {
path: &'static str,
log: HaulLog,
}
impl Respond for LongHaulResponder {
fn respond(&self, req: &Request) -> ResponseTemplate {
let body = body_json(req);
if body.get("tools").is_some() {
let empty = Vec::new();
let msgs = body["messages"].as_array().unwrap_or(&empty);
let last_tool_content = msgs
.iter()
.rev()
.find(|m| m["role"].as_str() == Some("tool"))
.and_then(|m| m["content"].as_str())
.map(str::to_string);
self.log
.lock()
.unwrap()
.push((msgs.len(), last_tool_content));
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": { "content": "", "tool_calls": [
{ "function": { "name": "apply_patch", "arguments": {} } },
{ "function": { "name": "read_file", "arguments": { "path": self.path } } }
]}
}))
} else {
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({ "message": { "content": "long haul done" } }))
}
}
}
fn assert_haul_round_is_full_content(round: usize, expected: &str, content: &str) {
assert_eq!(
content, expected,
"round {round}: a bare read_file repeat must return the exact file content \
(cached or fresh) — #2637 doctrine forbids ever refusing it"
);
}
fn multi_turn_msgs() -> Vec<MemMessage> {
vec![
MemMessage::system("you are a test"),
MemMessage::user("prior turn: inspect the workspace"),
MemMessage::assistant("inspected — looks healthy"),
MemMessage::user("prior turn: run the linters"),
MemMessage::assistant("linters are green"),
MemMessage::user("prior turn: sketch a fix"),
MemMessage::assistant("sketched in my head"),
MemMessage::user(TASK),
]
}
pub(super) fn add_observed_note(
messages: &mut Vec<serde_json::Value>,
task: &str,
workspace: &str,
) {
let claims = claim_check::TurnClaims::capture(workspace, &crate::Scope::All, None);
let mut projection = semantic_pins::Projection::default();
projection.planning = output_budget::guidance(
None,
None,
std::path::Path::new(workspace),
&crate::Scope::All,
crate::tokens::TokenEstimation::default(),
);
semantic_pins::refresh(
&mut projection,
None,
messages,
prompt_read::PromptReadContext::new(None, task, None),
None,
&claims,
workspace,
&crate::Scope::All,
)
.unwrap();
}
pub(super) fn observed_note_tokens(workspace: &str) -> usize {
let mut messages = Vec::new();
add_observed_note(&mut messages, TASK, workspace);
estimate_tokens(&messages, crate::tokens::TokenEstimation::default())
}
fn initial_request_budget(messages: &[MemMessage], task: &str, workspace: &str) -> usize {
let tools = merged_tool_definitions(
&NoMcp,
false,
false,
false,
None,
false,
false,
false,
false,
false,
false,
false,
false,
Default::default(),
crate::agentic::shell_dialect_sentence(false, false),
);
let mut wire_messages: Vec<serde_json::Value> = messages
.iter()
.map(|message| {
serde_json::json!({"role": message.role.as_str(), "content": message.content})
})
.collect();
let receipt = crate::TurnPromptContext::ephemeral_operator(
"ephemeral-headless",
task.as_bytes().to_vec(),
task.as_bytes().to_vec(),
);
prompt_read::ensure_active_prompt_card(
&mut wire_messages,
prompt_read::PromptReadContext::new(Some(&receipt), task, None),
None,
);
add_observed_note(&mut wire_messages, task, workspace);
estimate_request_tokens(
&wire_messages,
Some(&tools),
crate::tokens::TokenEstimation::default(),
)
.saturating_add(1)
}
async fn run_long_haul(
mem_messages: Vec<MemMessage>,
rounds: usize,
threshold: usize,
file: &'static str,
content: &str,
) -> (Vec<(usize, Option<String>)>, usize, String, bool) {
let _settings = crate::test_guard::GlobalSettingsGuard::acquire();
crate::initiative::set_initiative_config(crate::initiative::InitiativeConfig {
no_progress: crate::initiative::NoProgressRounds {
steer_after: 0,
stop_after: 0,
},
..Default::default()
});
let server = MockServer::start().await;
let log = Arc::new(Mutex::new(Vec::new()));
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(LongHaulResponder {
path: file,
log: log.clone(),
})
.mount(&server)
.await;
let ws = tempfile::TempDir::new().unwrap();
std::fs::write(ws.path().join(file), content).unwrap();
let workspace = ws.path().to_string_lossy().to_string();
let caveats = Caveats::top();
let uri = server.uri();
let prompts = Arc::new(Mutex::new(Vec::new()));
let summarizer = canned_summarizer(prompts.clone());
let mut compress_state = CompressState::new();
let mut c = ctx(&uri, &mem_messages, &caveats, &workspace);
c.max_tool_rounds = rounds;
c.mid_loop_trim_threshold = threshold;
c.mid_loop_trim_tokens = None; c.summarizer = Some(&*summarizer);
c.compress_state = Some(&mut compress_state);
let (reply, _, _, _) = chat_complete(c, &mut NoMcp)
.await
.expect("the long haul must complete");
let log = log.lock().unwrap().clone();
let calls = prompts.lock().unwrap().len();
(log, calls, reply, compress_state.is_disabled())
}
#[tokio::test]
async fn forty_rounds_single_turn_stay_bounded_with_fresh_results_intact() {
let line = "the quick brown newt compresses context without discarding it\n";
let threshold = 15usize;
let (log, summarizer_calls, reply, latched) =
run_long_haul(msgs(), 40, threshold, "big.txt", &line.repeat(64)).await;
assert_eq!(crate::agentic::model_explanation(&reply), "long haul done");
assert!(!latched, "count-only pressure must never latch anti-thrash");
assert_eq!(log.len(), 40, "all 40 tool rounds dispatched");
let mut fresh_rounds = 0;
for (round, (len, last_tool)) in log.iter().enumerate() {
assert!(
*len <= threshold + 6,
"round {round}: dispatched {len} messages — the count must stay \
bounded after every compression (threshold {threshold} + slack)"
);
match last_tool {
Some(content) => {
assert_haul_round_is_full_content(round, &line.repeat(64), content);
fresh_rounds += 1;
}
None => assert_eq!(round, 0, "only round 0 may lack a prior tool result"),
}
}
assert_eq!(
fresh_rounds,
log.len() - 1,
"every round but the first must carry the full read_file content — #2637: never refused"
);
assert!(summarizer_calls >= 2, "the long haul compresses repeatedly");
assert!(
summarizer_calls <= 16,
"summarizer invocations must be bounded, not per-round \
(got {summarizer_calls} in 40 rounds)"
);
let max_len = log.iter().map(|(l, _)| *l).max().unwrap();
println!(
"forty-round trace: max dispatched len {max_len}, \
summarizer calls {summarizer_calls}"
);
}
#[tokio::test]
async fn thirty_rounds_multi_turn_stay_bounded_with_fresh_results_intact() {
let line = "the quick brown newt compresses context without discarding it\n";
let threshold = 15usize;
let (log, summarizer_calls, reply, latched) = run_long_haul(
multi_turn_msgs(),
30,
threshold,
"big.txt",
&line.repeat(64),
)
.await;
assert_eq!(crate::agentic::model_explanation(&reply), "long haul done");
assert!(!latched, "count-only pressure must never latch anti-thrash");
assert_eq!(log.len(), 30, "all 30 tool rounds dispatched");
let mut fresh_rounds = 0;
for (round, (len, last_tool)) in log.iter().enumerate() {
assert!(
*len <= threshold + 6,
"round {round}: dispatched {len} messages — bounded"
);
match last_tool {
Some(content) => {
assert_haul_round_is_full_content(round, &line.repeat(64), content);
fresh_rounds += 1;
}
None => assert_eq!(round, 0, "only round 0 may lack a prior tool result"),
}
}
assert_eq!(
fresh_rounds,
log.len() - 1,
"every round but the first must carry the full read_file content — #2637: never refused"
);
assert!(summarizer_calls >= 2);
assert!(
summarizer_calls <= 14,
"summarizer invocations must be bounded, not per-round \
(got {summarizer_calls} in 30 rounds)"
);
let max_len = log.iter().map(|(l, _)| *l).max().unwrap();
println!(
"thirty-round multi-turn trace: max dispatched len {max_len}, \
summarizer calls {summarizer_calls}"
);
}
#[tokio::test]
async fn compaction_release_forces_a_real_read_not_a_cache_hit() {
let line = "the quick brown newt compresses context without discarding it\n";
let threshold = 15usize;
let file = "big.txt";
let rounds = 40;
let _settings = crate::test_guard::GlobalSettingsGuard::acquire();
crate::initiative::set_initiative_config(crate::initiative::InitiativeConfig {
no_progress: crate::initiative::NoProgressRounds {
steer_after: 0,
stop_after: 0,
},
..Default::default()
});
let server = MockServer::start().await;
let log = Arc::new(Mutex::new(Vec::new()));
let prompts = Arc::new(Mutex::new(Vec::new()));
let summarizer = canned_summarizer(prompts.clone());
let ws = tempfile::TempDir::new().unwrap();
std::fs::write(ws.path().join(file), line.repeat(64)).unwrap();
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(LongHaulResponder {
path: file,
log: log.clone(),
})
.mount(&server)
.await;
let workspace = ws.path().to_string_lossy().to_string();
let caveats = Caveats::top();
let uri = server.uri();
let mem_messages = msgs();
let mut compress_state = CompressState::new();
let mut events: Vec<crate::ToolEvent> = Vec::new();
let mut c = ctx(&uri, &mem_messages, &caveats, &workspace);
c.max_tool_rounds = rounds;
c.mid_loop_trim_threshold = threshold;
c.mid_loop_trim_tokens = None;
c.summarizer = Some(&*summarizer);
c.compress_state = Some(&mut compress_state);
c.tool_events = Some(&mut events);
let (reply, _, _, _) = chat_complete(c, &mut NoMcp)
.await
.expect("the long haul must complete");
assert_eq!(crate::agentic::model_explanation(&reply), "long haul done");
let summarizer_calls = prompts.lock().unwrap().len();
assert!(
summarizer_calls >= 2,
"the long haul must trigger at least one committed compaction \
(got {summarizer_calls} summarizer calls)"
);
let read_events: Vec<&crate::ToolEvent> =
events.iter().filter(|e| e.tool == "read_file").collect();
assert_eq!(
read_events.len(),
log.lock().unwrap().len(),
"one read_file ToolEvent must exist per dispatched round"
);
let log = log.lock().unwrap();
let commits: usize = (1..log.len()).filter(|&r| log[r].0 < log[r - 1].0).count();
assert_eq!(
commits, summarizer_calls,
"the dispatched-length-drop signal must count exactly one commit per \
summarizer call (got {commits} drops for {summarizer_calls} calls)"
);
for round in 1..log.len() {
let just_committed = log[round].0 < log[round - 1].0;
let served_from_cache = read_events[round].from_cache;
assert_eq!(
served_from_cache,
!just_committed,
"round {round}: {}, so its read_file must be {} \
(from_cache={served_from_cache})",
if just_committed {
"a compaction just committed"
} else {
"no compaction committed since the previous round"
},
if just_committed { "real" } else { "cached" }
);
}
}
#[tokio::test]
async fn count_only_low_reclaim_never_latches_or_bails() {
let (log, _calls, reply, latched) =
run_long_haul(multi_turn_msgs(), 30, 15, "small.txt", &"x".repeat(600)).await;
assert_eq!(
crate::agentic::model_explanation(&reply),
"long haul done",
"the turn must complete — no bail"
);
assert!(
!latched,
"count-only passes must never latch the disable switch"
);
assert_eq!(log.len(), 30, "all 30 rounds ran (no Refused early-exit)");
}
type NudgedLog = Arc<Mutex<Vec<(usize, bool, Vec<usize>)>>>;
struct NudgedHaulResponder {
log: NudgedLog,
}
impl Respond for NudgedHaulResponder {
fn respond(&self, req: &Request) -> ResponseTemplate {
let body = body_json(req);
if body.get("tools").is_some() {
let empty = Vec::new();
let msgs = body["messages"].as_array().unwrap_or(&empty);
let group_start = msgs
.iter()
.rposition(|m| m["tool_calls"].as_array().is_some_and(|t| !t.is_empty()))
.map(|i| i + 1)
.unwrap_or(msgs.len());
let group_lens: Vec<usize> = msgs[group_start..]
.iter()
.filter(|m| m["role"].as_str() == Some("tool"))
.filter_map(|m| m["content"].as_str())
.map(|c| c.chars().count())
.collect();
let nudged = messages_contain(&body, "read-only rounds so far");
self.log
.lock()
.unwrap()
.push((msgs.len(), nudged, group_lens));
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": { "role": "assistant", "content": "", "tool_calls": [
{ "function": { "name": "read_file", "arguments": { "path": "big1.txt" } } },
{ "function": { "name": "read_file", "arguments": { "path": "big2.txt" } } },
{ "function": { "name": "read_file", "arguments": { "path": "small.txt" } } }
]}
}))
} else {
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({ "message": { "content": "nudged haul done" } }))
}
}
}
#[tokio::test]
async fn nudged_long_haul_keeps_fresh_group_results_intact() {
let _settings = super::http_loop_tests::default_loop_settings();
let server = MockServer::start().await;
let log: NudgedLog = Arc::new(Mutex::new(Vec::new()));
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(NudgedHaulResponder { log: log.clone() })
.mount(&server)
.await;
let ws = tempfile::TempDir::new().unwrap();
std::fs::write(
ws.path().join("big1.txt"),
"the first big fixture keeps unseen results intact\n".repeat(200),
)
.unwrap();
std::fs::write(
ws.path().join("big2.txt"),
"the second big fixture must survive every nudge round\n".repeat(200),
)
.unwrap();
std::fs::write(
ws.path().join("small.txt"),
"the small newest fixture stays whole\n".repeat(5),
)
.unwrap();
let workspace = ws.path().to_string_lossy().to_string();
let messages = msgs();
let caveats = Caveats::top();
let uri = server.uri();
let prompts = Arc::new(Mutex::new(Vec::new()));
let summarizer = canned_summarizer(prompts.clone());
let mut compress_state = CompressState::new();
let mut c = ctx(&uri, &messages, &caveats, &workspace);
c.max_tool_rounds = 12;
c.mid_loop_trim_tokens = Some(
builtin_catalog_tokens(PromptDisposition::Act)
+ prompt_read::response_repository_policy_tokens()
+ 4_600,
);
c.summarizer = Some(&*summarizer);
c.compress_state = Some(&mut compress_state);
let (reply, _, _, _) = chat_complete(c, &mut NoMcp)
.await
.expect("the nudged haul must complete");
assert_eq!(
crate::agentic::model_explanation(&reply),
"nudged haul done"
);
assert!(
!compress_state.is_disabled(),
"real reclaims every round must never latch anti-thrash"
);
let log = log.lock().unwrap();
for (round, entry) in log.iter().enumerate() {
println!("nudged haul round {round}: {entry:?}");
}
assert_eq!(log.len(), 12, "all 12 tool rounds dispatched");
let nudged_rounds = log.iter().filter(|(_, n, _)| *n).count();
assert!(
nudged_rounds >= 2,
"the read-only nudge regime must actually be active \
(got {nudged_rounds} nudged requests)"
);
for (round, (len, nudged, group_lens)) in log.iter().enumerate() {
if group_lens.is_empty() {
continue; }
assert_eq!(group_lens.len(), 3, "round {round}: pairing intact");
assert!(
group_lens[2] > 150,
"round {round} (nudged={nudged}): the newest fresh result \
was destroyed pre-dispatch ({} chars)",
group_lens[2]
);
assert!(
group_lens[1] > 1_000,
"round {round} (nudged={nudged}, {len} msgs): the middle \
fresh result was destroyed pre-dispatch ({} chars)",
group_lens[1]
);
}
let max_tokens = log.iter().map(|(l, _, _)| *l).max().unwrap();
println!(
"nudged haul trace: {nudged_rounds} nudged rounds, \
max dispatched len {max_tokens}, group lens e.g. {:?}",
log.last().unwrap().2
);
}
type OversizedLog = Arc<Mutex<Vec<(usize, bool, bool, bool, bool)>>>;
struct OversizedRoundResponder {
log: OversizedLog,
}
impl Respond for OversizedRoundResponder {
fn respond(&self, req: &Request) -> ResponseTemplate {
let body = body_json(req);
let a_onelined = messages_contain(&body, "[read_file] a.txt lines");
if !body["stream"].as_bool().unwrap_or(false) {
self.log.lock().unwrap().push((
body_request_tokens(&body),
a_onelined,
messages_contain(&body, "[read_file] b.txt lines"),
messages_contain(&body, "NEWEST-PAYLOAD-C-INTACT"),
messages_contain(&body, TASK),
));
}
if a_onelined {
return ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": { "content": "the three files are summarized" }
}));
}
if body.get("tools").is_some() {
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": { "role": "assistant", "content": "", "tool_calls": [
{ "function": { "name": "read_file", "arguments": { "path": "a.txt" } } },
{ "function": { "name": "read_file", "arguments": { "path": "b.txt" } } },
{ "function": { "name": "read_file", "arguments": { "path": "c.txt" } } }
]}
}))
} else {
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({ "message": { "content": "cap exit" } }))
}
}
}
#[tokio::test]
async fn oversized_single_round_dispatches_within_the_window() {
let _settings = super::http_loop_tests::default_loop_settings();
let server = MockServer::start().await;
let log: OversizedLog = Arc::new(Mutex::new(Vec::new()));
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(OversizedRoundResponder { log: log.clone() })
.mount(&server)
.await;
let catalog = builtin_catalog_tokens(PromptDisposition::Act);
let input_ceiling = catalog + prompt_read::response_repository_policy_tokens() + 3_200;
let num_ctx = ((input_ceiling as f64) / 0.8).ceil() as u32;
let ws = tempfile::TempDir::new().unwrap();
std::fs::write(
ws.path().join("a.txt"),
"fixture a arrives first in the one giant trailing group\n".repeat(125),
)
.unwrap();
std::fs::write(
ws.path().join("b.txt"),
"fixture b arrives second and is older than the newest\n".repeat(130),
)
.unwrap();
std::fs::write(
ws.path().join("c.txt"),
format!(
"{}NEWEST-PAYLOAD-C-INTACT\n",
"fixture c arrives last and must reach the model whole\n".repeat(130)
),
)
.unwrap();
let workspace = ws.path().to_string_lossy().to_string();
let messages = msgs();
let caveats = Caveats::top();
let uri = server.uri();
let prompts = Arc::new(Mutex::new(Vec::new()));
let summarizer = canned_summarizer(prompts.clone());
let mut compress_state = CompressState::new();
let mut c = ctx(&uri, &messages, &caveats, &workspace);
c.max_ok_input = None;
c.safe_context = None;
c.mid_loop_trim_tokens = None;
c.num_ctx = Some(num_ctx);
c.summarizer = Some(&*summarizer);
c.compress_state = Some(&mut compress_state);
let (reply, _, _, _) = chat_complete(c, &mut NoMcp)
.await
.expect("the oversized round must complete");
let log = log.lock().unwrap();
for (i, entry) in log.iter().enumerate() {
println!("oversized round request {i}: {entry:?}");
}
assert_eq!(
crate::agentic::model_explanation(&reply),
"the three files are summarized"
);
for (i, &(tokens, ..)) in log.iter().enumerate() {
assert!(
tokens <= input_ceiling,
"request {i} dispatched over the window: ~{tokens} est. \
full-request tokens > {input_ceiling}"
);
}
let (tokens, _, b_onelined, c_intact, task_present) = *log
.iter()
.find(|(_, a, ..)| *a)
.expect("a dispatch with a.txt one-lined must have happened");
assert!(
b_onelined,
"older members one-lined in order: b.txt one-liner missing"
);
assert!(
c_intact,
"#285: the NEWEST result must reach the model whole"
);
assert!(task_present, "the task survives the within-group reclaim");
assert!(
tokens <= input_ceiling,
"#285: the reclaimed dispatch must fit the window \
(got ~{tokens} est. full-request tokens > {input_ceiling})"
);
println!(
"#285 e2e trace: reclaimed dispatch ~{tokens} est. tokens \
(full-request ceiling {input_ceiling}, num_ctx {num_ctx}), \
a/b one-lined, c intact"
);
}
struct UnderreportedAnchorResponder {
log: Arc<Mutex<Vec<(bool, usize)>>>,
}
impl Respond for UnderreportedAnchorResponder {
fn respond(&self, req: &Request) -> ResponseTemplate {
let body = body_json(req);
let has_marker = messages_contain(&body, SUMMARY_PREFIX);
if !body["stream"].as_bool().unwrap_or(false) {
self.log
.lock()
.unwrap()
.push((has_marker, body_request_tokens(&body)));
}
if has_marker {
return ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": { "content": "recovered after exact-request compaction" },
"prompt_eval_count": 100,
"eval_count": 5
}));
}
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": { "content": "", "tool_calls": [{
"function": { "name": "read_file", "arguments": { "path": "big.txt" } }
}]},
"prompt_eval_count": 1,
"eval_count": 1
}))
}
}
#[tokio::test]
async fn exact_request_pressure_self_compacts_after_tracker_underreport() {
let _settings = super::http_loop_tests::default_loop_settings();
let server = MockServer::start().await;
let log = Arc::new(Mutex::new(Vec::new()));
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(UnderreportedAnchorResponder { log: log.clone() })
.mount(&server)
.await;
let ws = gauntlet_workspace();
let workspace = ws.path().to_string_lossy().to_string();
let messages = vec![
MemMessage::system("you are a test"),
MemMessage::user("summarize this old investigation later"),
MemMessage::assistant("old reclaimable evidence\n".repeat(600)),
MemMessage::user(TASK),
];
let budget = initial_request_budget(&messages, TASK, &workspace);
let caveats = Caveats::top();
let uri = server.uri();
let prompts = Arc::new(Mutex::new(Vec::new()));
let summarizer = canned_summarizer(prompts.clone());
let mut compress_state = CompressState::new();
let mut c = ctx(&uri, &messages, &caveats, &workspace);
c.mid_loop_trim_tokens = Some(budget);
c.summarizer = Some(&*summarizer);
c.compress_state = Some(&mut compress_state);
let (reply, _, _, _) = chat_complete(c, &mut NoMcp)
.await
.expect("exact-request pressure should compact and retry quietly");
assert_eq!(
crate::agentic::model_explanation(&reply),
"recovered after exact-request compaction"
);
assert!(
!prompts.lock().unwrap().is_empty(),
"self-healing must run the real compression pipeline"
);
let log = log.lock().unwrap();
assert_eq!(log.first().map(|entry| entry.0), Some(false));
assert!(
log.iter()
.skip(1)
.any(|(marker, tokens)| *marker && *tokens <= budget),
"a compacted request must be dispatched within the authoritative budget: {log:?}"
);
}
#[tokio::test]
async fn hard_budget_thrash_latches_then_bails_with_named_error() {
let _settings = super::http_loop_tests::default_loop_settings();
let server = MockServer::start().await;
let log = Arc::new(Mutex::new(Vec::new()));
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(LongHaulResponder {
path: "tiny.txt",
log: log.clone(),
})
.mount(&server)
.await;
let ws = tempfile::TempDir::new().unwrap();
std::fs::write(ws.path().join("tiny.txt"), "ok").unwrap();
let workspace = ws.path().to_string_lossy().to_string();
let messages = vec![
MemMessage::system(format!("you are a test. {}", "rule. ".repeat(7_000))),
MemMessage::user(TASK),
];
let caveats = Caveats::top();
let uri = server.uri();
let mut compress_state = CompressState::new();
let mut c = ctx(&uri, &messages, &caveats, &workspace);
c.mid_loop_trim_tokens = Some(initial_request_budget(&messages, TASK, &workspace));
c.compress_state = Some(&mut compress_state);
let err = chat_complete(c, &mut NoMcp)
.await
.expect_err("the first known-over-budget follow-up must refuse the send");
let msg = err.to_string();
assert!(msg.contains("complete inference request needs"), "{msg}");
assert!(msg.contains("tool results were not truncated"), "{msg}");
assert!(
!compress_state.is_disabled(),
"preflight refuses before thrashing"
);
assert_eq!(
log.lock().unwrap().len(),
1,
"no known-over-budget follow-up may reach the wire"
);
}
#[tokio::test]
async fn lone_hwm_budget_fails_open_and_does_not_bail() {
let _settings = super::http_loop_tests::default_loop_settings();
let server = MockServer::start().await;
let log = Arc::new(Mutex::new(Vec::new()));
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(LongHaulResponder {
path: "tiny.txt",
log: log.clone(),
})
.mount(&server)
.await;
let ws = tempfile::TempDir::new().unwrap();
std::fs::write(ws.path().join("tiny.txt"), "ok").unwrap();
let workspace = ws.path().to_string_lossy().to_string();
let messages = vec![
MemMessage::system(format!("you are a test. {}", "rule. ".repeat(700))),
MemMessage::user(TASK),
];
let caveats = Caveats::top();
let uri = server.uri();
let mut compress_state = CompressState::new();
let mut c = ctx(&uri, &messages, &caveats, &workspace);
c.mid_loop_trim_tokens = None;
c.max_ok_input = Some(50); c.safe_context = None; c.num_ctx = None; c.compress_state = Some(&mut compress_state);
let result = chat_complete(c, &mut NoMcp).await;
if let Err(e) = &result {
let msg = e.to_string();
assert!(
!msg.contains("exceeds the model's input budget"),
"a lone-HWM budget must never refuse the send: {msg}"
);
}
assert!(
compress_state.is_disabled(),
"anti-thrash still latches on the poor passes"
);
assert!(
log.lock().unwrap().len() > 3,
"the loop kept dispatching past the latch instead of bailing"
);
}
#[serial_test::serial(real_fs)]
#[tokio::test]
async fn compaction_2799_releases_skill_reuse_in_real_loop() {
use crate::agentic::tools::disable_ocap_tests::EnvVar;
let _settings = crate::test_guard::GlobalSettingsGuard::acquire();
crate::initiative::set_initiative_config(crate::initiative::InitiativeConfig {
no_progress: crate::initiative::NoProgressRounds {
steer_after: 0,
stop_after: 0,
},
..Default::default()
});
let ws = tempfile::tempdir().unwrap();
let skill = ws.path().join("skills/compaction-fixture-2799");
std::fs::create_dir_all(&skill).unwrap();
std::fs::write(
skill.join("SKILL.md"),
format!(
"---\nname: compaction-fixture-2799\ndescription: Test context reuse\n---\n{}",
"Skill content marker: inspect the fixture before modifying it.\n".repeat(64)
),
)
.unwrap();
std::fs::write(
ws.path().join("config.toml"),
format!(
"[skills]\nsearch = [{}]\n",
serde_json::to_string(&ws.path().join("skills").to_string_lossy()).unwrap()
),
)
.unwrap();
let _config = EnvVar::set("NEWT_CONFIG_DIR", ws.path().to_str().unwrap());
let server = MockServer::start().await;
let log = Arc::new(Mutex::new(Vec::new()));
let observed = log.clone();
Mock::given(method("POST")).and(path("/api/chat"))
.respond_with(move |req: &Request| {
let body = body_json(req);
if body.get("tools").is_some() {
observed.lock().unwrap().push(body["messages"].as_array().unwrap().len());
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": {"content":"", "tool_calls":[
{"function":{"name":"apply_patch","arguments":{}}},
{"function":{"name":"use_skill","arguments":{"name":"compaction-fixture-2799"}}}
]}
}))
} else {
ResponseTemplate::new(200).set_body_json(serde_json::json!({"message":{"content":"skill haul done"}}))
}
}).mount(&server).await;
let prompts = Arc::new(Mutex::new(Vec::new()));
let summarizer = canned_summarizer(prompts.clone());
let workspace = ws.path().to_string_lossy().to_string();
let caveats = Caveats::top();
let uri = server.uri();
let messages = msgs();
let mut state = CompressState::new();
let mut events = Vec::new();
let mut c = ctx(&uri, &messages, &caveats, &workspace);
c.max_tool_rounds = 30;
c.mid_loop_trim_threshold = 15;
c.mid_loop_trim_tokens = None;
c.summarizer = Some(&*summarizer);
c.compress_state = Some(&mut state);
c.tool_events = Some(&mut events);
let (reply, _, _, _) = chat_complete(c, &mut NoMcp).await.unwrap();
assert_eq!(crate::agentic::model_explanation(&reply), "skill haul done");
let events: Vec<_> = events.iter().filter(|e| e.tool == "use_skill").collect();
assert!(
events[0].ok,
"the fixture must genuinely load first: {:?}",
events[0]
);
let log = log.lock().unwrap();
assert_eq!(events.len(), log.len());
let committed: Vec<_> = (1..log.len()).filter(|&r| log[r] < log[r - 1]).collect();
assert!(
committed.len() >= 2,
"exercise repeated committed compaction"
);
assert_eq!(committed.len(), prompts.lock().unwrap().len());
for &round in &committed {
assert!(
events[round].ok,
"post-compaction skill load was refused in round {round}: {:?}",
events[round]
);
assert!(
!events[round].from_cache,
"post-compaction skill must load again"
);
}
for (round, event) in events.iter().enumerate().skip(1) {
assert!(event.ok, "skill reuse is success, not a refusal");
assert_eq!(
event.from_cache,
!committed.contains(&round),
"round {round}"
);
}
}