use amberfork_model::DiffResult;
use assert_cmd::Command;
use std::path::{Path, PathBuf};
const EXIT_CONVERGED: i32 = 0;
const EXIT_FORKED: i32 = 1;
const EXIT_TROUBLE: i32 = 2;
fn fixture_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../spike/fixtures/smoke")
}
fn amberfork() -> Command {
Command::cargo_bin("amberfork").expect("amberfork binary builds")
}
fn manifest() -> (PathBuf, PathBuf, usize) {
let dir = fixture_dir();
let manifest: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(dir.join("pair_smoke.json")).unwrap())
.unwrap();
let bad = dir.join(manifest["failing"].as_str().unwrap());
let good = dir.join(manifest["reference"].as_str().unwrap());
let gold = manifest["gold_step"].as_u64().expect("gold_step") as usize;
(bad, good, gold)
}
#[test]
fn forked_pair_exits_1_and_json_locates_the_gold_step() {
let (bad, good, gold) = manifest();
let assert = amberfork()
.arg("diff")
.arg(&bad)
.arg("--against")
.arg(&good)
.arg("--json")
.assert()
.code(EXIT_FORKED);
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
let result: DiffResult =
serde_json::from_str(&stdout).expect("--json stdout must be a valid DiffResult");
let fork = result.fork.expect("smoke pair encodes a real divergence");
assert_eq!(
fork.b_step,
Some(gold),
"fork must land on failing-run step {gold} (manifest gold)"
);
assert_eq!(
result.runs.b.n_steps,
result
.alignment
.iter()
.filter(|m| m.b_idx.is_some())
.count()
);
assert!(
result.field_diffs.iter().any(|fd| fd.step == fork.index),
"--json must carry field diffs at the fork's alignment index"
);
}
#[test]
fn self_diff_exits_0_and_json_reports_converged() {
let (bad, _, _) = manifest();
let assert = amberfork()
.arg("diff")
.arg(&bad)
.arg("--against")
.arg(&bad)
.arg("--json")
.assert()
.code(EXIT_CONVERGED);
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
let result: DiffResult = serde_json::from_str(&stdout).unwrap();
assert_eq!(
result.fork, None,
"a run against itself is the converged state"
);
}
#[test]
fn jsonl_input_exits_2_and_stderr_names_the_shape_the_file_and_the_guide() {
let (_, good, _) = manifest();
let jsonl = Path::new(env!("CARGO_TARGET_TMPDIR")).join("raw_transcript.jsonl");
std::fs::write(
&jsonl,
"{\"role\": \"assistant\", \"content\": \"hi\"}\n{\"role\": \"tool\", \"name\": \"web.search\"}\n",
)
.unwrap();
amberfork()
.arg("diff")
.arg(&jsonl)
.arg("--against")
.arg(&good)
.assert()
.code(EXIT_TROUBLE)
.stdout(predicates::str::is_empty())
.stderr(predicates::str::contains("raw_transcript.jsonl"))
.stderr(predicates::str::contains("JSON-Lines"))
.stderr(predicates::str::contains("docs/run-on-your-own-agent.md"));
}
fn longest_side() -> usize {
let (bad, good, _) = manifest();
[bad, good]
.iter()
.map(|path| {
let run: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
run["steps"].as_array().expect("canonical trace").len()
})
.max()
.expect("two sides")
}
#[test]
fn lowered_max_steps_exits_2_and_stderr_names_the_escape_hatch() {
let (bad, good, _) = manifest();
amberfork()
.arg("diff")
.arg(&bad)
.arg("--against")
.arg(&good)
.arg("--max-steps")
.arg((longest_side() - 1).to_string())
.assert()
.code(EXIT_TROUBLE)
.stdout(predicates::str::is_empty())
.stderr(predicates::str::contains("alignment guard"))
.stderr(predicates::str::contains("--max-steps"));
}
#[test]
fn max_steps_exactly_at_the_trace_length_is_honored() {
let (bad, good, _) = manifest();
amberfork()
.arg("diff")
.arg(&bad)
.arg("--against")
.arg(&good)
.arg("--max-steps")
.arg(longest_side().to_string())
.assert()
.code(EXIT_FORKED);
}
#[test]
fn missing_file_exits_2_with_the_path_on_stderr_and_clean_stdout() {
let (_, good, _) = manifest();
amberfork()
.arg("diff")
.arg("no/such/trace.json")
.arg("--against")
.arg(&good)
.assert()
.code(EXIT_TROUBLE)
.stdout(predicates::str::is_empty())
.stderr(predicates::str::contains("no/such/trace.json"));
}
#[test]
fn verify_companion_flags_without_verify_exit_2_and_name_the_gate() {
let (bad, good, _) = manifest();
amberfork()
.arg("diff")
.arg(&bad)
.arg("--against")
.arg(&good)
.arg("--upstream")
.arg("http://127.0.0.1:0")
.assert()
.code(EXIT_TROUBLE)
.stdout(predicates::str::is_empty())
.stderr(predicates::str::contains("--verify"));
}
#[test]
fn verify_without_upstream_exits_2_and_names_the_missing_flag() {
let (bad, good, _) = manifest();
amberfork()
.arg("diff")
.arg(&bad)
.arg("--against")
.arg(&good)
.arg("--verify")
.arg("--base-url-env")
.arg("OPENAI_BASE_URL")
.arg("--")
.arg("true")
.assert()
.code(EXIT_TROUBLE)
.stdout(predicates::str::is_empty())
.stderr(predicates::str::contains("--upstream"));
}
#[test]
fn verify_on_canonical_traces_exits_2_and_asks_for_a_cassette() {
let (bad, good, _) = manifest();
amberfork()
.arg("diff")
.arg(&bad)
.arg("--against")
.arg(&good)
.arg("--verify")
.arg("--upstream")
.arg("http://127.0.0.1:0")
.arg("--base-url-env")
.arg("OPENAI_BASE_URL")
.arg("--")
.arg("true")
.assert()
.code(EXIT_TROUBLE)
.stdout(predicates::str::is_empty())
.stderr(predicates::str::contains("cassette"));
}
#[test]
fn verify_runs_below_one_is_rejected_by_the_parser() {
let (bad, good, _) = manifest();
amberfork()
.arg("diff")
.arg(&bad)
.arg("--against")
.arg(&good)
.arg("--verify")
.arg("--runs")
.arg("0")
.arg("--upstream")
.arg("http://127.0.0.1:0")
.arg("--base-url-env")
.arg("OPENAI_BASE_URL")
.arg("--")
.arg("true")
.assert()
.code(EXIT_TROUBLE)
.stderr(predicates::str::contains("invalid value"));
}