use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use argonauts_core::app::{AppEvent, ConversationEvent, UiCommand};
use argonauts_core::asking::{Ask, AskLine, AskReply};
use argonauts_core::input::SlashCommand;
use argonauts_core::session_runner::SessionSpec;
use argonauts_core::steps::StepName;
use argonauts_testkit::{FakeRunner, FakeSession};
use serde_json::json;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use crate::controller::test_support::{CliOptions, TestDriver};
const TASK: &str = "Write the release note.";
const DONE: &str = "`test -f done.txt` holds";
fn alone(config: &'static str) -> CliOptions {
CliOptions {
prepare: Some(Box::new(move |repo: &argonauts_testkit::TempRepo| {
std::fs::write(
repo.global_config(),
format!("{config}\n[captain_or_crew]\nforce = \"captain\"\n"),
)
.unwrap();
})),
..CliOptions::default()
}
}
async fn slash(driver: &TestDriver, command: SlashCommand) {
driver.send(UiCommand::Slash { command }).await;
}
async fn set_goal(driver: &mut TestDriver, condition: &str) {
slash(
driver,
SlashCommand::Goal {
text: Some(condition.to_owned()),
},
)
.await;
notice(driver, "Goal set").await;
}
async fn notice(driver: &mut TestDriver, part: &str) -> String {
let event = driver
.until(|event| matches!(event, AppEvent::Notice { text, .. } if text.contains(part)))
.await;
match event {
AppEvent::Notice { text, .. } => text,
_ => unreachable!(),
}
}
fn noticed(driver: &TestDriver, part: &str) -> bool {
driver
.seen()
.iter()
.any(|event| matches!(event, AppEvent::Notice { text, .. } if text.contains(part)))
}
fn sent(spec: &SessionSpec) -> String {
spec.prompt
.iter()
.map(|part| part.text.as_str())
.collect::<Vec<_>>()
.join("\n")
}
fn done_on_the_second_turn(
first: &'static str,
then: &'static str,
) -> impl Fn(&SessionSpec) -> FakeSession + Send + Sync + 'static {
let turns = Arc::new(AtomicU32::new(0));
move |spec: &SessionSpec| {
if turns.fetch_add(1, Ordering::SeqCst) == 0 {
return FakeSession::completed(first);
}
std::fs::write(spec.workdir.join("done.txt"), "done\n").unwrap();
FakeSession::completed(then)
}
}
#[tokio::test]
#[cfg_attr(
target_os = "linux",
ignore = "the goal's check runs in the OS sandbox, which on Linux the argonauts binary launches, not a test binary"
)]
async fn alone_on_claude_code_argonauts_keeps_the_goal() {
let mut driver = TestDriver::start_with(alone("")).await;
driver.agent.respond(done_on_the_second_turn(
"Drafted the note.",
"Wrote done.txt.",
));
let worker = driver.agent.clone();
set_goal(&mut driver, DONE).await;
driver.submit(TASK).await;
notice(&mut driver, "Goal · kept by Argonauts Code").await;
let checking = notice(&mut driver, "Checking the goal").await;
assert_eq!(checking, "Checking the goal · checked by test -f done.txt");
let failed = notice(&mut driver, "✗").await;
assert!(failed.starts_with("✗ `test -f done.txt` holds"), "{failed}");
notice(&mut driver, "Goal · try 2 · sent back what still fails").await;
let met = notice(&mut driver, "Goal met").await;
assert_eq!(met, "Goal met · 2 tries · kept by Argonauts Code");
assert!(noticed(&driver, "✓ `test -f done.txt` holds"));
let conductor = driver.conductor.clone();
driver.exit().await;
let specs = worker.specs();
assert_eq!(specs.len(), 2, "two tries, two turns of the same session");
let first = sent(&specs[0]);
assert!(first.starts_with(TASK), "{first}");
assert!(
first.contains("This request is worked as a goal"),
"{first}"
);
assert!(
first.contains("- `test -f done.txt` holds (checked by test -f done.txt)"),
"{first}"
);
let second = sent(&specs[1]);
assert!(second.starts_with("The goal doesn't hold yet"), "{second}");
assert!(second.contains("- `test -f done.txt` holds"), "{second}");
assert!(!second.contains("worked as a goal"), "told once: {second}");
assert!(
conductor.starts().is_empty(),
"no run: the main agent works alone"
);
}
#[tokio::test]
#[cfg_attr(
target_os = "linux",
ignore = "the goal's check runs in the OS sandbox, which on Linux the argonauts binary launches, not a test binary"
)]
async fn alone_on_codex_the_goal_is_codexs_own_and_argonauts_still_checks() {
let mut driver = TestDriver::start_with(alone("crew.main = \"codex\"\n")).await;
let codex = driver
.directory
.add(
FakeRunner::cli("codex", "openai").respond_with(done_on_the_second_turn(
"Drafted the note.\ngoal: active · the file is still to write",
"Wrote done.txt.\ngoal: complete · done.txt is there",
)),
);
set_goal(&mut driver, DONE).await;
driver.submit(TASK).await;
notice(
&mut driver,
"Goal · kept by Codex's own goal · checked by Argonauts Code",
)
.await;
let says = notice(&mut driver, "Codex says").await;
assert_eq!(
says,
"Codex says · goal active · \"the file is still to write\""
);
let met = notice(&mut driver, "Goal met").await;
assert_eq!(
met,
"Goal met · 1 try · kept by Codex's own goal · checked by Argonauts Code"
);
let checks = driver
.seen()
.iter()
.filter(|event| {
matches!(event, AppEvent::Notice { text, .. } if text.starts_with("Checking the goal"))
})
.count();
assert_eq!(checks, 1);
driver.exit().await;
let specs = codex.specs();
assert_eq!(specs.len(), 2);
let first = sent(&specs[0]);
assert!(first.contains("with your own goal tool"), "{first}");
assert!(
first.contains("End your answer with one last line"),
"{first}"
);
let second = sent(&specs[1]);
assert!(
second.starts_with("Continue working toward your goal"),
"{second}"
);
}
#[tokio::test]
async fn the_judge_reads_a_line_no_command_checks() {
let mut driver = TestDriver::start_with(alone("")).await;
driver
.agent
.respond(|_spec: &SessionSpec| FakeSession::completed("Wrote the note."));
let worker = driver.agent.clone();
let verdicts = Arc::new(AtomicU32::new(0));
let asked = Arc::clone(&verdicts);
let judge = driver
.directory
.add(
FakeRunner::cli("codex", "openai").respond_with(move |spec: &SessionSpec| {
assert_eq!(spec.step, Some(StepName::CheckGoal));
let met = asked.fetch_add(1, Ordering::SeqCst) > 0;
FakeSession::structured(json!({
"verdicts": [{
"id": "G1",
"verdict": if met { "met" } else { "not_yet" },
"evidence": if met { "NOTES.md names --dry-run" } else { "" },
"missing": if met { "" } else { "the note never names --dry-run" },
}],
"overall": if met { "met" } else { "not_yet" },
"reason": if met { "" } else { "the flag isn't named" },
}))
}),
);
set_goal(&mut driver, "the release note names the --dry-run flag").await;
assert!(noticed(&driver, "Goal set · checked by the judge"));
driver.submit(TASK).await;
let failed = notice(&mut driver, "✗").await;
assert_eq!(
failed,
"✗ the release note names the --dry-run flag · the note never names --dry-run"
);
let met = notice(&mut driver, "Goal met").await;
assert_eq!(met, "Goal met · 2 tries · kept by Argonauts Code");
driver.exit().await;
let read = judge.specs();
assert_eq!(read.len(), 2);
assert!(sent(&read[0]).contains("You are the judge of a goal"));
assert!(sent(&read[0]).contains("the release note names the --dry-run flag"));
let second = sent(&worker.specs()[1]);
assert!(
second.contains("the note never names --dry-run"),
"{second}"
);
}
#[tokio::test]
async fn a_paused_goal_sends_nothing_round() {
let mut driver = TestDriver::start_with(alone("")).await;
driver
.agent
.respond(|_spec: &SessionSpec| FakeSession::completed("Drafted the note."));
let worker = driver.agent.clone();
set_goal(&mut driver, DONE).await;
slash(
&driver,
SlashCommand::Goal {
text: Some("pause".to_owned()),
},
)
.await;
notice(&mut driver, "Goal paused").await;
driver.submit(TASK).await;
driver
.until(|event| {
matches!(
event,
AppEvent::Conversation {
event: ConversationEvent::TurnFinished { .. }
}
)
})
.await;
let events = driver.exit().await;
assert_eq!(worker.specs().len(), 1, "one turn, and nothing sent back");
assert!(!sent(&worker.specs()[0]).contains("worked as a goal"));
assert!(!events.iter().any(
|event| matches!(event, AppEvent::Notice { text, .. } if text.contains("Checking the goal"))
));
}
async fn ask(spec: &SessionSpec, ask: Ask) -> AskReply {
let asking = spec.asking.clone().expect("the turn listens for questions");
let mut stream = TcpStream::connect(&asking.endpoint).await.unwrap();
let line = AskLine {
token: asking.token,
ask,
};
let text = format!("{}\n", serde_json::to_string(&line).unwrap());
stream.write_all(text.as_bytes()).await.unwrap();
let mut answer = String::new();
BufReader::new(stream).read_line(&mut answer).await.unwrap();
serde_json::from_str(answer.trim()).unwrap()
}
fn goal_tool(arguments: serde_json::Value) -> Ask {
Ask::Tool {
name: argonauts_mcp::session::SET_GOAL_TOOL.to_owned(),
arguments,
}
}
#[tokio::test]
#[cfg_attr(
target_os = "linux",
ignore = "the goal's check runs in the OS sandbox, which on Linux the argonauts binary launches, not a test binary"
)]
async fn the_main_session_sets_a_goal_for_one_request_with_its_tool() {
let mut driver = TestDriver::start_with(alone("")).await;
driver.directory.taking_tools("claude-code");
driver.agent.respond(|spec: &SessionSpec| {
std::fs::write(spec.workdir.join("done.txt"), "done\n").unwrap();
FakeSession::completed("Wrote done.txt.").with_delay(std::time::Duration::from_millis(300))
});
let worker = driver.agent.clone();
driver
.submit("Write the release note, and keep going until done.txt is there.")
.await;
let deadline = tokio::time::Instant::now() + crate::controller::test_support::WAIT;
let spec = loop {
if let Some(spec) = worker.specs().into_iter().next() {
break spec;
}
assert!(tokio::time::Instant::now() < deadline, "no session started");
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
};
let refused = ask(&spec, goal_tool(json!({"condition": ""}))).await;
assert!(refused.is_error, "{refused:?}");
assert!(
refused.text.contains("a goal needs a condition"),
"{refused:?}"
);
let reply = ask(&spec, goal_tool(json!({"condition": DONE}))).await;
assert!(!reply.is_error, "{reply:?}");
assert!(
reply
.text
.starts_with("Goal set for this request · checked by test -f done.txt."),
"{}",
reply.text
);
let met = notice(&mut driver, "Goal met").await;
assert_eq!(met, "Goal met · 1 try · kept by Argonauts Code");
assert!(noticed(
&driver,
"Goal set for this request · checked by test -f done.txt"
));
slash(&driver, SlashCommand::Goal { text: None }).await;
notice(&mut driver, "No goal is set").await;
driver.exit().await;
}