use std::sync::atomic::{AtomicUsize, Ordering};
use async_trait::async_trait;
use supercode_harness::{
Agent, ChatMessage, ChatRequest, Config, FunctionCall, Provider, Role, ToolCall, Usage,
};
struct RepeatingToolCallScript {
call_index: AtomicUsize,
n_repeats: usize,
command: String,
}
#[async_trait]
impl Provider for RepeatingToolCallScript {
async fn complete(
&self,
_req: &ChatRequest,
_on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
) -> supercode_harness::Result<(ChatMessage, Usage)> {
let n = self.call_index.fetch_add(1, Ordering::SeqCst);
if n < self.n_repeats {
Ok((
ChatMessage {
role: Role::Assistant,
content: None,
content_parts: None,
tool_calls: Some(vec![ToolCall {
id: format!("call_{n}"),
kind: "function".into(),
function: FunctionCall {
name: "bash".into(),
arguments: serde_json::json!({"command": self.command}).to_string(),
},
}]),
tool_call_id: None,
name: None,
metadata: Default::default(),
},
Usage::default(),
))
} else {
Ok((ChatMessage::assistant("done"), Usage::default()))
}
}
}
fn temp_cwd() -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"sc-p4c-doomloop-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[tokio::test]
async fn default_off_repeated_identical_calls_all_succeed() {
let config = Config::builder().cwd(temp_cwd()).build();
assert_eq!(config.doom_loop_threshold, None);
let script = RepeatingToolCallScript {
call_index: AtomicUsize::new(0),
n_repeats: 5,
command: "true".to_string(),
};
let mut agent = Agent::with_provider(config, Box::new(script));
let answer = agent.send("go").await.unwrap();
assert_eq!(answer, "done");
let errors = agent
.history()
.iter()
.filter(|m| m.role == Role::Tool)
.filter(|m| {
m.content
.as_deref()
.map(|c| c.starts_with("Error:"))
.unwrap_or(false)
})
.count();
assert_eq!(
errors, 0,
"no call should be blocked with the threshold unset"
);
let tool_count = agent
.history()
.iter()
.filter(|m| m.role == Role::Tool)
.count();
assert_eq!(tool_count, 5);
}
#[tokio::test]
async fn threshold_blocks_the_nth_consecutive_identical_call() {
let config = Config::builder()
.cwd(temp_cwd())
.doom_loop_threshold(3)
.build();
let script = RepeatingToolCallScript {
call_index: AtomicUsize::new(0),
n_repeats: 4,
command: "true".to_string(),
};
let mut agent = Agent::with_provider(config, Box::new(script));
agent.send("go").await.unwrap();
let tool_msgs: Vec<&ChatMessage> = agent
.history()
.iter()
.filter(|m| m.role == Role::Tool)
.collect();
assert_eq!(tool_msgs.len(), 4, "all 4 calls still produce a result");
let is_err = |m: &ChatMessage| {
m.content
.as_deref()
.map(|c| c.starts_with("Error:"))
.unwrap_or(false)
};
assert!(!is_err(tool_msgs[0]), "{:?}", tool_msgs[0].content);
assert!(!is_err(tool_msgs[1]), "{:?}", tool_msgs[1].content);
assert!(is_err(tool_msgs[2]), "{:?}", tool_msgs[2].content);
assert!(
tool_msgs[2]
.content
.as_deref()
.unwrap()
.contains("doom-loop"),
"{:?}",
tool_msgs[2].content
);
assert!(is_err(tool_msgs[3]), "{:?}", tool_msgs[3].content);
}
#[tokio::test]
async fn a_different_call_in_between_resets_the_streak() {
struct AlternatingScript {
call_index: AtomicUsize,
}
#[async_trait]
impl Provider for AlternatingScript {
async fn complete(
&self,
_req: &ChatRequest,
_on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
) -> supercode_harness::Result<(ChatMessage, Usage)> {
let n = self.call_index.fetch_add(1, Ordering::SeqCst);
let commands = ["true", "echo hi", "true", "echo hi"];
if n < commands.len() {
Ok((
ChatMessage {
role: Role::Assistant,
content: None,
content_parts: None,
tool_calls: Some(vec![ToolCall {
id: format!("call_{n}"),
kind: "function".into(),
function: FunctionCall {
name: "bash".into(),
arguments: serde_json::json!({"command": commands[n]}).to_string(),
},
}]),
tool_call_id: None,
name: None,
metadata: Default::default(),
},
Usage::default(),
))
} else {
Ok((ChatMessage::assistant("done"), Usage::default()))
}
}
}
let config = Config::builder()
.cwd(temp_cwd())
.doom_loop_threshold(2)
.build();
let mut agent = Agent::with_provider(
config,
Box::new(AlternatingScript {
call_index: AtomicUsize::new(0),
}),
);
agent.send("go").await.unwrap();
let errors = agent
.history()
.iter()
.filter(|m| m.role == Role::Tool)
.filter(|m| {
m.content
.as_deref()
.map(|c| c.starts_with("Error:"))
.unwrap_or(false)
})
.count();
assert_eq!(
errors, 0,
"no two CONSECUTIVE calls are identical, so the streak counter never reaches 2"
);
}
#[tokio::test]
async fn threshold_of_one_never_fires_boundary() {
let config = Config::builder()
.cwd(temp_cwd())
.doom_loop_threshold(1)
.build();
let script = RepeatingToolCallScript {
call_index: AtomicUsize::new(0),
n_repeats: 3,
command: "true".to_string(),
};
let mut agent = Agent::with_provider(config, Box::new(script));
agent.send("go").await.unwrap();
let errors = agent
.history()
.iter()
.filter(|m| m.role == Role::Tool)
.filter(|m| {
m.content
.as_deref()
.map(|c| c.starts_with("Error:"))
.unwrap_or(false)
})
.count();
assert_eq!(errors, 0, "threshold=1 must never block anything");
}