mod common;
use common::{
Judges, fixture, fixture_with_quota, fixture_with_review_seat_that_recovers_on_retry,
fixture_with_silent_review_seat,
};
use magi::graph::Runner;
use magi::run::RunStatus;
fn run_git(repo: &std::path::Path, args: &[&str]) {
let out = std::process::Command::new("git")
.args(args)
.current_dir(repo)
.output()
.expect("spawn git");
assert!(
out.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
}
common::e2e! {
async fn a_review_only_run_reviews_an_existing_branch_without_competing() {
let _home = common::home_lock().await;
let fx = fixture(_home, Judges::Unanimous, true);
run_git(&fx.repo, &["checkout", "-q", "-b", "feat/by-hand"]);
std::fs::write(fx.repo.join("note.txt"), "written by a human\n").unwrap();
run_git(&fx.repo, &["add", "-A"]);
run_git(&fx.repo, &["commit", "-q", "-m", "add note.txt by hand"]);
run_git(&fx.repo, &["checkout", "-q", "main"]);
let mut runner = Runner::review(&fx.repo, "feat/by-hand", fx.config.clone())
.await
.expect("open a review-only run");
assert_eq!(runner.state.candidates.len(), 1);
let c = &runner.state.candidates[0];
assert_eq!(c.label, 'A');
assert_eq!(c.branch, "feat/by-hand");
assert_eq!(c.commits, 1);
assert!(
!fx.config.agents.iter().any(|a| a.id == c.agent),
"the candidate must not be attributed to a roster agent, got {}",
c.agent
);
let tally = runner.state.tally.as_ref().expect("a decided tally");
assert_eq!(tally.winner, 'A');
assert_eq!(tally.rankings, 0, "nothing was ranked");
runner.execute().await.expect("execute");
let state = &runner.state;
assert!(state.judgements.is_empty(), "nobody judged");
assert!(state.deliberation.is_empty(), "nobody deliberated");
assert!(state.votes.is_empty(), "nobody voted");
assert_eq!(state.reviews.len(), 2, "{:?}", state.reviews);
let first = &state.reviews[0];
assert_eq!(first.reviews.len(), 2);
assert!(!first.clean);
let fix = first.fix.as_ref().expect("the fixer ran");
assert!(fix.committed, "fixes must land on the branch");
assert!(state.reviews[1].clean);
assert!(state.gate.iter().all(|o| o.ok()));
assert_eq!(state.status, RunStatus::Ready);
let winner = state.winner().expect("winner");
assert!(winner.worktree.join("fixed.txt").is_file());
let head = std::process::Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.current_dir(&winner.worktree)
.output()
.expect("spawn git");
assert_eq!(
String::from_utf8_lossy(&head.stdout).trim(),
"feat/by-hand",
"the review worktree must stay attached to the branch"
);
}
}
common::e2e! {
async fn a_reviewer_that_never_answered_is_never_reported_as_a_clean_round() {
let _home = common::home_lock().await;
let mut fx = fixture_with_silent_review_seat(_home, &["review-2"]);
fx.config.graph.review_rounds = 1;
run_git(&fx.repo, &["checkout", "-q", "-b", "feat/by-hand"]);
std::fs::write(fx.repo.join("note.txt"), "written by a human\n").unwrap();
run_git(&fx.repo, &["add", "-A"]);
run_git(&fx.repo, &["commit", "-q", "-m", "add note.txt by hand"]);
run_git(&fx.repo, &["checkout", "-q", "main"]);
let mut runner = Runner::review(&fx.repo, "feat/by-hand", fx.config.clone())
.await
.expect("open a review-only run");
runner.execute().await.expect("execute");
let state = &runner.state;
assert_eq!(state.reviews.len(), 1, "{:?}", state.reviews);
for round in &state.reviews {
assert_eq!(round.answered, 1);
assert_eq!(round.expected, 2);
assert!(round.incomplete());
assert_eq!(round.blocking, 0, "the seat that did answer found nothing");
assert!(
!round.clean,
"a round missing half its panel must never be reported clean: {round:?}"
);
let missing = round
.reviews
.iter()
.find(|r| r.reviewer == 2)
.expect("review-2's record is still in the round");
assert!(
missing.failed.is_some(),
"a seat that never produced a usable answer is `failed`, whatever \
`attempts` says: {missing:?}"
);
}
assert!(state.reviews.iter().all(|r| r.fix.is_none()));
assert_eq!(state.status, RunStatus::Blocked);
}
}
common::e2e! {
async fn a_reviewer_that_only_answers_on_retry_is_recorded_as_recovered_not_silent() {
let _home = common::home_lock().await;
let mut fx = fixture_with_review_seat_that_recovers_on_retry(_home, &["review-2"]);
fx.config.graph.review_rounds = 1;
run_git(&fx.repo, &["checkout", "-q", "-b", "feat/by-hand"]);
std::fs::write(fx.repo.join("note.txt"), "written by a human\n").unwrap();
run_git(&fx.repo, &["add", "-A"]);
run_git(&fx.repo, &["commit", "-q", "-m", "add note.txt by hand"]);
run_git(&fx.repo, &["checkout", "-q", "main"]);
let mut runner = Runner::review(&fx.repo, "feat/by-hand", fx.config.clone())
.await
.expect("open a review-only run");
runner.execute().await.expect("execute");
let state = &runner.state;
assert_eq!(state.reviews.len(), 1, "{:?}", state.reviews);
let round = &state.reviews[0];
assert_eq!(round.answered, 2, "both seats answered, one on a retry");
assert!(!round.incomplete());
let recovered = round
.reviews
.iter()
.find(|r| r.reviewer == 2)
.expect("review-2's record is still in the round");
assert!(
recovered.failed.is_none(),
"a seat that did eventually answer is not `failed`: {recovered:?}"
);
assert!(
recovered.attempts > 0,
"recorded attempts must show the retry actually happened: {recovered:?}"
);
assert!(
state
.events
.iter()
.any(|e| e.node == "review" && e.message.contains("retry 1")),
"the retry itself is also visible in the event log: {:?}",
state.events
);
}
}
common::e2e! {
async fn a_reviewer_rate_limited_by_quota_gates_clean_on_the_answered_panel() {
let _home = common::home_lock().await;
let mut fx = fixture_with_quota(_home, &["review-2"]);
fx.config.graph.review_rounds = 5;
run_git(&fx.repo, &["checkout", "-q", "-b", "feat/by-hand"]);
std::fs::write(fx.repo.join("note.txt"), "written by a human\n").unwrap();
run_git(&fx.repo, &["add", "-A"]);
run_git(&fx.repo, &["commit", "-q", "-m", "add note.txt by hand"]);
run_git(&fx.repo, &["checkout", "-q", "main"]);
let mut runner = Runner::review(&fx.repo, "feat/by-hand", fx.config.clone())
.await
.expect("open a review-only run");
runner.execute().await.expect("execute");
let state = &runner.state;
assert_eq!(state.reviews.len(), 1, "{:?}", state.reviews);
let round = &state.reviews[0];
assert_eq!(round.answered, 1);
assert_eq!(round.expected, 2);
assert!(round.incomplete(), "review-2 never answered");
assert!(
round.clean,
"a quota-lost seat must not block a decision the rest of the panel already made: \
{round:?}"
);
let missing = round
.reviews
.iter()
.find(|r| r.reviewer == 2)
.expect("review-2's own record");
assert!(
missing
.failed
.as_deref()
.is_some_and(|why| why.contains("rate limited")),
"{missing:?}"
);
assert_eq!(state.quota.len(), 1, "{:?}", state.quota);
assert_eq!(state.quota[0].seat, "review-2");
assert_eq!(state.status, RunStatus::Ready);
}
}
common::e2e! {
async fn a_panel_lost_entirely_to_quota_still_waits_instead_of_deciding_on_nobody() {
let _home = common::home_lock().await;
let mut fx = fixture_with_quota(_home, &["review-1", "review-2"]);
fx.config.graph.review_rounds = 1;
run_git(&fx.repo, &["checkout", "-q", "-b", "feat/by-hand"]);
std::fs::write(fx.repo.join("note.txt"), "written by a human\n").unwrap();
run_git(&fx.repo, &["add", "-A"]);
run_git(&fx.repo, &["commit", "-q", "-m", "add note.txt by hand"]);
run_git(&fx.repo, &["checkout", "-q", "main"]);
let mut runner = Runner::review(&fx.repo, "feat/by-hand", fx.config.clone())
.await
.expect("open a review-only run");
runner.execute().await.expect("execute");
let state = &runner.state;
assert_eq!(state.reviews.len(), 1, "{:?}", state.reviews);
let round = &state.reviews[0];
assert_eq!(round.answered, 0);
assert_eq!(round.expected, 2);
assert!(
!round.clean,
"a panel with nobody left to review must never gate clean: {round:?}"
);
assert_eq!(state.quota.len(), 2, "{:?}", state.quota);
assert_eq!(state.status, RunStatus::Blocked);
}
}
common::e2e! {
async fn review_refuses_the_cases_that_cannot_mean_anything() {
let _home = common::home_lock().await;
let fx = fixture(_home, Judges::Unanimous, false);
let missing = Runner::review(&fx.repo, "no/such/branch", fx.config.clone()).await;
assert!(missing.is_err(), "a branch that does not exist");
let base = Runner::review(&fx.repo, "main", fx.config.clone()).await;
assert!(base.is_err(), "reviewing the base branch");
run_git(&fx.repo, &["branch", "feat/empty"]);
let empty = Runner::review(&fx.repo, "feat/empty", fx.config.clone()).await;
assert!(empty.is_err(), "a branch with no commits of its own");
}
}
fn rev(repo: &std::path::Path, rev: &str) -> String {
let out = std::process::Command::new("git")
.args(["rev-parse", rev])
.current_dir(repo)
.output()
.expect("spawn git");
assert!(out.status.success(), "rev-parse {rev}");
String::from_utf8_lossy(&out.stdout).trim().to_owned()
}
fn wire_origin(fx: &common::Fixture) -> std::path::PathBuf {
let origin = fx.tmp.path().join("origin.git");
run_git(
fx.tmp.path(),
&[
"clone",
"--bare",
"-q",
fx.repo.to_str().unwrap(),
origin.to_str().unwrap(),
],
);
run_git(
&fx.repo,
&["remote", "add", "origin", origin.to_str().unwrap()],
);
let side = fx.tmp.path().join("side");
run_git(
fx.tmp.path(),
&[
"clone",
"-q",
origin.to_str().unwrap(),
side.to_str().unwrap(),
],
);
run_git(&side, &["config", "user.name", "other"]);
run_git(&side, &["config", "user.email", "other@example.com"]);
side
}
fn push_real_work(side: &std::path::Path, branch: &str) {
run_git(side, &["checkout", "-q", "-b", branch]);
std::fs::write(side.join("real.txt"), "the real change\n").unwrap();
run_git(side, &["add", "-A"]);
run_git(side, &["commit", "-q", "-m", "the real change"]);
run_git(side, &["push", "-q", "origin", branch]);
}
common::e2e! {
async fn a_stale_local_branch_is_fast_forwarded_to_the_pushed_work() {
let _home = common::home_lock().await;
let fx = fixture(_home, Judges::Unanimous, true);
let side = wire_origin(&fx);
run_git(&fx.repo, &["branch", "feat/pushed", "main"]);
push_real_work(&side, "feat/pushed");
let pushed = rev(&side, "HEAD");
let runner = Runner::review(&fx.repo, "feat/pushed", fx.config.clone())
.await
.expect("open a review-only run");
assert_eq!(rev(&fx.repo, "refs/heads/feat/pushed"), pushed);
let c = &runner.state.candidates[0];
assert!(c.worktree.join("real.txt").is_file());
assert_eq!(c.commits, 1);
}
}
common::e2e! {
async fn a_diverged_branch_with_local_work_is_refused_and_leaves_no_worktree() {
let _home = common::home_lock().await;
let fx = fixture(_home, Judges::Unanimous, true);
let side = wire_origin(&fx);
run_git(&fx.repo, &["checkout", "-q", "-b", "feat/split"]);
std::fs::write(fx.repo.join("local.txt"), "local only\n").unwrap();
run_git(&fx.repo, &["add", "-A"]);
run_git(&fx.repo, &["commit", "-q", "-m", "local only"]);
run_git(&fx.repo, &["checkout", "-q", "main"]);
let local = rev(&fx.repo, "feat/split");
push_real_work(&side, "feat/split");
let err = Runner::review(&fx.repo, "feat/split", fx.config.clone())
.await
.err()
.expect("a diverged branch must not be reviewed");
assert!(err.to_string().contains("diverged"), "{err}");
assert_eq!(rev(&fx.repo, "refs/heads/feat/split"), local);
let list = std::process::Command::new("git")
.args(["worktree", "list"])
.current_dir(&fx.repo)
.output()
.unwrap();
assert!(!String::from_utf8_lossy(&list.stdout).contains("under-review"));
}
}
common::e2e! {
async fn a_rewritten_placeholder_gives_way_to_the_pushed_work() {
let _home = common::home_lock().await;
let fx = fixture(_home, Judges::Unanimous, true);
let side = wire_origin(&fx);
run_git(&fx.repo, &["checkout", "-q", "-b", "feat/rewritten"]);
run_git(&fx.repo, &["commit", "-q", "--allow-empty", "-m", "placeholder"]);
run_git(&fx.repo, &["checkout", "-q", "main"]);
push_real_work(&side, "feat/rewritten");
let pushed = rev(&side, "HEAD");
let runner = Runner::review(&fx.repo, "feat/rewritten", fx.config.clone())
.await
.expect("open a review-only run");
assert_eq!(rev(&fx.repo, "refs/heads/feat/rewritten"), pushed);
assert!(runner.state.candidates[0].worktree.join("real.txt").is_file());
}
}
common::e2e! {
async fn a_branch_whose_tree_equals_the_base_is_refused_and_releases_its_worktree() {
let _home = common::home_lock().await;
let fx = fixture(_home, Judges::Unanimous, true);
run_git(&fx.repo, &["checkout", "-q", "-b", "feat/placeholder"]);
run_git(
&fx.repo,
&["commit", "-q", "--allow-empty", "-m", "placeholder"],
);
run_git(&fx.repo, &["checkout", "-q", "main"]);
let err = Runner::review(&fx.repo, "feat/placeholder", fx.config.clone())
.await
.err()
.expect("an empty-tree branch must not be reviewed");
assert!(err.to_string().contains("tree identical"), "{err}");
run_git(&fx.repo, &["worktree", "prune"]);
let list = std::process::Command::new("git")
.args(["worktree", "list"])
.current_dir(&fx.repo)
.output()
.unwrap();
assert!(!String::from_utf8_lossy(&list.stdout).contains("feat/placeholder"));
}
}