#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use agentplane::model::ModelProvider;
use agentplane::model::fake::FakeProvider;
use serde_json::{Value, json};
use speccoder_core::verify::Verdict;
use speccoder_core::{CommandData, CommandResult, RunReport};
use speccoder_runtime::run_with;
fn has(program: &str) -> bool {
Command::new(program)
.arg("--version")
.output()
.is_ok_and(|o| o.status.success())
}
fn git(dir: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.args(["-c", "maintenance.auto=false", "-c", "gc.auto=0"])
.args([
"-c",
"user.name=t",
"-c",
"user.email=t@example.com",
"-c",
"commit.gpgsign=false",
])
.args([
"-c",
"init.defaultBranch=main",
"-c",
"core.hooksPath=/dev/null",
])
.args(args)
.current_dir(dir)
.output()
.unwrap();
assert!(
out.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).into_owned()
}
const CONFIG: &str = "[model]\nprovider = \"openai\"\nmodel = \"gpt-5.4-mini\"\n\n[checks.test]\nargv = [\"cargo\", \"nextest\", \"run\", \"--profile\", \"speccoder\", \"--no-tests=warn\"]\njunit = \"target/nextest/speccoder/junit.xml\"\n\n[commands]\nallow = [[\"cargo\", \"test\"], [\"cargo\", \"check\"], [\"cargo\", \"build\"]]\n\n[acceptance]\nheld_out = false\n";
fn poc() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let r = dir.path();
let fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/poc-greet");
assert!(
Command::new("cp")
.arg("-R")
.arg(fixture.join("."))
.arg(r)
.status()
.unwrap()
.success()
);
fs::rename(r.join("Cargo.toml.fixture"), r.join("Cargo.toml")).unwrap();
fs::write(r.join("speccoder.toml"), CONFIG).unwrap();
fs::create_dir_all(r.join(".config")).unwrap();
fs::write(
r.join(".config/nextest.toml"),
"[profile.speccoder.junit]\npath = \"junit.xml\"\n",
)
.unwrap();
fs::write(r.join(".gitignore"), "target\n").unwrap();
assert!(
Command::new("cargo")
.args(["generate-lockfile", "--offline"])
.current_dir(r)
.status()
.unwrap()
.success()
);
git(r, &["init", "-q"]);
git(r, &["add", "-A"]);
git(r, &["commit", "-q", "-m", "spec(001-greet-function)"]);
dir
}
fn done(summary: &str) -> Value {
json!({ "summary": summary, "status": "done", "files_changed": [], "checks_run": [], "notes": [] })
}
fn report(r: &CommandResult) -> &RunReport {
match r.data() {
Some(CommandData::Run(rep)) => rep,
other => panic!("{} → {other:?}", r.message()),
}
}
fn run(repo: &Path, state: &Path, task: &str, f: Arc<FakeProvider>) -> CommandResult {
let d: Arc<dyn ModelProvider> = f;
run_with(repo, None, task, state, d, None)
}
const GREET: &str = "pub fn greet(name: &str) -> String {\n if name.is_empty() {\n \"Hello, world!\".to_string()\n } else {\n format!(\"Hello, {}!\", name)\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn it_works() {\n assert_eq!(greet(\"world\"), \"Hello, world!\");\n }\n}\n";
const TEST: &str = "use greet::greet;\n\n#[test]\nfn greets_by_name() {\n assert_eq!(greet(\"Ada\"), \"Hello, Ada!\");\n}\n";
const KEPT: &str = "use greet::greet;\n\n#[test]\nfn keeps_the_text() {\n assert_eq!(greet(\"Ada L., Esq.\"), \"Hello, Ada L., Esq.!\");\n}\n";
#[test]
fn the_poc_s_dead_test_files_fail_and_every_tick_says_why() {
if !has("cargo-nextest") {
eprintln!("skipped: cargo-nextest is not installed");
return;
}
let r = poc();
let state = tempfile::tempdir().unwrap();
for task in ["T001", "T002"] {
let f = FakeProvider::new();
f.will_structure(done("the crate structure is in place"));
let res = run(r.path(), state.path(), task, f);
let rep = report(&res);
assert_eq!(
rep.verdict,
Some(Verdict::RegressionOnly),
"{}",
res.message()
);
assert_eq!(
rep.regression.as_ref().unwrap().text,
"no user story — nothing to derive a test from"
);
}
let f = FakeProvider::new();
f.will_call_tool(
"c1",
"write_file",
json!({ "path": "src/lib.rs", "content": GREET }),
);
f.will_structure(done("greet"));
let t003 = run(r.path(), state.path(), "T003", f);
let rep = report(&t003);
assert_eq!(
rep.verdict,
Some(Verdict::RegressionOnly),
"{}",
t003.message()
);
assert_eq!(rep.regression.as_ref().unwrap().reason.name(), "no_story");
let f = FakeProvider::new();
f.will_structure(done("the rules are encoded"));
let t004 = run(r.path(), state.path(), "T004", f);
assert_eq!(
report(&t004).verdict,
Some(Verdict::RegressionOnly),
"{}",
t004.message()
);
let f = FakeProvider::new();
let argv = ["cargo", "test", "--test", "acceptance_us1"];
let kept = ["cargo", "test", "--test", "acceptance_us1_kept"];
f.will_structure(json!({
"files": [
{ "path": "tests/acceptance_us1.rs", "content": TEST, "scenarios": ["US1-1"], "argv": argv },
{ "path": "tests/acceptance_us1_kept.rs", "content": KEPT, "scenarios": ["US1-2"], "argv": kept },
],
"argv": argv,
"notes": [],
"expectations": [
{ "scenario": "US1-1", "given": "a name", "expect": "Hello, the name!" },
{ "scenario": "US1-2", "given": "spaces and punctuation", "expect": "kept as given" },
],
"untestable": [],
}));
f.will_call_tool(
"c1",
"write_file",
json!({ "path": "tests/unit/greet_name.rs", "content": TEST }),
);
f.will_structure(done("added unit test coverage"));
f.will_call_tool(
"c2",
"write_file",
json!({ "path": "tests/greet_name.rs", "content": TEST }),
);
f.will_structure(done("moved the test where Cargo builds it"));
let t005 = run(r.path(), state.path(), "T005", f.clone());
let rep = report(&t005);
assert_eq!(
rep.verdict,
Some(Verdict::RegressionOnly),
"{}",
t005.message()
);
assert_eq!(
rep.earlier[0].verdict,
Some(Verdict::Failed {
reasons: vec!["tests not collected: `tests/unit/greet_name.rs` — Cargo builds `tests/*.rs` and `tests/<dir>/main.rs` (and the modules they declare); this file is neither".to_owned()]
})
);
let c = rep.collection.as_ref().unwrap();
assert_eq!(c.files[0].path, "tests/greet_name.rs");
assert_eq!(
c.files[0].decided_by,
Some(speccoder_core::collection::DecidedBy::Junit)
);
assert_eq!(c.files[0].tests, 1);
assert_eq!(
rep.regression.as_ref().unwrap().text,
"already passing before the attempt"
);
let message = git(Path::new(&rep.worktree), &["log", "-1", "--format=%B"]);
assert!(
message.contains("Regression-Only: already_passing\n"),
"{message}"
);
let inbox = speccoder_runtime::inbox::inbox_with(r.path(), None, Some(state.path()));
let Some(CommandData::Inbox(i)) = inbox.data() else {
panic!("{}", inbox.message())
};
let u: Vec<_> = i.items.iter().filter(|i| i.kind == "unverified").collect();
assert_eq!(u.len(), 1, "{:?}", i.items);
assert_eq!(
u[0].title,
"Little was verified in `001-greet-function`: 5 of 5 ticks are regression-only"
);
let texts: Vec<&str> = u[0]
.unverified
.as_ref()
.unwrap()
.counts
.iter()
.map(|c| c.text.as_str())
.collect();
assert_eq!(texts, ["4 had no user story", "1 was already passing"]);
}
#[test]
fn an_acceptance_command_given_as_one_string_runs() {
if !has("cargo-nextest") {
eprintln!("skipped: cargo-nextest is not installed");
return;
}
let r = poc();
let state = tempfile::tempdir().unwrap();
for task in ["T001", "T002", "T003", "T004"] {
let f = FakeProvider::new();
if task == "T003" {
f.will_call_tool(
"c1",
"write_file",
json!({ "path": "src/lib.rs", "content": GREET }),
);
}
f.will_structure(done("done"));
let _ = run(r.path(), state.path(), task, f);
}
let f = FakeProvider::new();
f.will_structure(json!({
"files": [{ "path": "tests/acceptance_us1.rs", "content": TEST, "scenarios": ["US1-1"], "argv": ["cargo test --test acceptance_us1"] }],
"argv": ["cargo test --test acceptance_us1"],
"notes": [],
"expectations": [{ "scenario": "US1-1", "given": "a name", "expect": "Hello, the name!" }],
"untestable": [],
}));
f.will_structure(done("nothing to add"));
let t005 = run(r.path(), state.path(), "T005", f);
let oracle = git(
r.path(),
&[
"log",
"--format=%(trailers)",
"--grep=^Oracle: US1$",
"speccoder/001-greet-function",
],
);
assert!(
oracle.contains("Oracle-Scenarios: US1-1=green") && oracle.contains("Oracle-Tests: 1"),
"{oracle} {}",
t005.message()
);
}