1use std::fmt::Write as _;
13use std::sync::atomic::{AtomicBool, Ordering};
14
15use crate::config::{MergeMode, MergeStyle};
16use crate::run::{
17 CommandOutcome, ContinuationOutcome, E2eStatus, GateStatus, JobStatus, Liveness,
18 OperatorFixOutcome, RunState, RunStatus, tail,
19};
20use crate::stats::Stats;
21use crate::verdict::ReviewVote;
22
23static COLOR: AtomicBool = AtomicBool::new(true);
24
25pub fn set_color(on: bool) {
27 COLOR.store(on, Ordering::Relaxed);
28}
29
30fn paint(text: &str, code: &str) -> String {
31 if COLOR.load(Ordering::Relaxed) {
32 format!("\x1b[{code}m{text}\x1b[0m")
33 } else {
34 text.to_owned()
35 }
36}
37
38fn bold(t: &str) -> String {
39 paint(t, "1")
40}
41fn dim(t: &str) -> String {
42 paint(t, "2")
43}
44fn red(t: &str) -> String {
45 paint(t, "31")
46}
47fn green(t: &str) -> String {
48 paint(t, "32")
49}
50fn yellow(t: &str) -> String {
51 paint(t, "33")
52}
53fn cyan(t: &str) -> String {
54 paint(t, "36")
55}
56
57fn status_word(state: &RunState) -> String {
68 if state.unmerged_by_design() {
69 return cyan("unmerged (no-op by design)");
70 }
71 let text = state.status.display_label();
72 if state.needs_attention() {
73 return bold(&yellow(&format!("{text} - release needs a human")));
74 }
75 match state.status {
76 RunStatus::Merged => bold(&green(text)),
77 RunStatus::Ready => green(text),
78 RunStatus::Stalled => bold(&yellow(text)),
79 RunStatus::Blocked => yellow(text),
80 RunStatus::Failed => red(text),
81 RunStatus::VerifiedNoop => cyan(text),
85 _ => cyan(text),
86 }
87}
88
89fn vote_tag(vote: ReviewVote) -> String {
93 let text = vote.label();
94 match vote {
95 ReviewVote::Approve => green(text),
96 ReviewVote::ApproveWithFindings => yellow(text),
97 ReviewVote::Reject => red(text),
98 }
99}
100
101pub fn line(state: &RunState) -> String {
103 line_with_liveness(state, Liveness::Unknown)
104}
105
106pub fn line_with_liveness(state: &RunState, live: Liveness) -> String {
109 let winner = state
110 .tally
111 .as_ref()
112 .map_or("-".to_owned(), |t| t.winner.to_string());
113 let agent = state.winner().map_or("-", |c| c.agent.as_str());
114 let quorum = match state.tally.as_ref() {
117 Some(t) if !t.met_quorum => format!(
118 " {}",
119 bold(&red(&format!("quorum {}/{}", t.present, t.judges)))
120 ),
121 Some(t) if t.present > 0 && t.present < t.judges => format!(
122 " {}",
123 yellow(&format!("judges {}/{}", t.present, t.judges))
124 ),
125 _ => String::new(),
126 };
127 let stale = if !state.status.done() && live == Liveness::Dead {
128 format!(
129 " {}",
130 bold(&yellow("STALE — driver exited; resume required"))
131 )
132 } else {
133 String::new()
134 };
135 format!(
136 "{} {:<20} {:>2}c {:>2}j win {} ({}){quorum}{stale} {}",
137 dim(&state.id),
138 status_word(state),
139 state.candidates.len(),
140 state.judgements.len(),
141 winner,
142 agent,
143 first_line(&state.instruction)
144 )
145}
146
147pub fn liveness_notice(state: &RunState, live: Liveness) -> String {
151 if !state.status.done() && live == Liveness::Dead {
152 format!(
153 "{}\n\n",
154 yellow("STALE — the process driving this run exited; resume it to continue.")
155 )
156 } else {
157 String::new()
158 }
159}
160
161fn first_line(text: &str) -> String {
162 let line = text.lines().next().unwrap_or_default();
163 if line.chars().count() > 68 {
164 format!("{}…", line.chars().take(67).collect::<String>())
165 } else {
166 line.to_owned()
167 }
168}
169
170fn short(commit: &str) -> String {
171 commit.chars().take(7).collect()
172}
173
174fn continuation_note(c: &crate::run::ContinuationRecord) -> String {
179 match c.outcome {
180 ContinuationOutcome::NotNeeded => String::new(),
181 ContinuationOutcome::Resumed => format!(" [resumed x{}]", c.attempts),
182 ContinuationOutcome::Exhausted => format!(" [continuation exhausted x{}]", c.attempts),
183 ContinuationOutcome::QuotaLost => " [continuation: quota]".to_owned(),
184 ContinuationOutcome::NoSession => " [no session to resume]".to_owned(),
185 }
186}
187
188fn jobs_section(state: &RunState) -> String {
209 let mut s = String::new();
210 if state.jobs.is_empty() {
211 if state
216 .config
217 .agents
218 .iter()
219 .any(|a| a.kind == crate::config::AgentKind::Codex)
220 {
221 let _ = writeln!(
222 s,
223 "\n{}",
224 dim(
225 "background jobs: no completed command evidence yet for this run (see \
226 active seats above for what is still mid-turn)"
227 )
228 );
229 }
230 return s;
231 }
232 let _ = writeln!(
233 s,
234 "\n{}",
235 bold("background jobs (from each seat's own CLI)")
236 );
237 let mut by_seat: std::collections::BTreeMap<(&str, &str), Vec<&crate::run::JobRecord>> =
238 std::collections::BTreeMap::new();
239 for j in &state.jobs {
240 by_seat
241 .entry((j.node.as_str(), j.seat.as_str()))
242 .or_default()
243 .push(j);
244 }
245 for ((node, seat), records) in by_seat {
246 let _ = writeln!(s, " {node}/{seat}");
247 for j in records {
248 let status = match j.status {
249 JobStatus::Completed => green("completed"),
250 JobStatus::Failed => red("failed"),
251 JobStatus::Unknown => yellow("unknown"),
252 };
253 let _ = writeln!(
254 s,
255 " {} {}{}{} checked {}",
256 dim(&j.id),
257 status,
258 j.exit_code
259 .map_or(String::new(), |c| format!(" (exit {c})")),
260 j.round.map_or(String::new(), |r| format!(" round {r}")),
261 j.checked_at
262 .to_zoned(jiff::tz::TimeZone::system())
263 .strftime("%Y-%m-%d %H:%M:%S")
264 );
265 let desc = first_line(&j.description);
266 if !desc.trim().is_empty() {
267 let _ = writeln!(s, " $ {desc}");
268 }
269 let summary = first_line(&j.result_summary);
270 if !summary.trim().is_empty() {
271 let _ = writeln!(s, " {}", dim(&summary));
272 }
273 }
274 }
275 let _ = writeln!(
276 s,
277 " {}",
278 dim(
279 "(adapter coverage: codex only today; other backends, and a command a CLI never \
280 reported finishing, leave no entry here — that is unknown, never \"nothing ran\")"
281 )
282 );
283 s
284}
285
286pub fn run(state: &RunState) -> String {
288 let mut s = String::new();
289 let _ = writeln!(
290 s,
291 "{} {} {}",
292 bold("magi run"),
293 bold(&state.id),
294 status_word(state)
295 );
296 let _ = writeln!(
297 s,
298 " repo {} ({} @ {})",
299 state.repo.display(),
300 state.base_branch,
301 short(&state.base_commit)
302 );
303 let _ = writeln!(s, " created {}", state.created_local());
304 let _ = writeln!(s, " task {}", first_line(&state.instruction));
305 let _ = writeln!(s, " state {}", state.dir().display());
306
307 let _ = writeln!(s, "\n{}", bold("candidates"));
308 for c in &state.candidates {
309 let flag = match (&c.failed, c.empty, &c.verified_noop) {
310 (Some(e), _, _) => red(&format!("failed: {e}")),
311 (None, true, Some(_)) => cyan("agent-verified no-op (unconfirmed)"),
315 (None, true, None) => yellow("no change"),
316 _ => format!("{} files, {} commits", c.files, c.commits),
317 };
318 let crown = if state.tally.as_ref().is_some_and(|t| t.winner == c.label) {
319 bold(&green(" <- winner"))
320 } else {
321 String::new()
322 };
323 let _ = writeln!(
324 s,
325 " {} {:<12} {:<30} {:>5}s{}",
326 bold(&c.label.to_string()),
327 c.agent,
328 flag,
329 c.duration_ms / 1000,
330 crown
331 );
332 if let Some(evidence) = &c.verified_noop {
333 let _ = writeln!(s, " {}", dim(&first_line(evidence)));
334 } else if !c.summary.trim().is_empty() {
335 let _ = writeln!(s, " {}", dim(&first_line(&c.summary)));
344 }
345 }
346
347 if !state.judgements.is_empty() {
348 let _ = writeln!(s, "\n{}", bold("blind judging"));
349 for j in &state.judgements {
350 match &j.failed {
351 Some(e) => {
352 let _ = writeln!(
353 s,
354 " judge {} {}",
355 j.judge,
356 red(&format!("no ranking: {e}"))
357 );
358 }
359 None => {
360 let _ = writeln!(
361 s,
362 " judge {} {:<12} {} confidence {}",
363 j.judge,
364 j.agent,
365 bold(&j.ranking.iter().collect::<String>()),
366 j.confidence.map_or("-".to_owned(), |c| c.to_string())
367 );
368 }
369 }
370 }
371 }
372
373 if let Some(t) = &state.tally {
374 if t.deliberated {
375 let _ = writeln!(s, "\n{}", bold("deliberation"));
376 for round in &state.deliberation {
377 for turn in &round.turns {
378 let _ = writeln!(
379 s,
380 " r{} judge {} -> {}",
381 round.round,
382 turn.judge,
383 turn.tentative.map_or("-".to_owned(), |c| c.to_string())
384 );
385 }
386 }
387 }
388
389 if !state.votes.is_empty() {
390 let _ = writeln!(s, "\n{}", bold("final votes (collected privately)"));
391 for v in &state.votes {
392 let _ = writeln!(
393 s,
394 " judge {} {:<12} {}{}",
395 v.judge,
396 v.agent,
397 bold(&v.vote.unwrap_or('?').to_string()),
398 if v.changed {
399 yellow(" (changed after deliberation)")
400 } else {
401 String::new()
402 }
403 );
404 }
405 }
406
407 let _ = writeln!(s, "\n{}", bold("tally"));
408 match &t.uncontested {
414 Some(reason) => {
415 let _ = writeln!(
416 s,
417 " judging {}",
418 cyan(&format!("not needed — {reason}"))
419 );
420 }
421 None => {
422 let _ = writeln!(
423 s,
424 " judges {} present{}",
425 if t.met_quorum {
426 green(&format!("{}/{}", t.present, t.judges))
427 } else {
428 red(&format!("{}/{}", t.present, t.judges))
429 },
430 if t.quorum > 0 {
431 format!(" ({quorum} required)", quorum = t.quorum)
432 } else {
433 String::new()
434 }
435 );
436 if !t.met_quorum {
437 let _ = writeln!(
438 s,
439 " {}",
440 bold(&red("BELOW QUORUM — verdict is not trustworthy"))
441 );
442 }
443 let _ = writeln!(
444 s,
445 " first choice {}",
446 t.first_choice
447 .iter()
448 .map(|(k, v)| format!("{k}:{v}"))
449 .collect::<Vec<_>>()
450 .join(" ")
451 );
452 let _ = writeln!(
453 s,
454 " initial {}",
455 match (t.rankings, t.unanimous_initial) {
456 (0, _) => red("no usable ranking"),
457 (1, _) => yellow("one usable ranking - not a consensus"),
458 (_, true) => green("unanimous"),
459 (_, false) => yellow("split"),
460 }
461 );
462 let _ = writeln!(
463 s,
464 " after votes {} ({} judge(s) moved)",
465 if t.unanimous_final {
466 green("unanimous")
467 } else {
468 yellow("still split")
469 },
470 t.changed_votes
471 );
472 if let Some(tb) = &t.tie_break {
473 let _ = writeln!(s, " tie break {tb}");
474 }
475 }
476 }
477 if !state.quota.is_empty() {
478 let _ = writeln!(
479 s,
480 " rate limited {}",
481 state
482 .quota
483 .iter()
484 .map(|q| q.seat.as_str())
485 .collect::<Vec<_>>()
486 .join(", ")
487 );
488 }
489 let _ = writeln!(s, " winner {}", bold(&green(&t.winner.to_string())));
490 }
491
492 if !state.withheld.is_empty() {
493 let _ = writeln!(s, "\n{}", bold("withheld from commit"));
494 for w in &state.withheld {
495 let _ = writeln!(
496 s,
497 " {} {} ({} lockfile; the directory uses {})",
498 yellow("!"),
499 w.path,
500 w.manager,
501 w.kept_by
502 );
503 }
504 }
505
506 if !state.reviews.is_empty() {
507 let _ = writeln!(s, "\n{}", bold("review + verification"));
508 for r in &state.reviews {
509 let raised: usize = r.reviews.iter().map(|x| x.findings.len()).sum();
510 let e2e = match r.e2e_status() {
518 E2eStatus::NotConfigured => dim("no e2e"),
519 E2eStatus::Deferred => yellow(&format!(
520 "e2e deferred{}",
521 r.e2e_defer_reason
522 .as_deref()
523 .map(|why| format!(" ({why})"))
524 .unwrap_or_default()
525 )),
526 E2eStatus::Passed => green("e2e green"),
527 E2eStatus::Failed if r.e2e.iter().any(CommandOutcome::build_failed) => {
528 yellow("e2e could not run (build/link failure)")
529 }
530 E2eStatus::Failed => red("e2e RED"),
531 E2eStatus::ResourceBlocked => {
536 yellow("e2e could not run (shared build cache unavailable)")
537 }
538 };
539 let e2e = if r.verify_retried {
540 format!("{e2e}, retried once")
541 } else {
542 e2e
543 };
544 let status = if r.incomplete() {
548 yellow("incomplete")
549 } else if r.clean {
550 green("clean")
551 } else {
552 yellow("open")
553 };
554 let panel = if r.incomplete() {
558 let missing: Vec<String> = r
559 .reviews
560 .iter()
561 .filter_map(|x| {
562 x.failed
563 .as_ref()
564 .map(|why| format!("review-{}: {why}", x.reviewer))
565 })
566 .collect();
567 format!(
568 " {}/{} reviewers answered ({})",
569 r.answered,
570 r.expected,
571 missing.join(", ")
572 )
573 } else {
574 String::new()
575 };
576 let verdict = r.verdict.map_or(String::new(), |v| {
582 format!(
583 ", verdict {}{}",
584 vote_tag(v),
585 if r.vote_split { " (panel split)" } else { "" }
586 )
587 });
588 let _ = writeln!(
589 s,
590 " round {} {} @ {}{}{panel} {raised} finding(s), {} blocking, {e2e}{verdict}{}",
591 r.round,
592 status,
593 short(&r.head),
594 r.verified_head.as_ref().map_or(String::new(), |head| {
595 format!(
596 " (verified @ {}{})",
597 short(head),
598 r.verified_at.map_or(String::new(), |t| format!(
599 " on {}",
600 t.to_zoned(jiff::tz::TimeZone::system())
601 .strftime("%Y-%m-%d %H:%M:%S")
602 ))
603 )
604 }),
605 r.blocking,
606 r.fix.as_ref().map_or(String::new(), |f| {
607 let tree = if r.progressed {
608 green("changed")
609 } else {
610 yellow("unchanged")
611 };
612 let cont = f
613 .continuation
614 .as_ref()
615 .map_or(String::new(), continuation_note);
616 match &f.failed {
617 Some(reason) => format!(
622 " fix: {}, tree {tree}{}{cont}",
623 yellow(&format!("adoption report lost ({reason})")),
624 if f.committed {
625 String::new()
626 } else {
627 red(" (NO COMMIT)")
628 }
629 ),
630 None => format!(
631 " fix: {} addressed / {} rejected, tree {tree}{}{cont}",
632 f.addressed.len(),
633 f.rejected.len(),
634 if f.committed {
635 String::new()
636 } else {
637 red(" (NO COMMIT)")
638 }
639 ),
640 }
641 })
642 );
643 for o in &r.e2e {
650 let label = if o.resource_blocked {
651 yellow("blocked")
652 } else if o.ok() {
653 green("pass")
654 } else {
655 red("FAIL")
656 };
657 let _ = writeln!(s, " {label} {}", o.command);
658 if !o.ok() {
659 let _ = writeln!(s, "{}", dim(&tail(&o.output_tail, 2_000)));
660 }
661 }
662 for rec in &r.reviews {
663 if let Some(vote) = rec.vote {
664 let _ = writeln!(s, " review-{} vote {}", rec.reviewer, vote_tag(vote));
665 }
666 for f in &rec.findings {
667 let adopted = r
668 .fix
669 .as_ref()
670 .is_some_and(|fix| fix.addressed.contains(&f.id));
671 let _ = writeln!(
672 s,
673 " {} [{:?}] {}{}",
674 dim(&f.id),
675 f.severity,
676 f.title,
677 if adopted {
678 green(" fixed")
679 } else {
680 String::new()
681 }
682 );
683 }
684 }
685 if let Some(fix) = &r.fix {
686 for rej in &fix.rejected {
687 let _ = writeln!(
688 s,
689 " {} {}: {}",
690 dim(&rej.id),
691 yellow("declined"),
692 rej.why
693 );
694 }
695 }
696 if !r.reconsideration.is_empty() {
700 let _ = writeln!(s, " {}", dim("reconsideration:"));
701 for rv in &r.reconsideration {
702 match rv.vote {
703 Some(v) => {
704 let _ = writeln!(
705 s,
706 " review-{} -> {} {}",
707 rv.reviewer,
708 vote_tag(v),
709 rv.reason
710 );
711 }
712 None => {
713 let _ = writeln!(
714 s,
715 " review-{} -> {}",
716 rv.reviewer,
717 red(&format!(
718 "no revote ({})",
719 rv.failed.as_deref().unwrap_or("unknown")
720 ))
721 );
722 }
723 }
724 }
725 }
726 }
727 if state.handed_off_with_open_findings() {
728 let _ = writeln!(
729 s,
730 "\n {}",
731 yellow(&format!(
732 "handed off with {} finding(s) still open — gate and e2e were green; \
733 see above for what a person should still look at",
734 state.open_findings().len()
735 ))
736 );
737 }
738 }
739
740 if !state.operator_fixes.is_empty() {
741 let _ = writeln!(s, "\n{}", bold("operator fix(es)"));
742 for (i, req) in state.operator_fixes.iter().enumerate() {
743 let _ = writeln!(
744 s,
745 " [{}] {} finding(s) at {}{}",
746 i + 1,
747 req.findings.len(),
748 req.requested_at
749 .to_zoned(jiff::tz::TimeZone::system())
750 .strftime("%Y-%m-%d %H:%M:%S"),
751 if req.stale {
752 yellow(" stale head, --allow-stale used")
753 } else {
754 String::new()
755 }
756 );
757 let _ = writeln!(s, " reason: {}", req.reason);
758 for f in &req.findings {
759 let outcome = match &f.outcome {
760 OperatorFixOutcome::Pending => yellow("pending"),
761 OperatorFixOutcome::Addressed => green("addressed"),
762 OperatorFixOutcome::Rejected { why } => red(&format!("rejected: {why}")),
763 OperatorFixOutcome::Unreported => {
764 red("unreported — no adoption report came back")
765 }
766 };
767 let _ = writeln!(
768 s,
769 " {} [{:?}] {} {outcome}",
770 dim(&f.id),
771 f.severity,
772 f.title
773 );
774 }
775 match &req.follow_up_review_run {
776 Some(id) => {
777 let _ = writeln!(s, " re-verified by run {id}");
778 }
779 None if req.fix.as_ref().is_some_and(|fx| fx.committed) => {
780 let _ = writeln!(
781 s,
782 " {}",
783 red("committed, but the follow-up review could not be opened")
784 );
785 }
786 None => {
787 let _ = writeln!(s, " no change committed; nothing to re-verify");
788 }
789 }
790 }
791 }
792
793 if let Some(bs) = &state.base_sync {
794 let _ = writeln!(s, "\n{}", bold("base sync"));
795 let status = if let Some(c) = &bs.conflict {
796 red(&format!("conflict: {}", first_line(c)))
797 } else if bs.behind == 0 {
798 green("in sync")
799 } else {
800 yellow(&format!("{} commit(s) behind, not yet rebased", bs.behind))
801 };
802 let _ = writeln!(
803 s,
804 " {} @ {} {status}{}",
805 state.base_branch,
806 short(&bs.tip),
807 if bs.attempts > 0 {
808 format!(" ({} rebase attempt(s))", bs.attempts)
809 } else {
810 String::new()
811 }
812 );
813 }
814
815 if !state.pre_gate.is_empty() {
816 let _ = writeln!(s, "\n{}", bold("pre_gate"));
817 for o in &state.pre_gate {
818 let _ = writeln!(
819 s,
820 " {} {}",
821 if o.ok() {
822 green("pass")
823 } else {
824 yellow("warn")
825 },
826 o.command
827 );
828 if !o.ok() {
829 let _ = writeln!(s, "{}", dim(&tail(&o.output_tail, 2_000)));
830 }
831 }
832 if let Some(c) = &state.pre_gate_commit {
833 let _ = writeln!(s, " committed mechanical fixes @ {}", short(c));
834 }
835 }
836
837 match state.gate_status() {
841 GateStatus::NotRun => {}
842 GateStatus::PassedWithNoCommands => {
843 let _ = writeln!(s, "\n{}", bold("gate"));
844 let _ = writeln!(s, " {} no gate commands configured", green("pass"));
845 }
846 GateStatus::Passed | GateStatus::Failed => {
847 let _ = writeln!(s, "\n{}", bold("gate"));
848 for o in &state.gate {
849 let _ = writeln!(
850 s,
851 " {} {}",
852 if o.ok() { green("pass") } else { red("FAIL") },
853 o.command
854 );
855 if !o.ok() {
856 let _ = writeln!(s, "{}", dim(&tail(&o.output_tail, 2_000)));
857 }
858 }
859 }
860 }
861
862 if let Some(m) = &state.merge {
863 let _ = writeln!(s, "\n{}", bold("merge"));
864 if m.mode == MergeMode::None {
865 let _ = writeln!(
869 s,
870 " mode None {}",
871 cyan("not landed — nothing to do by design")
872 );
873 if let Some(w) = state.winner() {
874 let _ = writeln!(
875 s,
876 " branch {} still exists, unmerged into {}",
877 w.branch, state.base_branch
878 );
879 }
880 let _ = writeln!(
886 s,
887 " rebase onto {} before merging by hand{}",
888 state.base_branch,
889 if state.config.merge.style == MergeStyle::Squash {
890 ", and pass an explicit commit message — a squash merge \
891 otherwise inherits the candidate's placeholder subject"
892 } else {
893 ""
894 }
895 );
896 let _ = writeln!(s, " {}", m.detail.lines().next().unwrap_or(""));
897 } else {
898 let _ = writeln!(
899 s,
900 " mode {:?} {}\n {}",
901 m.mode,
902 if m.ok {
903 green("ok")
904 } else {
905 yellow("not merged")
906 },
907 m.detail.lines().next().unwrap_or("")
908 );
909 }
910 }
911
912 if let Some(b) = &state.release_bump {
913 let _ = writeln!(s, "\n{}", bold("release bump"));
914 if let Some(v) = &b.version {
915 let _ = writeln!(s, " version v{v}");
916 }
917 if let Some(url) = &b.pr_url {
918 let _ = writeln!(s, " pr {url}");
919 }
920 let _ = writeln!(
921 s,
922 " automerge {}",
923 if b.automerge_enabled {
924 green("enabled")
925 } else if b.problem.is_some() {
926 bold(&red("FAILED"))
927 } else {
928 dim("not enabled")
929 }
930 );
931 if let Some(p) = &b.problem {
932 let _ = writeln!(s, " {}", p.lines().next().unwrap_or(""));
933 }
934 if let Some(a) = &b.action_required {
935 let _ = writeln!(s, " {} {a}", bold(&yellow("action required:")));
936 }
937 }
938
939 if !state.leaks.is_empty() {
940 let _ = writeln!(s, "\n{}", bold(&yellow("blindness warnings")));
941 for l in &state.leaks {
942 let _ = writeln!(s, " {} x{} in {}", l.token, l.count, l.site);
943 }
944 }
945
946 if let Some(w) = state.winner()
947 && !w.folded
948 {
949 let _ = writeln!(
950 s,
951 "\n{} {}\n branch {}",
952 bold("winner worktree"),
953 w.worktree.display(),
954 w.branch
955 );
956 }
957 s.push_str(&jobs_section(state));
958 s
959}
960
961pub fn active_seats(state: &RunState, live: Liveness) -> String {
977 if state.active.is_empty() {
978 return String::new();
979 }
980 let mut s = String::new();
981 let _ = writeln!(s, "\n{}", bold("running now"));
982 let now = jiff::Timestamp::now();
983 match live {
984 Liveness::Live => {}
985 Liveness::Dead => {
986 let _ = writeln!(
987 s,
988 " {}",
989 yellow(
990 "no live daemon claims this run right now — likely left behind by a killed process"
991 )
992 );
993 }
994 Liveness::Unknown => {
995 let overrun = if state.active_all_overrun(now) {
1001 " — every active seat has already run past its own timeout budget"
1002 } else {
1003 ""
1004 };
1005 let _ = writeln!(
1006 s,
1007 " {}",
1008 yellow(&format!(
1009 "whether a process is still driving this run could not be confirmed{overrun}"
1010 ))
1011 );
1012 }
1013 }
1014 for (seat, a) in state.seats_active() {
1015 let retry = if a.attempt > 0 {
1016 format!(" retry {}", a.attempt)
1017 } else {
1018 String::new()
1019 };
1020 let _ = writeln!(
1021 s,
1022 " {:<12} {:<12}{retry} {}s elapsed, {}s left of {}s",
1023 seat,
1024 a.node,
1025 a.elapsed_secs(now),
1026 a.remaining_secs(now),
1027 a.timeout_secs
1028 );
1029 }
1030 for (task, a) in state.tasks_active() {
1031 let retry = if a.attempt > 0 {
1032 format!(" retry {}", a.attempt)
1033 } else {
1034 String::new()
1035 };
1036 let progress = match (a.index, a.total) {
1037 (Some(i), Some(t)) => format!(" ({i}/{t})"),
1038 _ => String::new(),
1039 };
1040 let _ = writeln!(
1041 s,
1042 " {:<12} {:<12}{retry}{progress} {}s elapsed, {}s left of {}s",
1043 task,
1044 a.node,
1045 a.elapsed_secs(now),
1046 a.remaining_secs(now),
1047 a.timeout_secs
1048 );
1049 if let Some(command) = &a.command {
1050 let _ = writeln!(s, " {command}");
1051 }
1052 }
1053 s
1054}
1055
1056pub fn stats(stats: &Stats) -> String {
1058 let t = &stats.totals;
1059 let mut s = String::new();
1060 let _ = writeln!(s, "{}", bold("runs"));
1061 let _ = writeln!(
1062 s,
1063 " {} total - {} merged, {} ready, {} blocked, {} failed ({:.0}% completion)",
1064 t.runs,
1065 t.merged,
1066 t.ready,
1067 t.blocked,
1068 t.failed,
1069 t.completion_rate()
1070 );
1071 if t.tallied > 0 {
1072 let _ = writeln!(
1073 s,
1074 " {} tallied - {} split on first choice ({:.0}%), {} deliberated, \
1075 {} of those changed a mind, {} converged to unanimous",
1076 t.tallied,
1077 t.split,
1078 t.split_rate(),
1079 t.deliberated,
1080 t.minds_changed,
1081 t.converged
1082 );
1083 }
1084
1085 if !stats.agents.is_empty() {
1086 let _ = writeln!(
1087 s,
1088 "\n{}",
1089 bold("implementation (relative, on this workload)")
1090 );
1091 let _ = writeln!(
1092 s,
1093 " {:<14}{:>6}{:>8}{:>8}{:>8}",
1094 "agent", "won", "entered", "rate", "empty"
1095 );
1096 for a in &stats.agents {
1097 let _ = writeln!(
1098 s,
1099 " {:<14}{:>6}{:>8}{:>7.0}%{:>8}",
1100 a.agent,
1101 a.wins,
1102 a.entered,
1103 a.win_rate(),
1104 a.empty
1105 );
1106 }
1107 }
1108
1109 if !stats.reviewers.is_empty() {
1110 let _ = writeln!(s, "\n{}", bold("review"));
1111 let _ = writeln!(
1112 s,
1113 " {:<14}{:>8}{:>10}{:>11}{:>9}{:>9}{:>9}",
1114 "reviewer", "rounds", "submitted", "adopted/rd", "precision", "unique", "timeout"
1115 );
1116 for r in &stats.reviewers {
1117 let _ = writeln!(
1118 s,
1119 " {:<14}{:>8}{:>10}{:>11.2}{:>8.0}%{:>8.0}%{:>8.0}%",
1120 r.agent,
1121 r.rounds,
1122 r.submitted,
1123 r.adopted_per_round(),
1124 r.precision(),
1125 r.unique_rate(),
1126 r.timeout_rate()
1127 );
1128 }
1129 }
1130
1131 if stats.e2e.rounds > 0 || stats.e2e.deferred > 0 {
1132 let _ = writeln!(s, "\n{}", bold("verification"));
1133 let _ = writeln!(
1134 s,
1135 " {} rounds ran e2e, {} failed, {} of those with a clean static \
1136 review ({:.0}% sole detections), {} round(s) deferred it to the fixer",
1137 stats.e2e.rounds,
1138 stats.e2e.failures,
1139 stats.e2e.sole_detections,
1140 stats.e2e.sole_rate(),
1141 stats.e2e.deferred
1142 );
1143 }
1144 s
1145}
1146
1147#[cfg(test)]
1148mod tests {
1149 use super::*;
1150 use crate::config::Config;
1151 use crate::run::{
1152 Candidate, CommandOutcome, FixRecord, MergeOutcome, ReviewRecord, ReviewRound, RunState,
1153 Tally,
1154 };
1155 use std::collections::BTreeMap;
1156 use std::path::PathBuf;
1157 use std::sync::{Mutex, MutexGuard};
1158
1159 static SERIAL: Mutex<()> = Mutex::new(());
1161
1162 fn plain() -> MutexGuard<'static, ()> {
1163 let guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
1164 set_color(false);
1165 guard
1166 }
1167
1168 fn state() -> RunState {
1169 crate::run::set_home(std::env::temp_dir().join("magi-report-test-home"));
1174 let mut s = RunState::new(
1175 PathBuf::from("/repo"),
1176 "main".to_owned(),
1177 "abcdef1234".to_owned(),
1178 "add retries to the uploader".to_owned(),
1179 Config::default(),
1180 );
1181 s.candidates = vec![Candidate {
1182 index: 0,
1183 label: 'A',
1184 agent: "opus".to_owned(),
1185 branch: "magi/x/A".to_owned(),
1186 worktree: PathBuf::from("/wt/A"),
1187 summary: String::new(),
1188 stat: String::new(),
1189 files: 3,
1190 commits: 2,
1191 empty: false,
1192 failed: None,
1193 verified_noop: None,
1194 duration_ms: 42_000,
1195 folded: false,
1196 }];
1197 s.tally = Some(Tally {
1198 first_choice: BTreeMap::from([('A', 3)]),
1199 borda: BTreeMap::new(),
1200 winner: 'A',
1201 rankings: 3,
1202 unanimous_initial: true,
1203 deliberated: false,
1204 changed_votes: 0,
1205 unanimous_final: true,
1206 tie_break: None,
1207 judges: 3,
1208 present: 3,
1209 quorum: 2,
1210 met_quorum: true,
1211 uncontested: None,
1212 });
1213 s
1214 }
1215
1216 #[test]
1217 fn run_report_names_the_winner_and_its_author() {
1218 let _guard = plain();
1219 let text = run(&state());
1220 assert!(text.contains("<- winner"), "{text}");
1221 assert!(text.contains("opus"));
1222 assert!(text.contains("3 files, 2 commits"));
1223 assert!(text.contains("winner A"));
1224 assert!(!text.contains('\x1b'), "colour leaked into a plain render");
1225 }
1226
1227 #[test]
1228 fn a_candidates_own_summary_is_surfaced_not_only_kept_in_run_json() {
1229 let _guard = plain();
1235 let mut s = state();
1236 s.candidates[0].summary =
1237 "investigated 6c5e/8df3: both already merged, see talk 07fe.\nmore detail below."
1238 .to_owned();
1239 let text = run(&s);
1240 assert!(
1241 text.contains("investigated 6c5e/8df3: both already merged, see talk 07fe."),
1242 "{text}"
1243 );
1244 }
1245
1246 #[test]
1247 fn a_verified_noop_run_does_not_read_as_a_failure() {
1248 let _guard = plain();
1252 let mut s = state();
1253 s.status = RunStatus::VerifiedNoop;
1254 s.tally = None;
1255 s.candidates = vec![Candidate {
1256 index: 0,
1257 label: 'A',
1258 agent: "opus".to_owned(),
1259 branch: "magi/x/A".to_owned(),
1260 worktree: PathBuf::from("/wt/A"),
1261 summary: String::new(),
1262 stat: String::new(),
1263 files: 0,
1264 commits: 0,
1265 empty: true,
1266 failed: None,
1267 verified_noop: Some("already fixed by b32cfc4, which is on main".to_owned()),
1268 duration_ms: 9_000,
1269 folded: false,
1270 }];
1271 let text = run(&s);
1272 assert!(
1273 text.contains("agent-verified no-op"),
1274 "the status and the candidate flag must both say so: {text}"
1275 );
1276 assert!(
1277 text.contains("already fixed by b32cfc4"),
1278 "the evidence itself must be readable, not just the verdict: {text}"
1279 );
1280 assert!(
1281 !text.to_lowercase().contains("failed"),
1282 "a verified no-op must never read as the failure it is not: {text}"
1283 );
1284 }
1285
1286 #[test]
1287 fn colour_is_emitted_only_when_enabled() {
1288 let _guard = plain();
1289 set_color(true);
1290 let coloured = run(&state());
1291 set_color(false);
1292 let plain = run(&state());
1293 assert!(coloured.contains('\x1b'));
1294 assert!(!plain.contains('\x1b'));
1295 assert!(coloured.len() > plain.len());
1296 }
1297
1298 #[test]
1299 fn list_line_is_single_line() {
1300 let _guard = plain();
1301 let l = line(&state());
1302 assert_eq!(l.lines().count(), 1);
1303 assert!(l.contains("add retries"));
1304 assert!(l.contains("win A (opus)"));
1305 }
1306
1307 #[test]
1308 fn an_uncontested_run_does_not_read_as_a_collapsed_panel() {
1309 let _guard = plain();
1310 let mut s = state();
1311 s.tally = Some(Tally {
1312 first_choice: BTreeMap::from([('A', 0)]),
1313 borda: BTreeMap::new(),
1314 winner: 'A',
1315 rankings: 0,
1316 unanimous_initial: false,
1317 deliberated: false,
1318 changed_votes: 0,
1319 unanimous_final: false,
1320 tie_break: None,
1321 judges: 0,
1322 present: 0,
1323 quorum: 0,
1324 met_quorum: true,
1325 uncontested: Some(
1326 "only candidate A produced a usable change; no panel was asked".to_owned(),
1327 ),
1328 });
1329 let text = run(&s);
1330 assert!(
1331 !text.contains("0/3"),
1332 "no panel sat, so the judges line must not read as one that collapsed: {text}"
1333 );
1334 assert!(!text.contains("no usable ranking"), "{text}");
1335 assert!(!text.contains("still split"), "{text}");
1336 assert!(!text.contains("BELOW QUORUM"), "{text}");
1337 assert!(
1338 text.contains("not needed"),
1339 "the report must say judging was skipped, not silent: {text}"
1340 );
1341 assert!(text.contains("winner A"));
1342 }
1343
1344 #[test]
1345 fn a_below_quorum_run_still_reads_as_a_collapsed_panel() {
1346 let _guard = plain();
1347 let mut s = state();
1348 s.tally = Some(Tally {
1349 first_choice: BTreeMap::from([('A', 1), ('B', 0)]),
1350 borda: BTreeMap::new(),
1351 winner: 'A',
1352 rankings: 1,
1353 unanimous_initial: false,
1354 deliberated: false,
1355 changed_votes: 0,
1356 unanimous_final: false,
1357 tie_break: None,
1358 judges: 3,
1359 present: 1,
1360 quorum: 2,
1361 met_quorum: false,
1362 uncontested: None,
1363 });
1364 let text = run(&s);
1365 assert!(text.contains("1/3"), "{text}");
1366 assert!(
1367 text.contains("BELOW QUORUM"),
1368 "a real collapse must still be flagged: {text}"
1369 );
1370 assert!(
1371 !text.contains("not needed"),
1372 "a collapsed panel must not be described as one that was never asked: {text}"
1373 );
1374 }
1375
1376 #[test]
1377 fn a_mode_none_merge_does_not_read_as_landed() {
1378 let _guard = plain();
1379 let mut s = state();
1380 s.merge = Some(MergeOutcome {
1381 mode: crate::config::MergeMode::None,
1382 ok: true,
1383 detail: "git -C /repo merge --no-ff magi/x/A".to_owned(),
1384 });
1385 let text = run(&s);
1386 assert!(
1387 !text.contains(" ok"),
1388 "mode none must not be shown as a landed merge: {text}"
1389 );
1390 assert!(text.contains("not landed"), "{text}");
1391 assert!(
1392 text.contains("branch magi/x/A"),
1393 "the report must say what's left behind: {text}"
1394 );
1395 assert!(
1396 text.contains("rebase"),
1397 "the report must point at the hand-landing steps: {text}"
1398 );
1399 assert!(
1400 !text.contains("placeholder subject"),
1401 "the default merge style is `merge`, which never inherits a \
1402 placeholder subject, so the squash caveat must not appear: {text}"
1403 );
1404 }
1405
1406 #[test]
1407 fn a_mode_none_squash_merge_warns_about_the_placeholder_subject() {
1408 let _guard = plain();
1409 let mut s = state();
1410 s.config.merge.style = MergeStyle::Squash;
1411 s.merge = Some(MergeOutcome {
1412 mode: crate::config::MergeMode::None,
1413 ok: true,
1414 detail: "git -C /repo merge --squash magi/x/A && git -C /repo commit -m \"add \
1415 retries\""
1416 .to_owned(),
1417 });
1418 let text = run(&s);
1419 assert!(
1420 text.contains("placeholder subject"),
1421 "a squash-style manual merge must warn about the missing message: {text}"
1422 );
1423 assert!(text.contains("--squash"), "{text}");
1424 }
1425
1426 #[test]
1427 fn a_ready_run_left_by_merge_mode_none_does_not_read_as_a_plain_ready() {
1428 let _guard = plain();
1429 let mut s = state();
1430 s.status = RunStatus::Ready;
1431 s.merge = Some(MergeOutcome {
1432 mode: crate::config::MergeMode::None,
1433 ok: true,
1434 detail: "git -C /repo merge --no-ff magi/x/A".to_owned(),
1435 });
1436
1437 let list = line(&s);
1438 assert!(
1439 !list.contains(" ready "),
1440 "a mode-none run must not read as a plain ready in `magi list`: {list}"
1441 );
1442 assert!(list.contains("no-op by design"), "{list}");
1443
1444 let full = run(&s);
1445 assert!(
1446 !full.contains("magi run") || !full.lines().next().unwrap().contains(" ready"),
1447 "the header line of `magi show` must not say plain ready either: {full}"
1448 );
1449 assert!(full.contains("no-op by design"), "{full}");
1450 }
1451
1452 #[test]
1453 fn an_ordinary_ready_run_still_reads_as_ready() {
1454 let _guard = plain();
1455 let mut s = state();
1456 s.status = RunStatus::Ready;
1457 s.merge = Some(MergeOutcome {
1461 mode: crate::config::MergeMode::Pr,
1462 ok: false,
1463 detail: "https://example.com/pr/1 was closed without merging".to_owned(),
1464 });
1465
1466 let list = line(&s);
1467 assert!(list.contains("ready"), "{list}");
1468 assert!(!list.contains("no-op by design"), "{list}");
1469 }
1470
1471 #[test]
1472 fn the_list_line_does_not_flag_an_uncontested_run_as_short_judges() {
1473 let _guard = plain();
1474 let mut s = state();
1475 s.tally = Some(Tally {
1476 first_choice: BTreeMap::from([('A', 0)]),
1477 borda: BTreeMap::new(),
1478 winner: 'A',
1479 rankings: 0,
1480 unanimous_initial: false,
1481 deliberated: false,
1482 changed_votes: 0,
1483 unanimous_final: false,
1484 tie_break: None,
1485 judges: 0,
1486 present: 0,
1487 quorum: 0,
1488 met_quorum: true,
1489 uncontested: Some("only candidate A produced a usable change".to_owned()),
1490 });
1491 let l = line(&s);
1492 assert!(
1493 !l.contains("judges") && !l.contains("quorum"),
1494 "an uncontested run must not carry the same badge a short panel gets: {l}"
1495 );
1496 }
1497
1498 #[test]
1499 fn long_instructions_are_elided() {
1500 let _guard = plain();
1501 let mut s = state();
1502 s.instruction = "x".repeat(200);
1503 assert!(line(&s).contains('…'));
1504 }
1505
1506 #[test]
1507 fn a_lost_fix_report_reads_differently_from_zero_adoption() {
1508 let _guard = plain();
1509 let mut lost = state();
1510 lost.reviews = vec![ReviewRound {
1511 round: 1,
1512 head: "abc1234".to_owned(),
1513 verified_head: None,
1514 verified_at: None,
1515 reviews: Vec::new(),
1516 e2e: Vec::new(),
1517 verify_retried: false,
1518 e2e_deferred: false,
1519 e2e_defer_reason: None,
1520 fix: Some(FixRecord {
1521 agent: "opus".to_owned(),
1522 addressed: Vec::new(),
1523 rejected: Vec::new(),
1524 notes: String::new(),
1525 committed: true,
1526 failed: Some("timed out".to_owned()),
1527 duration_ms: 0,
1528 continuation: None,
1529 }),
1530 blocking: 3,
1531 answered: 0,
1532 expected: 0,
1533 clean: false,
1534 progressed: false,
1535 vote_split: false,
1536 reconsideration: Vec::new(),
1537 verdict: None,
1538 }];
1539 let text = run(&lost);
1540 assert!(text.contains("adoption report lost (timed out)"), "{text}");
1541 assert!(
1542 !text.contains("0 addressed"),
1543 "a lost report must never read as `0 addressed`: {text}"
1544 );
1545
1546 let mut rejected_all = state();
1547 rejected_all.reviews = vec![ReviewRound {
1548 round: 1,
1549 head: "abc1234".to_owned(),
1550 verified_head: None,
1551 verified_at: None,
1552 reviews: Vec::new(),
1553 e2e: Vec::new(),
1554 verify_retried: false,
1555 e2e_deferred: false,
1556 e2e_defer_reason: None,
1557 fix: Some(FixRecord {
1558 agent: "opus".to_owned(),
1559 addressed: Vec::new(),
1560 rejected: Vec::new(),
1561 notes: String::new(),
1562 committed: true,
1563 failed: None,
1564 duration_ms: 0,
1565 continuation: None,
1566 }),
1567 blocking: 3,
1568 answered: 0,
1569 expected: 0,
1570 clean: false,
1571 progressed: false,
1572 vote_split: false,
1573 reconsideration: Vec::new(),
1574 verdict: None,
1575 }];
1576 let text2 = run(&rejected_all);
1577 assert!(
1578 text2.contains("0 addressed / 0 rejected"),
1579 "a round the fixer actually reported on keeps the count: {text2}"
1580 );
1581 }
1582
1583 #[test]
1584 fn a_split_round_shows_every_seat_vote_and_the_reconsideration() {
1585 use crate::run::ReviewRevoteRecord;
1586 use crate::verdict::ReviewVote;
1587
1588 let _guard = plain();
1589 let mut s = state();
1590 s.reviews = vec![ReviewRound {
1591 round: 1,
1592 head: "abc1234".to_owned(),
1593 verified_head: None,
1594 verified_at: None,
1595 reviews: vec![
1596 ReviewRecord {
1597 attempts: 0,
1598 reviewer: 1,
1599 agent: "alpha".to_owned(),
1600 summary: String::new(),
1601 findings: Vec::new(),
1602 vote: Some(ReviewVote::Approve),
1603 failed: None,
1604 duration_ms: 0,
1605 },
1606 ReviewRecord {
1607 attempts: 0,
1608 reviewer: 2,
1609 agent: "beta".to_owned(),
1610 summary: String::new(),
1611 findings: Vec::new(),
1612 vote: Some(ReviewVote::Reject),
1613 failed: None,
1614 duration_ms: 0,
1615 },
1616 ],
1617 e2e: Vec::new(),
1618 verify_retried: false,
1619 e2e_deferred: false,
1620 e2e_defer_reason: None,
1621 fix: None,
1622 blocking: 0,
1623 answered: 2,
1624 expected: 2,
1625 clean: false,
1626 progressed: false,
1627 vote_split: true,
1628 reconsideration: vec![ReviewRevoteRecord {
1629 reviewer: 2,
1630 agent: "beta".to_owned(),
1631 vote: Some(ReviewVote::ApproveWithFindings),
1632 reason: "the other seat's read holds up".to_owned(),
1633 failed: None,
1634 }],
1635 verdict: Some(ReviewVote::ApproveWithFindings),
1636 }];
1637 let text = run(&s);
1638 assert!(text.contains("review-1 vote"), "{text}");
1639 assert!(text.contains("review-2 vote"), "{text}");
1640 assert!(text.contains("panel split"), "{text}");
1641 assert!(text.contains("reconsideration"), "{text}");
1642 assert!(text.contains("the other seat's read holds up"), "{text}");
1643 }
1644
1645 #[test]
1646 fn an_incomplete_panel_and_a_lost_fix_report_both_stay_on_the_round_line() {
1647 let _guard = plain();
1653 let mut s = state();
1654 s.reviews = vec![ReviewRound {
1655 round: 1,
1656 head: "abc1234".to_owned(),
1657 verified_head: None,
1658 verified_at: None,
1659 reviews: vec![
1660 ReviewRecord {
1661 attempts: 0,
1662 reviewer: 1,
1663 agent: "alpha".to_owned(),
1664 summary: String::new(),
1665 findings: Vec::new(),
1666 vote: None,
1667 failed: None,
1668 duration_ms: 0,
1669 },
1670 ReviewRecord {
1671 attempts: 0,
1672 reviewer: 2,
1673 agent: "beta".to_owned(),
1674 summary: String::new(),
1675 findings: Vec::new(),
1676 vote: None,
1677 failed: Some("agent timed out".to_owned()),
1678 duration_ms: 0,
1679 },
1680 ],
1681 e2e: Vec::new(),
1682 verify_retried: false,
1683 e2e_deferred: false,
1684 e2e_defer_reason: None,
1685 fix: Some(FixRecord {
1686 agent: "opus".to_owned(),
1687 addressed: Vec::new(),
1688 rejected: Vec::new(),
1689 notes: String::new(),
1690 committed: true,
1691 failed: Some("timed out".to_owned()),
1692 duration_ms: 0,
1693 continuation: None,
1694 }),
1695 blocking: 0,
1696 answered: 1,
1697 expected: 2,
1698 clean: false,
1699 progressed: true,
1700 vote_split: false,
1701 reconsideration: Vec::new(),
1702 verdict: None,
1703 }];
1704 let text = run(&s);
1705 assert!(text.contains("incomplete"), "{text}");
1706 assert!(text.contains("1/2 reviewers answered"), "{text}");
1707 assert!(text.contains("review-2: agent timed out"), "{text}");
1708 assert!(text.contains("adoption report lost (timed out)"), "{text}");
1709 assert!(
1710 !text.contains("clean"),
1711 "a round missing half its panel must never render as clean: {text}"
1712 );
1713 }
1714
1715 #[test]
1716 fn a_build_failure_is_not_reported_as_a_test_failure() {
1717 let _guard = plain();
1718 let mut s = state();
1719 s.reviews = vec![ReviewRound {
1720 round: 1,
1721 head: "abc1234".to_owned(),
1722 verified_head: None,
1723 verified_at: None,
1724 reviews: Vec::new(),
1725 e2e: vec![CommandOutcome {
1726 command: "cargo test".to_owned(),
1727 code: Some(1),
1728 output_tail: "LINK : fatal error LNK1104: cannot open file".to_owned(),
1729 duration_ms: 100,
1730 resource_blocked: false,
1731 }],
1732 verify_retried: true,
1733 e2e_deferred: false,
1734 e2e_defer_reason: None,
1735 fix: None,
1736 blocking: 0,
1737 answered: 0,
1738 expected: 0,
1739 clean: false,
1740 progressed: false,
1741 vote_split: false,
1742 reconsideration: Vec::new(),
1743 verdict: None,
1744 }];
1745 let text = run(&s);
1746 assert!(text.contains("could not run"), "{text}");
1747 assert!(text.contains("retried once"), "{text}");
1748 assert!(!text.contains("e2e RED"), "{text}");
1749 }
1750
1751 #[test]
1752 fn a_resource_blocked_e2e_never_reads_as_red_or_as_a_build_failure() {
1753 let _guard = plain();
1754 let mut s = state();
1755 s.reviews = vec![ReviewRound {
1756 round: 1,
1757 head: "abc1234".to_owned(),
1758 verified_head: None,
1759 verified_at: None,
1760 reviews: Vec::new(),
1761 e2e: vec![CommandOutcome {
1762 command: "(waiting for the shared build cache)".to_owned(),
1763 code: None,
1764 output_tail: "held by run x node e2e seat e2e".to_owned(),
1765 duration_ms: 100,
1766 resource_blocked: true,
1767 }],
1768 verify_retried: false,
1769 e2e_deferred: false,
1770 e2e_defer_reason: None,
1771 fix: None,
1772 blocking: 0,
1773 answered: 0,
1774 expected: 0,
1775 clean: false,
1776 progressed: false,
1777 vote_split: false,
1778 reconsideration: Vec::new(),
1779 verdict: None,
1780 }];
1781 let text = run(&s);
1782 assert!(text.contains("shared build cache unavailable"), "{text}");
1783 assert!(!text.contains("e2e RED"), "{text}");
1784 assert!(!text.contains("build/link failure"), "{text}");
1785 }
1786
1787 #[test]
1788 fn a_round_with_more_than_one_e2e_command_names_each_one() {
1789 let _guard = plain();
1794 let mut s = state();
1795 s.reviews = vec![ReviewRound {
1796 round: 1,
1797 head: "abc1234".to_owned(),
1798 verified_head: Some("abc1234".to_owned()),
1799 verified_at: Some(jiff::Timestamp::now()),
1800 reviews: Vec::new(),
1801 e2e: vec![
1802 CommandOutcome {
1803 command: "cargo test --locked --all-targets".to_owned(),
1804 code: Some(0),
1805 output_tail: String::new(),
1806 duration_ms: 0,
1807 resource_blocked: false,
1808 },
1809 CommandOutcome {
1810 command: "cargo make check".to_owned(),
1811 code: Some(1),
1812 output_tail: "clippy: unused import".to_owned(),
1813 duration_ms: 0,
1814 resource_blocked: false,
1815 },
1816 ],
1817 verify_retried: false,
1818 e2e_deferred: false,
1819 e2e_defer_reason: None,
1820 fix: None,
1821 blocking: 0,
1822 answered: 0,
1823 expected: 0,
1824 clean: false,
1825 progressed: false,
1826 vote_split: false,
1827 reconsideration: Vec::new(),
1828 verdict: None,
1829 }];
1830 let text = run(&s);
1831 assert!(text.contains("cargo test --locked --all-targets"), "{text}");
1832 assert!(text.contains("cargo make check"), "{text}");
1833 assert!(text.contains("clippy: unused import"), "{text}");
1834 }
1835
1836 #[test]
1837 fn a_declined_finding_shows_its_reason() {
1838 use crate::verdict::{Finding, Rejection, Severity};
1839
1840 let _guard = plain();
1841 let mut s = state();
1842 s.status = RunStatus::Ready;
1843 s.reviews = vec![ReviewRound {
1844 round: 1,
1845 head: "deadbee".to_owned(),
1846 verified_head: None,
1847 verified_at: None,
1848 reviews: vec![ReviewRecord {
1849 attempts: 0,
1850 reviewer: 1,
1851 agent: "alpha".to_owned(),
1852 summary: String::new(),
1853 findings: vec![Finding {
1854 id: "R1-1-1".to_owned(),
1855 severity: Severity::Major,
1856 file: None,
1857 line: None,
1858 title: "still open".to_owned(),
1859 detail: String::new(),
1860 }],
1861 vote: None,
1862 failed: None,
1863 duration_ms: 0,
1864 }],
1865 e2e: vec![CommandOutcome {
1866 command: "cargo test".to_owned(),
1867 code: Some(0),
1868 output_tail: String::new(),
1869 duration_ms: 0,
1870 resource_blocked: false,
1871 }],
1872 verify_retried: false,
1873 e2e_deferred: false,
1874 e2e_defer_reason: None,
1875 fix: Some(FixRecord {
1876 agent: "alpha".to_owned(),
1877 addressed: Vec::new(),
1878 rejected: vec![Rejection {
1879 id: "R1-1-2".to_owned(),
1880 why: "cannot be triggered from any caller".to_owned(),
1881 }],
1882 notes: String::new(),
1883 committed: true,
1884 failed: None,
1885 duration_ms: 0,
1886 continuation: None,
1887 }),
1888 blocking: 1,
1889 answered: 1,
1890 expected: 1,
1891 clean: false,
1892 progressed: true,
1893 vote_split: false,
1894 reconsideration: Vec::new(),
1895 verdict: None,
1896 }];
1897
1898 let text = run(&s);
1899 assert!(text.contains("R1-1-2"), "{text}");
1900 assert!(text.contains("cannot be triggered"), "{text}");
1901 assert!(text.contains("still open"), "{text}");
1902 assert!(
1903 text.contains("handed off"),
1904 "a mergeable run with an open round must say so: {text}"
1905 );
1906 }
1907
1908 #[test]
1909 fn a_failing_gate_command_shows_its_output() {
1910 let _guard = plain();
1911 let mut s = state();
1912 s.status = RunStatus::Blocked;
1913 s.gate = vec![CommandOutcome {
1914 command: "cargo make check".to_owned(),
1915 code: Some(101),
1916 output_tail: "error[E0308]: mismatched types".to_owned(),
1917 duration_ms: 0,
1918 resource_blocked: false,
1919 }];
1920 s.gate_ran = true;
1921
1922 let text = run(&s);
1923 assert!(text.contains("mismatched types"), "{text}");
1924 }
1925
1926 #[test]
1927 fn a_gate_with_no_commands_configured_shows_a_pass_not_silence() {
1928 let _guard = plain();
1929 let mut s = state();
1930 s.status = RunStatus::Ready;
1931 s.gate_ran = true;
1932 assert!(s.gate.is_empty());
1933
1934 let text = run(&s);
1935 assert!(
1936 text.contains("gate") && text.contains("no gate commands configured"),
1937 "a run gated on nothing must say so, not read as if the gate never ran: {text}"
1938 );
1939 }
1940
1941 #[test]
1942 fn a_gate_that_has_not_run_yet_shows_nothing() {
1943 let _guard = plain();
1944 let s = state();
1945 assert!(!s.gate_ran);
1946 assert!(s.gate.is_empty());
1947
1948 let text = run(&s);
1949 assert!(
1950 !text.contains("no gate commands configured"),
1951 "an unattempted gate must not be shown as a pass: {text}"
1952 );
1953 }
1954
1955 #[test]
1956 fn active_seats_shows_who_has_not_answered_and_how_long_is_left() {
1957 let _guard = plain();
1958 let mut s = state();
1959 s.seat_started("judge", "judge-2", std::time::Duration::from_secs(120), 0);
1960 let text = active_seats(&s, Liveness::Live);
1961 assert!(text.contains("running now"));
1962 assert!(text.contains("judge-2"));
1963 assert!(text.contains("judge"));
1964 assert!(!text.contains("no live daemon"), "{text}");
1965 assert!(!text.contains("could not be confirmed"), "{text}");
1966 }
1967
1968 #[test]
1969 fn active_seats_flags_a_leftover_from_a_dead_process() {
1970 let _guard = plain();
1971 let mut s = state();
1972 s.seat_started("implement", "impl-B", std::time::Duration::from_secs(60), 0);
1973 let text = active_seats(&s, Liveness::Dead);
1974 assert!(
1975 text.contains("no live daemon"),
1976 "a stale entry must not read as running: {text}"
1977 );
1978 }
1979
1980 #[test]
1981 fn list_line_marks_a_nonterminal_run_with_a_dead_driver_stale() {
1982 let _guard = plain();
1983 let mut s = state();
1984 s.status = RunStatus::Reviewing;
1985 let text = line_with_liveness(&s, Liveness::Dead);
1986 assert!(text.contains("STALE"), "{text}");
1987 assert!(text.contains("resume required"), "{text}");
1988 assert!(liveness_notice(&s, Liveness::Dead).contains("STALE"));
1989 }
1990
1991 #[test]
1996 fn active_seats_reports_uncertainty_without_claiming_death() {
1997 let _guard = plain();
1998 let mut s = state();
1999 s.seat_started("review", "review-1", std::time::Duration::from_secs(60), 0);
2000 let text = active_seats(&s, Liveness::Unknown);
2001 assert!(
2002 text.contains("could not be confirmed"),
2003 "an unproven state must read as uncertain, not dead: {text}"
2004 );
2005 assert!(!text.contains("no live daemon"), "{text}");
2006 }
2007
2008 #[test]
2009 fn active_seats_is_empty_when_nothing_is_running() {
2010 let _guard = plain();
2011 assert_eq!(active_seats(&state(), Liveness::Live), "");
2012 }
2013
2014 #[test]
2020 fn active_seats_shows_a_running_verify_task_and_its_command() {
2021 let _guard = plain();
2022 let mut s = state();
2023 s.task_command(
2024 "e2e",
2025 "verify",
2026 0,
2027 "cargo test",
2028 2,
2029 3,
2030 std::time::Duration::from_secs(600),
2031 );
2032 let text = active_seats(&s, Liveness::Live);
2033 assert!(text.contains("e2e"), "{text}");
2034 assert!(text.contains("(2/3)"), "{text}");
2035 assert!(text.contains("cargo test"), "{text}");
2036 }
2037
2038 #[test]
2039 fn no_jobs_section_appears_when_nothing_was_ever_collected() {
2040 let _guard = plain();
2041 assert!(!run(&state()).contains("background jobs"));
2044 }
2045
2046 #[test]
2047 fn a_codex_roster_with_no_completed_jobs_yet_says_so_instead_of_staying_silent() {
2048 let _guard = plain();
2049 let mut s = state();
2050 s.config.agents.push(crate::config::AgentSpec {
2051 id: "codex-one".to_owned(),
2052 kind: crate::config::AgentKind::Codex,
2053 model: None,
2054 command: vec!["codex".to_owned()],
2055 extra_args: Vec::new(),
2056 env: BTreeMap::new(),
2057 prompt_delivery: None,
2058 });
2059 let text = run(&s);
2060 assert!(
2061 text.contains("background jobs"),
2062 "a run that could report this must not read the same as one that never could: \
2063 {text}"
2064 );
2065 assert!(text.contains("no completed command evidence yet"));
2066 }
2067
2068 #[test]
2069 fn recovered_running_and_unreadable_jobs_are_told_apart() {
2070 let _guard = plain();
2071 let mut s = state();
2072 s.jobs = vec![
2073 crate::run::JobRecord {
2074 node: "implement".to_owned(),
2075 round: None,
2076 seat: "impl-A".to_owned(),
2077 id: "item49".to_owned(),
2078 description: "cargo test --test graph_cached_gate".to_owned(),
2079 checked_at: jiff::Timestamp::now(),
2080 status: crate::run::JobStatus::Completed,
2081 exit_code: Some(0),
2082 result_summary: "test result: 2 passed; 0 failed".to_owned(),
2083 source: "codex".to_owned(),
2084 },
2085 crate::run::JobRecord {
2086 node: "fix".to_owned(),
2087 round: None,
2088 seat: "impl-A".to_owned(),
2089 id: "item52".to_owned(),
2090 description: "cargo test --test graph_split".to_owned(),
2091 checked_at: jiff::Timestamp::now(),
2092 status: crate::run::JobStatus::Failed,
2093 exit_code: Some(101),
2094 result_summary: "test result: 1 passed; 1 failed".to_owned(),
2095 source: "codex".to_owned(),
2096 },
2097 crate::run::JobRecord {
2098 node: "fix".to_owned(),
2099 round: None,
2100 seat: "impl-A".to_owned(),
2101 id: "item60".to_owned(),
2102 description: "cargo build".to_owned(),
2103 checked_at: jiff::Timestamp::now(),
2104 status: crate::run::JobStatus::Unknown,
2105 exit_code: None,
2106 result_summary: String::new(),
2107 source: "codex".to_owned(),
2108 },
2109 ];
2110 let text = run(&s);
2111 assert!(text.contains("background jobs"));
2112 assert!(text.contains("item49"));
2113 assert!(text.contains("item52"));
2114 assert!(text.contains("item60"));
2115 assert!(text.contains("completed"));
2119 assert!(text.contains("failed"));
2120 assert!(text.contains("unknown"));
2121 assert!(text.contains("adapter coverage"));
2123 }
2124
2125 #[test]
2126 fn a_jobs_own_round_is_shown_when_known() {
2127 let _guard = plain();
2128 let mut s = state();
2129 s.jobs = vec![crate::run::JobRecord {
2130 node: "review".to_owned(),
2131 round: Some(2),
2132 seat: "review-1".to_owned(),
2133 id: "item9".to_owned(),
2134 description: "cargo test --test graph_cached_gate".to_owned(),
2135 checked_at: jiff::Timestamp::now(),
2136 status: crate::run::JobStatus::Completed,
2137 exit_code: Some(0),
2138 result_summary: "test result: 2 passed; 0 failed".to_owned(),
2139 source: "codex".to_owned(),
2140 }];
2141 let text = run(&s);
2142 assert!(
2143 text.contains("round 2"),
2144 "the round this seat's own command ran in must be visible, distinct from magi's \
2145 own recorded verify: {text}"
2146 );
2147 }
2148
2149 #[test]
2150 fn stats_table_renders_without_runs() {
2151 let _guard = plain();
2152 let text = stats(&Stats::default());
2153 assert!(text.contains("0 total"));
2154 assert!(!text.contains("implementation"));
2155 }
2156}