mod common;
use common::{fixture_with_failure, fixture_with_quota};
use magi::graph::Runner;
use magi::report;
use magi::run::RunStatus;
#[tokio::test]
async fn below_quorum_run_is_stalled_not_ready_and_resumable() {
let _home = common::home_lock().await;
let fx = fixture_with_quota(_home, &["judge-1", "judge-2"]);
let mut runner = Runner::start(&fx.repo, "create note.txt".to_owned(), fx.config.clone())
.await
.expect("start");
runner.execute().await.expect("execute");
let state = &runner.state;
let losses = &state.quota;
assert_eq!(losses.len(), 2, "both rate-limited judges recorded");
let seats: Vec<&str> = losses.iter().map(|l| l.seat.as_str()).collect();
assert!(seats.contains(&"judge-1"), "{seats:?}");
assert!(seats.contains(&"judge-2"), "{seats:?}");
assert!(losses.iter().all(|l| l.node == "judge"));
assert_eq!(state.candidates.len(), 3);
assert_eq!(state.judgements.len(), 3, "all three judge slots recorded");
let tally = state.tally.as_ref().expect("a tally is still computed");
assert_eq!(tally.judges, 3);
assert_eq!(tally.present, 1);
assert_eq!(tally.quorum, 2);
assert!(!tally.met_quorum);
assert_eq!(state.status, RunStatus::Stalled);
assert!(state.reviews.is_empty());
assert!(state.gate.is_empty());
assert!(state.merge.is_none());
assert!(state.candidates.iter().all(|c| !c.folded));
assert!(report::line(state).contains("stalled"));
assert!(
report::line(state).contains("quorum 1/3"),
"{}",
report::line(state)
);
let full = report::run(state);
assert!(full.contains("BELOW QUORUM"), "{full}");
let mut again = Runner::resume(&state.id).expect("resume");
again.execute().await.expect("re-execute");
assert_eq!(again.state.status, RunStatus::Stalled);
assert_eq!(again.state.judgements.len(), 3);
assert_eq!(again.state.tally.as_ref().unwrap().present, 1);
}
#[tokio::test]
async fn a_rate_limited_seat_is_recorded_and_the_panel_accounts_for_it() {
let _home = common::home_lock().await;
let fx = fixture_with_quota(_home, &["judge-1"]);
let mut runner = Runner::start(&fx.repo, "create note.txt".to_owned(), fx.config.clone())
.await
.expect("start");
runner.execute().await.expect("execute");
let state = &runner.state;
let losses: Vec<(&str, &str)> = state
.quota
.iter()
.map(|q| (q.seat.as_str(), q.node.as_str()))
.collect();
assert!(
losses.contains(&("judge-1", "judge")),
"quota losses were {losses:?}"
);
let tally = state.tally.as_ref().expect("tally");
assert_eq!(tally.judges, 3);
assert_eq!(tally.present, 2, "one seat was rate limited out");
assert!(tally.met_quorum, "2 of 3 still meets the majority quorum");
let ranked = state
.judgements
.iter()
.filter(|j| !j.ranking.is_empty())
.count();
assert_eq!(ranked, 2, "{:?}", state.judgements);
}
#[tokio::test]
async fn a_stalled_run_recovers_to_ready_once_the_quota_resets() {
let _home = common::home_lock().await;
let fx = fixture_with_quota(_home, &["judge-1", "judge-2"]);
let mut runner = Runner::start(&fx.repo, "create note.txt".to_owned(), fx.config.clone())
.await
.expect("start");
runner.execute().await.expect("execute");
assert_eq!(runner.state.status, RunStatus::Stalled);
assert_eq!(runner.state.tally.as_ref().unwrap().present, 1);
let id = runner.state.id.clone();
drop(runner);
{
let mut state = magi::run::RunState::load(&id).expect("load");
for a in &mut state.config.agents {
a.env.remove("MOCK_QUOTA_SEAT");
}
state.save().expect("save");
}
let mut again = Runner::resume(&id).expect("resume");
again.execute().await.expect("re-execute");
assert_eq!(
again.state.status,
RunStatus::Ready,
"{}",
report::run(&again.state)
);
let tally = again.state.tally.as_ref().expect("tally");
assert!(tally.met_quorum, "quorum restored on resume");
assert_eq!(tally.present, 3);
assert!(
again.state.quota.is_empty(),
"recovered seats drop their loss"
);
assert!(
!report::line(&again.state).contains("quorum"),
"{}",
report::line(&again.state)
);
}
#[tokio::test]
async fn a_stalled_run_stays_stalled_when_the_quota_has_not_reset() {
let _home = common::home_lock().await;
let fx = fixture_with_quota(_home, &["judge-1", "judge-2"]);
let mut runner = Runner::start(&fx.repo, "create note.txt".to_owned(), fx.config.clone())
.await
.expect("start");
runner.execute().await.expect("execute");
assert_eq!(runner.state.status, RunStatus::Stalled);
let id = runner.state.id.clone();
drop(runner);
let mut again = Runner::resume(&id).expect("resume");
again.execute().await.expect("re-execute");
assert_eq!(again.state.status, RunStatus::Stalled);
assert_eq!(again.state.tally.as_ref().unwrap().present, 1);
assert!(again.state.reviews.is_empty());
assert!(again.state.gate.is_empty());
assert!(again.state.merge.is_none());
}
#[tokio::test]
async fn a_plain_failure_collapse_stalls_and_recovers_on_resume() {
let _home = common::home_lock().await;
let fx = fixture_with_failure(_home, &["judge-1", "judge-2"]);
let mut runner = Runner::start(&fx.repo, "create note.txt".to_owned(), fx.config.clone())
.await
.expect("start");
runner.execute().await.expect("execute");
let state = &runner.state;
let art = state.dir().join("artifacts");
assert!(
art.join("judge-1-retry1.out").exists(),
"a plain failure is retried like any other unusable reply"
);
assert!(
state.quota.is_empty(),
"a plain failure is not a quota loss"
);
assert!(
state
.events
.iter()
.any(|e| e.node == "judge" && e.message.contains("retry 1")),
"no retry event recorded: {:?}",
state.events
);
assert!(
state.active.is_empty(),
"a finished attempt leaves nothing marked as still answering: {:?}",
state.active
);
let failed_j = |seat: &str| {
state
.judgements
.iter()
.find(|j| j.seat == seat)
.map(|j| j.failed.is_some())
.unwrap_or(false)
};
assert!(failed_j("judge-1"), "judge-1 failed outright");
assert!(failed_j("judge-2"), "judge-2 failed outright");
assert!(!failed_j("judge-3"), "judge-3 ranked and voted");
assert_eq!(state.status, RunStatus::Stalled);
let tally = state.tally.as_ref().expect("tally");
assert_eq!(tally.present, 1);
assert!(!tally.met_quorum);
let id = state.id.clone();
drop(runner);
{
let mut s = magi::run::RunState::load(&id).expect("load");
for a in &mut s.config.agents {
a.env.remove("MOCK_FAILED_SEAT");
}
s.save().expect("save");
}
let mut again = Runner::resume(&id).expect("resume");
again.execute().await.expect("re-execute");
assert_eq!(
again.state.status,
RunStatus::Ready,
"{}",
report::run(&again.state)
);
assert!(again.state.tally.as_ref().unwrap().met_quorum);
assert_eq!(again.state.tally.as_ref().unwrap().present, 3);
}
#[tokio::test]
async fn a_plain_failure_collapse_stays_stalled_while_the_failure_persists() {
let _home = common::home_lock().await;
let fx = fixture_with_failure(_home, &["judge-1", "judge-2"]);
let mut runner = Runner::start(&fx.repo, "create note.txt".to_owned(), fx.config.clone())
.await
.expect("start");
runner.execute().await.expect("execute");
assert_eq!(runner.state.status, RunStatus::Stalled);
let id = runner.state.id.clone();
drop(runner);
let mut again = Runner::resume(&id).expect("resume");
again.execute().await.expect("re-execute");
assert_eq!(again.state.status, RunStatus::Stalled);
assert_eq!(again.state.tally.as_ref().unwrap().present, 1);
assert!(again.state.reviews.is_empty());
assert!(again.state.gate.is_empty());
assert!(again.state.merge.is_none());
}
#[tokio::test]
async fn the_full_panel_present_is_still_a_healthy_ready() {
let _home = common::home_lock().await;
let fx = fixture_with_quota(_home, &[]);
let mut runner = Runner::start(&fx.repo, "create note.txt".to_owned(), fx.config.clone())
.await
.expect("start");
runner.execute().await.expect("execute");
let state = &runner.state;
assert!(state.quota.is_empty());
let tally = state.tally.as_ref().expect("tally");
assert!(tally.met_quorum);
assert_eq!(tally.present, 3);
assert_eq!(state.status, RunStatus::Ready);
assert!(
!report::line(state).contains("quorum"),
"{}",
report::line(state)
);
}