use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use io_harness::provider::{CompletionRequest, CompletionResponse, ToolCall};
use io_harness::{
run_tree, run_with, ApproveAll, Containment, Policy, Provider, RunOutcome, StallPolicy, Store,
TaskContract, Verification,
};
use serde_json::json;
struct MockScript {
steps: Vec<Vec<ToolCall>>,
at: AtomicUsize,
seen: Arc<Mutex<Vec<CompletionRequest>>>,
}
impl MockScript {
fn new(steps: Vec<Vec<ToolCall>>) -> Self {
Self {
steps,
at: AtomicUsize::new(0),
seen: Arc::new(Mutex::new(Vec::new())),
}
}
fn repeating(n: usize, c: ToolCall) -> Self {
Self::new((0..n).map(|_| vec![c.clone()]).collect())
}
fn prompt(&self, n: usize) -> String {
let seen = self.seen.lock().unwrap();
seen.get(n)
.unwrap_or_else(|| panic!("the loop ran only {} turn(s), wanted turn {n}", seen.len()))
.user
.clone()
}
fn turns(&self) -> usize {
self.seen.lock().unwrap().len()
}
fn any_prompt_carries_the_directive(&self) -> bool {
self.seen
.lock()
.unwrap()
.iter()
.any(|r| r.user.contains(DIRECTIVE))
}
}
impl Provider for MockScript {
async fn complete(&self, req: CompletionRequest) -> io_harness::Result<CompletionResponse> {
let i = self.at.fetch_add(1, Ordering::SeqCst);
self.seen.lock().unwrap().push(req);
Ok(CompletionResponse {
tool_calls: self.steps.get(i).cloned().unwrap_or_default(),
..Default::default()
})
}
}
const DIRECTIVE: &str = "[no progress]";
fn call(name: &str, args: serde_json::Value) -> ToolCall {
ToolCall {
name: name.into(),
arguments: args,
}
}
fn ws() -> tempfile::TempDir {
tempfile::tempdir().unwrap()
}
fn never_passes(root: &Path, steps: u32) -> TaskContract {
TaskContract::workspace("exercise stall detection", root)
.with_verification(Verification::WorkspaceFileContains {
file: "unreachable.txt".into(),
needle: "never".into(),
})
.with_max_steps(steps)
}
fn open_policy() -> Policy {
Policy::default()
.layer("test")
.allow_read("*")
.allow_write("*")
.allow_exec("*")
}
fn rows_of_kind(store: &Store, run_id: i64, kind: &str) -> Vec<(u32, String)> {
store
.context_events(run_id)
.unwrap()
.into_iter()
.filter(|r| r.kind == kind)
.map(|r| (r.step, r.detail.unwrap_or_default()))
.collect()
}
fn replans(store: &Store, run_id: i64) -> Vec<(u32, String)> {
rows_of_kind(store, run_id, "replan")
}
fn stalls(store: &Store, run_id: i64) -> Vec<(u32, String)> {
rows_of_kind(store, run_id, "stalled")
}
async fn go(
contract: &TaskContract,
provider: &MockScript,
store: &Store,
) -> io_harness::RunResult {
run_with(contract, provider, store, &open_policy(), &ApproveAll)
.await
.unwrap()
}
#[tokio::test]
async fn the_same_read_repeated_over_an_unchanged_workspace_is_caught_within_the_window() {
let dir = ws();
std::fs::write(dir.path().join("a.rs"), "fn a() {}\n").unwrap();
let contract = never_passes(dir.path(), 4);
let provider = MockScript::repeating(4, call("read_file", json!({ "path": "a.rs" })));
let store = Store::memory().unwrap();
let result = go(&contract, &provider, &store).await;
for turn in 0..3 {
assert!(
!provider.prompt(turn).contains(DIRECTIVE),
"turn {turn} is inside the window and must not be warned:\n{}",
provider.prompt(turn)
);
}
let fourth = provider.prompt(3);
assert!(
fourth.contains(DIRECTIVE),
"the step after the window must carry the replan directive, got:\n{fourth}"
);
assert!(
fourth.contains("3 times over") && fourth.contains("without changing anything"),
"the directive must say what it observed, got:\n{fourth}"
);
assert!(
fourth.contains("- read a.rs"),
"the directive must name what was already tried, got:\n{fourth}"
);
assert!(
fourth.contains("Change approach"),
"the directive must ask for something different, got:\n{fourth}"
);
let rows = replans(&store, result.run_id);
assert_eq!(rows.len(), 1, "exactly one replan row, got {rows:?}");
assert_eq!(
rows[0].0, 3,
"the row belongs to the step that closed the window"
);
assert!(
rows[0].1.contains("3 steps without progress"),
"the row must name the window it measured, got {rows:?}"
);
assert!(
stalls(&store, result.run_id).is_empty(),
"a nudge is not a terminal stall, got {:?}",
stalls(&store, result.run_id)
);
assert_eq!(result.outcome, RunOutcome::StepCapReached { steps: 4 });
}
#[tokio::test]
async fn an_agent_that_changes_the_workspace_is_never_flagged() {
let dir = ws();
let script: Vec<Vec<ToolCall>> = (0..8)
.map(|i| {
vec![call(
"write_file",
json!({ "path": "out.txt", "content": format!("revision {i}\n") }),
)]
})
.collect();
let contract = never_passes(dir.path(), script.len() as u32);
let provider = MockScript::new(script);
let store = Store::memory().unwrap();
let result = go(&contract, &provider, &store).await;
assert_eq!(result.outcome, RunOutcome::StepCapReached { steps: 8 });
assert_eq!(provider.turns(), 8, "every scripted turn must have run");
assert!(
!provider.any_prompt_carries_the_directive(),
"a working agent must never be told it is stuck"
);
assert!(
stalls(&store, result.run_id).is_empty(),
"a working agent must leave no stall row, got {:?}",
stalls(&store, result.run_id)
);
}
#[tokio::test]
async fn varied_calls_that_write_nothing_are_exploration_and_are_never_flagged() {
let dir = ws();
for name in ["a.rs", "b.rs", "c.rs"] {
std::fs::write(dir.path().join(name), "fn thing() {}\n").unwrap();
}
let script = vec![
vec![call("read_file", json!({ "path": "a.rs" }))],
vec![call("read_file", json!({ "path": "b.rs" }))],
vec![call("grep", json!({ "pattern": "thing" }))],
vec![call("find", json!({ "name_glob": "*.rs" }))],
vec![call("read_file", json!({ "path": "c.rs" }))],
vec![call("grep", json!({ "pattern": "fn" }))],
vec![call("find", json!({ "name_glob": "*.txt" }))],
];
let contract = never_passes(dir.path(), script.len() as u32);
let provider = MockScript::new(script);
let store = Store::memory().unwrap();
let result = go(&contract, &provider, &store).await;
assert_eq!(result.outcome, RunOutcome::StepCapReached { steps: 7 });
assert_eq!(provider.turns(), 7, "every scripted turn must have run");
assert!(
!provider.any_prompt_carries_the_directive(),
"an exploring agent must never be told it is stuck"
);
assert!(
stalls(&store, result.run_id).is_empty(),
"an exploring agent must leave no stall row, got {:?}",
stalls(&store, result.run_id)
);
}
#[tokio::test]
async fn a_write_that_changed_nothing_is_not_progress_and_is_flagged() {
let dir = ws();
const BODY: &str = "pub fn done() -> u32 { 42 }\n";
std::fs::write(dir.path().join("out.rs"), BODY).unwrap();
let contract = never_passes(dir.path(), 6);
let provider = MockScript::repeating(
6,
call("write_file", json!({ "path": "out.rs", "content": BODY })),
);
let store = Store::memory().unwrap();
let result = go(&contract, &provider, &store).await;
let fourth = provider.prompt(3);
assert!(
fourth.contains(DIRECTIVE),
"an unchanged write is not progress and must be caught, got:\n{fourth}"
);
assert!(
fourth.contains("- wrote out.rs"),
"the directive must name the write it is refusing to count, got:\n{fourth}"
);
assert!(
fourth.contains("identical to what was already there"),
"the write observation must say the workspace did not change, got:\n{fourth}"
);
assert_eq!(result.outcome, RunOutcome::Stalled { steps: 6 });
let nudges = replans(&store, result.run_id);
let stops = stalls(&store, result.run_id);
assert_eq!(nudges.len(), 1, "exactly one replan, got {nudges:?}");
assert_eq!(stops.len(), 1, "exactly one terminal stall, got {stops:?}");
assert!(
nudges[0].0 < stops[0].0,
"the nudge must precede the stop, got {nudges:?} then {stops:?}"
);
assert!(
stops[0].1.contains("still no progress after replanning"),
"the closing row must say the warning was already given, got {stops:?}"
);
assert_eq!(
std::fs::read_to_string(dir.path().join("out.rs")).unwrap(),
BODY
);
}
#[tokio::test]
async fn a_still_stalled_run_stops_far_short_of_its_step_budget() {
let dir = ws();
std::fs::write(dir.path().join("a.rs"), "fn a() {}\n").unwrap();
const BUDGET: u32 = 20;
let contract = never_passes(dir.path(), BUDGET);
let provider = MockScript::repeating(
BUDGET as usize,
call("read_file", json!({ "path": "a.rs" })),
);
let store = Store::memory().unwrap();
let result = go(&contract, &provider, &store).await;
let RunOutcome::Stalled { steps } = result.outcome.clone() else {
panic!(
"a run that never changes anything must end Stalled, got {:?}",
result.outcome
);
};
assert!(
steps * 2 < BUDGET,
"the run must stop materially short of its budget, got {steps} of {BUDGET}"
);
assert_eq!(
provider.turns(),
steps as usize,
"one completion per step, and none after the stop"
);
let nudges = replans(&store, result.run_id);
let stops = stalls(&store, result.run_id);
assert_eq!(nudges.len(), 1, "exactly one replan, got {nudges:?}");
assert_eq!(stops.len(), 1, "exactly one terminal stall, got {stops:?}");
assert!(nudges[0].1.contains("replanning"), "got {nudges:?}");
assert!(stops[0].1.contains("still no progress"), "got {stops:?}");
assert!(
nudges[0].0 < stops[0].0,
"the nudge must precede the stop, got {nudges:?} then {stops:?}"
);
assert_eq!(stops[0].0, steps, "the stop belongs to the last step");
assert_eq!(
store.outcome(result.run_id).unwrap().as_deref(),
Some("stalled")
);
}
#[tokio::test]
async fn a_window_of_zero_restores_the_old_step_cap_behaviour_exactly() {
let dir = ws();
std::fs::write(dir.path().join("a.rs"), "fn a() {}\n").unwrap();
const BUDGET: u32 = 20;
let contract = never_passes(dir.path(), BUDGET).with_stall_policy(StallPolicy {
window: 0,
max_replans: 0,
});
let provider = MockScript::repeating(
BUDGET as usize,
call("read_file", json!({ "path": "a.rs" })),
);
let store = Store::memory().unwrap();
let result = go(&contract, &provider, &store).await;
assert_eq!(result.outcome, RunOutcome::StepCapReached { steps: BUDGET });
assert_eq!(provider.turns(), BUDGET as usize);
assert!(
!provider.any_prompt_carries_the_directive(),
"detection is off, so nothing may be said to the model"
);
assert!(
stalls(&store, result.run_id).is_empty(),
"detection is off, so nothing may be written to the trace, got {:?}",
stalls(&store, result.run_id)
);
assert_eq!(
store.outcome(result.run_id).unwrap().as_deref(),
Some("step_cap_reached")
);
}
#[tokio::test]
async fn a_stalled_root_agent_ends_the_tree_as_stalled() {
use io_harness::{run_tree, Containment};
let dir = ws();
std::fs::write(dir.path().join("a.rs"), "fn a() {}\n").unwrap();
const BUDGET: u32 = 20;
let contract = never_passes(dir.path(), BUDGET);
let provider = MockScript::repeating(
BUDGET as usize,
call("read_file", json!({ "path": "a.rs" })),
);
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&provider,
&store,
&Policy::permissive(),
&ApproveAll,
&Containment::new(10, 4, 3, 1_000_000),
)
.await
.unwrap();
let RunOutcome::Stalled { steps } = result.outcome.clone() else {
panic!(
"a stalled root must end the tree Stalled, got {:?}",
result.outcome
);
};
assert!(
steps * 2 < BUDGET,
"the tree must stop short of the budget too, got {steps} of {BUDGET}"
);
assert!(
provider.any_prompt_carries_the_directive(),
"the root agent must have been warned before it was stopped"
);
assert_eq!(
replans(&store, result.run_id).len(),
1,
"the root was nudged exactly once"
);
assert_eq!(
stalls(&store, result.run_id).len(),
1,
"and stopped exactly once"
);
assert_eq!(
store.outcome(result.run_id).unwrap().as_deref(),
Some("stalled")
);
}
fn spawn_same() -> ToolCall {
call(
"spawn_agent",
json!({
"goal": "look into the failing test",
"verify_file": "notes.txt",
"verify_contains": "cause",
"max_steps": 1
}),
)
}
#[tokio::test]
async fn the_same_spawn_repeated_is_caught_even_though_every_step_counts_as_progress() {
let dir = ws();
let contract = never_passes(dir.path(), 8);
let provider = MockScript::new(vec![
vec![spawn_same()], vec![], vec![spawn_same()], vec![], vec![spawn_same()], vec![], vec![spawn_same()], vec![],
]);
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&provider,
&store,
&Policy::permissive(),
&ApproveAll,
&Containment::new(10, 4, 3, 1_000_000),
)
.await
.unwrap();
let rows = replans(&store, result.run_id);
assert_eq!(
rows.len(),
1,
"the repeated spawn must be nudged exactly once, got {rows:?}"
);
assert!(
provider.any_prompt_carries_the_directive(),
"the parent must actually have been told; the verdict alone changes nothing"
);
let steps = match result.outcome {
RunOutcome::Stalled { steps } | RunOutcome::StepCapReached { steps } => steps,
ref other => panic!("expected the loop to be caught, got {other:?}"),
};
assert!(
steps < 8,
"before 0.21.0 this ran its whole 8-step budget; it took {steps}"
);
}
#[tokio::test]
async fn three_different_spawns_that_each_do_work_are_never_flagged() {
let dir = ws();
let contract = TaskContract::workspace("split the work three ways", dir.path())
.with_verification(Verification::WorkspaceFileContains {
file: "c.txt".into(),
needle: "C".into(),
})
.with_max_steps(8);
let spawn_for = |goal: &str, file: &str, needle: &str| {
call(
"spawn_agent",
json!({ "goal": goal, "verify_file": file, "verify_contains": needle }),
)
};
let provider = MockScript::new(vec![
vec![spawn_for("write a", "a.txt", "A")],
vec![call(
"write_file",
json!({ "path": "a.txt", "content": "A" }),
)],
vec![spawn_for("write b", "b.txt", "B")],
vec![call(
"write_file",
json!({ "path": "b.txt", "content": "B" }),
)],
vec![spawn_for("write c", "c.txt", "C")],
vec![call(
"write_file",
json!({ "path": "c.txt", "content": "C" }),
)],
]);
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&provider,
&store,
&Policy::permissive(),
&ApproveAll,
&Containment::new(10, 4, 3, 1_000_000),
)
.await
.unwrap();
assert!(
!provider.any_prompt_carries_the_directive(),
"an agent delegating three different pieces of work is not stuck"
);
assert!(
replans(&store, result.run_id).is_empty() && stalls(&store, result.run_id).is_empty(),
"no replan and no stall row for a working agent; got {:?} / {:?}",
replans(&store, result.run_id),
stalls(&store, result.run_id)
);
assert!(
matches!(result.outcome, RunOutcome::Success { .. }),
"the delegating run should have succeeded, got {:?}",
result.outcome
);
}