use std::sync::atomic::{AtomicUsize, Ordering};
use async_trait::async_trait;
use supercode_harness::session_title::SessionTitler;
use supercode_harness::{
Agent, ChatMessage, ChatRequest, Config, Provider, Role, SessionStore, Usage,
};
fn temp_dir(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"supercode-p4b-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
struct AlwaysFinalProvider {
calls: AtomicUsize,
}
#[async_trait]
impl Provider for AlwaysFinalProvider {
async fn complete(
&self,
_req: &ChatRequest,
_on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
) -> supercode_harness::Result<(ChatMessage, Usage)> {
let n = self.calls.fetch_add(1, Ordering::SeqCst);
Ok((
ChatMessage::assistant(format!("final-{n}")),
Usage::default(),
))
}
}
#[tokio::test]
async fn stop_gate_default_off_returns_immediately() {
let config = Config::builder().build();
let mut agent = Agent::with_provider(
config,
Box::new(AlwaysFinalProvider {
calls: AtomicUsize::new(0),
}),
);
let reply = agent.send("go").await.unwrap();
assert_eq!(reply, "final-0", "no stop_gate installed: stop immediately");
}
#[tokio::test]
async fn stop_gate_vetoes_once_then_allows_the_second_final_answer() {
let veto_calls = std::sync::Arc::new(AtomicUsize::new(0));
let vc = veto_calls.clone();
let config = Config::builder()
.stop_gate(Box::new(move |_content: &str| {
let n = vc.fetch_add(1, Ordering::SeqCst);
if n == 0 {
Some("not done yet, keep going".to_string())
} else {
None
}
}))
.build();
let mut agent = Agent::with_provider(
config,
Box::new(AlwaysFinalProvider {
calls: AtomicUsize::new(0),
}),
);
let reply = agent.send("go").await.unwrap();
assert_eq!(reply, "final-1", "veto forces a second model round-trip");
assert_eq!(
veto_calls.load(Ordering::SeqCst),
2,
"gate consulted once per would-be-final answer"
);
assert!(agent
.history()
.iter()
.any(|m| m.role == Role::User && m.content.as_deref() == Some("not done yet, keep going")));
}
#[tokio::test]
async fn stop_gate_still_bounded_by_max_iterations_if_it_always_vetoes() {
let config = Config::builder()
.max_iterations(3)
.stop_gate(Box::new(|_: &str| Some("never stop".to_string())))
.build();
let mut agent = Agent::with_provider(
config,
Box::new(AlwaysFinalProvider {
calls: AtomicUsize::new(0),
}),
);
let err = agent.send("go").await.unwrap_err();
assert!(
matches!(err, supercode_harness::Error::MaxIterations(3)),
"an always-vetoing gate must not loop forever — bounded by max_iterations: {err:?}"
);
}
struct AssertingProvider<F> {
check: F,
}
#[async_trait]
impl<F> Provider for AssertingProvider<F>
where
F: Fn(&ChatRequest) -> ChatMessage + Send + Sync,
{
async fn complete(
&self,
req: &ChatRequest,
_on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
) -> supercode_harness::Result<(ChatMessage, Usage)> {
Ok(((self.check)(req), Usage::default()))
}
}
#[tokio::test]
async fn queued_steer_message_delivered_one_at_a_time_and_leftover_persists_for_next_turn() {
let provider = AssertingProvider {
check: |req: &ChatRequest| {
assert!(
req.messages
.iter()
.any(|m| m.content.as_deref() == Some("first")),
"queued steer message must be injected before this request"
);
assert!(
!req.messages
.iter()
.any(|m| m.content.as_deref() == Some("second")),
"one-at-a-time mode must not deliver the second queued message yet"
);
ChatMessage::assistant("ok")
},
};
let config = Config::builder().build();
let mut agent = Agent::with_provider(config, Box::new(provider));
agent.queue_steer("first");
agent.queue_steer("second");
let reply = agent.send("go").await.unwrap();
assert_eq!(reply, "ok");
assert_eq!(
agent.queued_steer_count(),
1,
"the second steer message stays queued for the next turn"
);
}
#[tokio::test]
async fn steering_mode_all_drains_every_queued_message_at_once() {
let provider = AssertingProvider {
check: |req: &ChatRequest| {
let combined = req
.messages
.iter()
.rev()
.find(|m| m.role == Role::User && m.content.as_deref() != Some("go"))
.and_then(|m| m.content.clone());
assert_eq!(combined.as_deref(), Some("first\n\nsecond"));
ChatMessage::assistant("ok")
},
};
let config = Config::builder()
.steering_mode(supercode_harness::SteeringMode::All)
.build();
let mut agent = Agent::with_provider(config, Box::new(provider));
agent.queue_steer("first");
agent.queue_steer("second");
agent.send("go").await.unwrap();
assert_eq!(
agent.queued_steer_count(),
0,
"All mode drains everything at once"
);
}
#[tokio::test]
async fn queued_follow_up_message_is_delivered_at_idle_and_continues_the_loop() {
struct FollowUpProvider {
calls: AtomicUsize,
}
#[async_trait]
impl Provider for FollowUpProvider {
async fn complete(
&self,
req: &ChatRequest,
_on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
) -> supercode_harness::Result<(ChatMessage, Usage)> {
let n = self.calls.fetch_add(1, Ordering::SeqCst);
if n == 0 {
Ok((ChatMessage::assistant("first answer"), Usage::default()))
} else {
assert!(
req.messages
.iter()
.any(|m| m.content.as_deref() == Some("follow up question")),
"follow-up message must be injected once the loop went idle"
);
Ok((ChatMessage::assistant("second answer"), Usage::default()))
}
}
}
let config = Config::builder().build();
let mut agent = Agent::with_provider(
config,
Box::new(FollowUpProvider {
calls: AtomicUsize::new(0),
}),
);
agent.queue_follow_up("follow up question");
let reply = agent.send("go").await.unwrap();
assert_eq!(reply, "second answer");
assert_eq!(agent.queued_steer_count(), 0);
}
#[tokio::test]
async fn queued_follow_up_takes_priority_over_a_pending_stop_gate_veto() {
struct OnceProvider {
calls: AtomicUsize,
}
#[async_trait]
impl Provider for OnceProvider {
async fn complete(
&self,
_req: &ChatRequest,
_on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
) -> supercode_harness::Result<(ChatMessage, Usage)> {
let n = self.calls.fetch_add(1, Ordering::SeqCst);
Ok((
ChatMessage::assistant(format!("answer-{n}")),
Usage::default(),
))
}
}
let gate_calls = std::sync::Arc::new(AtomicUsize::new(0));
let gc = gate_calls.clone();
let config = Config::builder()
.stop_gate(Box::new(move |_: &str| {
gc.fetch_add(1, Ordering::SeqCst);
None
}))
.build();
let mut agent = Agent::with_provider(
config,
Box::new(OnceProvider {
calls: AtomicUsize::new(0),
}),
);
agent.queue_follow_up("one more thing");
let reply = agent.send("go").await.unwrap();
assert_eq!(reply, "answer-1");
assert_eq!(
gate_calls.load(Ordering::SeqCst),
1,
"stop_gate is skipped entirely on the idle point where a follow-up was delivered instead"
);
}
struct EchoLongProvider;
#[async_trait]
impl Provider for EchoLongProvider {
async fn complete(
&self,
_req: &ChatRequest,
_on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
) -> supercode_harness::Result<(ChatMessage, Usage)> {
Ok((ChatMessage::assistant("x".repeat(3000)), Usage::default()))
}
}
#[tokio::test]
async fn compaction_pressure_trigger_fires_and_appends_focus_instructions() {
let config = Config::builder()
.model("totally-unrecognized-model-xyz")
.compaction_pressure(199_000, 500)
.compaction_focus_instructions("stay focused on the login bug")
.build();
let mut agent = Agent::with_provider(config, Box::new(EchoLongProvider));
for i in 0..8 {
agent.send(format!("turn {i}")).await.unwrap();
}
let compacted = agent.history().iter().any(|m| {
m.role == Role::System
&& m.content
.as_deref()
.is_some_and(|c| c.contains("earlier conversation compacted"))
});
assert!(compacted, "pressure trigger must have fired across 8 turns");
let has_focus = agent.history().iter().any(|m| {
m.content
.as_deref()
.is_some_and(|c| c.contains("Focus: stay focused on the login bug"))
});
assert!(
has_focus,
"the marker must carry the configured focus instructions"
);
}
#[tokio::test]
async fn compaction_pressure_trigger_off_by_default_never_fires() {
let config = Config::builder()
.model("totally-unrecognized-model-xyz")
.build();
let mut agent = Agent::with_provider(config, Box::new(EchoLongProvider));
for i in 0..8 {
agent.send(format!("turn {i}")).await.unwrap();
}
let compacted = agent.history().iter().any(|m| {
m.content
.as_deref()
.is_some_and(|c| c.contains("compacted"))
});
assert!(
!compacted,
"pressure trigger must stay off when unconfigured"
);
}
struct UsageProvider {
calls: AtomicUsize,
}
#[async_trait]
impl Provider for UsageProvider {
async fn complete(
&self,
_req: &ChatRequest,
_on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
) -> supercode_harness::Result<(ChatMessage, Usage)> {
let n = self.calls.fetch_add(1, Ordering::SeqCst) as u64;
let usage = Usage {
prompt_tokens: 100 + n,
completion_tokens: 10 + n,
total_tokens: 110 + 2 * n,
prompt_tokens_details: None,
};
Ok((ChatMessage::assistant(format!("answer {n}")), usage))
}
}
#[tokio::test]
async fn usage_records_accumulate_per_turn_and_round_trip_losslessly_through_the_store() {
let config = Config::builder().model("test-model").build();
let mut agent = Agent::with_provider(
config,
Box::new(UsageProvider {
calls: AtomicUsize::new(0),
}),
);
agent.send("q1").await.unwrap();
agent.send("q2").await.unwrap();
let records = agent.usage_records().to_vec();
assert_eq!(records.len(), 2);
assert_eq!(records[0].turn, 0);
assert_eq!(records[0].model, "test-model");
assert_eq!(records[0].prompt_tokens, 100);
assert_eq!(records[1].turn, 1);
assert_eq!(records[1].prompt_tokens, 101);
let dir = temp_dir("usage-log-roundtrip");
let store = SessionStore::open(&dir).unwrap();
agent.save_usage_log(&store, "sess1").unwrap();
let loaded = store.load_usage_log("sess1").unwrap();
assert_eq!(
loaded, records,
"usage log must round-trip losslessly through the store"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn usage_log_is_empty_by_default_no_calls_made_yet() {
let config = Config::builder().build();
let agent = Agent::with_provider(
config,
Box::new(UsageProvider {
calls: AtomicUsize::new(0),
}),
);
assert!(agent.usage_records().is_empty());
}
struct FakeTitler {
response: String,
}
impl SessionTitler for FakeTitler {
fn title(&self, _preview: &str) -> supercode_harness::Result<String> {
Ok(self.response.clone())
}
fn model_id(&self) -> &str {
"fake-small-model"
}
}
#[tokio::test]
async fn agent_auto_title_uses_installed_titler_and_persists_via_session_store() {
let config = Config::builder().auto_title(true).build();
let mut agent = Agent::with_provider(
config,
Box::new(AlwaysFinalProvider {
calls: AtomicUsize::new(0),
}),
);
agent.send("please fix the login bug").await.unwrap();
agent.set_session_titler(FakeTitler {
response: "Fix the login bug".to_string(),
});
let title = agent.auto_title().unwrap();
assert_eq!(title, "Fix the login bug");
let dir = temp_dir("auto-title-roundtrip");
let store = SessionStore::open(&dir).unwrap();
store.save("sess1", &title, "{}").unwrap();
let info = store
.list()
.into_iter()
.find(|i| i.name == "sess1")
.unwrap();
assert_eq!(info.title, title, "title must round-trip through the store");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn agent_auto_title_is_none_when_no_titler_installed() {
let config = Config::builder().auto_title(true).build();
let mut agent = Agent::with_provider(
config,
Box::new(AlwaysFinalProvider {
calls: AtomicUsize::new(0),
}),
);
agent.send("hello").await.unwrap();
assert!(agent.auto_title().is_none());
}