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,
19};
20use crate::stats::{RepoStats, 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 RunStatus::Superseded => dim(text),
88 RunStatus::AlreadyInBase => dim(text),
91 _ => cyan(text),
92 }
93}
94
95fn vote_tag(vote: ReviewVote) -> String {
99 let text = vote.label();
100 match vote {
101 ReviewVote::Approve => green(text),
102 ReviewVote::ApproveWithFindings => yellow(text),
103 ReviewVote::Reject => red(text),
104 }
105}
106
107pub fn line(state: &RunState) -> String {
109 line_with_liveness(state, Liveness::Unknown)
110}
111
112pub fn line_with_liveness(state: &RunState, live: Liveness) -> String {
115 let winner = state
116 .tally
117 .as_ref()
118 .map_or("-".to_owned(), |t| t.winner.to_string());
119 let agent = state.winner().map_or("-", |c| c.agent.as_str());
120 let quorum = match state.tally.as_ref() {
123 Some(t) if !t.met_quorum => format!(
124 " {}",
125 bold(&red(&format!("quorum {}/{}", t.present, t.judges)))
126 ),
127 Some(t) if t.present > 0 && t.present < t.judges => format!(
128 " {}",
129 yellow(&format!("judges {}/{}", t.present, t.judges))
130 ),
131 _ => String::new(),
132 };
133 let stale = if let Some(startup) = state.startup.as_ref().filter(|s| !s.complete) {
134 format!(
135 " startup incomplete ({}) — inspect retained evidence; no ordinary resume",
136 startup.stage
137 )
138 } else if !state.status.done() && live == Liveness::Dead {
139 format!(
140 " {}",
141 bold(&yellow("STALE — driver exited; resume required"))
142 )
143 } else {
144 String::new()
145 };
146 let origin = state
149 .origin
150 .as_ref()
151 .map_or("origin unknown".to_owned(), crate::run::Origin::label);
152 format!(
153 "{} {:<20} {:>2}c {:>2}j win {} ({}){quorum}{stale} {} {}",
154 dim(&state.id),
155 status_word(state),
156 state.candidates.len(),
157 state.judgements.len(),
158 winner,
159 agent,
160 dim(&format!("[{origin}]")),
161 first_line(&state.instruction)
162 )
163}
164
165pub fn liveness_notice(state: &RunState, live: Liveness) -> String {
169 if state.startup.as_ref().is_some_and(|s| !s.complete) {
170 return "Startup incomplete; inspect startup evidence and retained assets. Ordinary resume is disabled.\n\n".to_owned();
171 }
172 if !state.status.done() && live == Liveness::Dead {
173 format!(
174 "{}\n\n",
175 yellow("STALE — the process driving this run exited; resume it to continue.")
176 )
177 } else {
178 String::new()
179 }
180}
181
182pub fn superseded_notice(later: Option<&str>) -> String {
193 match later {
194 Some(later) => format!(
195 "{}\n\n",
196 yellow(&format!(
197 "SUPERSEDED — a later attempt at this task ({later}) replaced it; this record \
198 is kept but is not the one to act on."
199 ))
200 ),
201 None => String::new(),
202 }
203}
204
205fn first_line(text: &str) -> String {
206 let line = text.lines().next().unwrap_or_default();
207 if line.chars().count() > 68 {
208 format!("{}…", line.chars().take(67).collect::<String>())
209 } else {
210 line.to_owned()
211 }
212}
213
214fn short(commit: &str) -> String {
215 commit.chars().take(7).collect()
216}
217
218fn continuation_note(c: &crate::run::ContinuationRecord) -> String {
223 match c.outcome {
224 ContinuationOutcome::NotNeeded => String::new(),
225 ContinuationOutcome::Resumed => format!(" [resumed x{}]", c.attempts),
226 ContinuationOutcome::Exhausted => format!(" [continuation exhausted x{}]", c.attempts),
227 ContinuationOutcome::QuotaLost => " [continuation: quota]".to_owned(),
228 ContinuationOutcome::NoSession => " [no session to resume]".to_owned(),
229 }
230}
231
232fn jobs_section(state: &RunState) -> String {
253 let mut s = String::new();
254 if state.jobs.is_empty() {
255 if state
260 .config
261 .agents
262 .iter()
263 .any(|a| a.kind == crate::config::AgentKind::Codex)
264 {
265 let _ = writeln!(
266 s,
267 "\n{}",
268 dim(
269 "background jobs: no completed command evidence yet for this run (see \
270 active seats above for what is still mid-turn)"
271 )
272 );
273 }
274 return s;
275 }
276 let _ = writeln!(
277 s,
278 "\n{}",
279 bold("background jobs (from each seat's own CLI)")
280 );
281 let mut by_seat: std::collections::BTreeMap<(&str, &str), Vec<&crate::run::JobRecord>> =
282 std::collections::BTreeMap::new();
283 for j in &state.jobs {
284 by_seat
285 .entry((j.node.as_str(), j.seat.as_str()))
286 .or_default()
287 .push(j);
288 }
289 for ((node, seat), records) in by_seat {
290 let _ = writeln!(s, " {node}/{seat}");
291 for j in records {
292 let status = match j.status {
293 JobStatus::Completed => green("completed"),
294 JobStatus::Failed => red("failed"),
295 JobStatus::Unknown => yellow("unknown"),
296 };
297 let _ = writeln!(
298 s,
299 " {} {}{}{} checked {}",
300 dim(&j.id),
301 status,
302 j.exit_code
303 .map_or(String::new(), |c| format!(" (exit {c})")),
304 j.round.map_or(String::new(), |r| format!(" round {r}")),
305 j.checked_at
306 .to_zoned(jiff::tz::TimeZone::system())
307 .strftime("%Y-%m-%d %H:%M:%S")
308 );
309 let desc = first_line(&j.description);
310 if !desc.trim().is_empty() {
311 let _ = writeln!(s, " $ {desc}");
312 }
313 let summary = first_line(&j.result_summary);
314 if !summary.trim().is_empty() {
315 let _ = writeln!(s, " {}", dim(&summary));
316 }
317 }
318 }
319 let _ = writeln!(
320 s,
321 " {}",
322 dim(
323 "(adapter coverage: codex only today; other backends, and a command a CLI never \
324 reported finishing, leave no entry here — that is unknown, never \"nothing ran\")"
325 )
326 );
327 s
328}
329
330pub fn run(state: &RunState) -> String {
332 let mut s = String::new();
333 let _ = writeln!(
334 s,
335 "{} {} {}",
336 bold("magi run"),
337 bold(&state.id),
338 status_word(state)
339 );
340 let _ = writeln!(
341 s,
342 " repo {} ({} @ {})",
343 state.repo.display(),
344 state.base_branch,
345 short(&state.base_commit)
346 );
347 let _ = writeln!(s, " created {}", state.created_local());
348 let _ = writeln!(s, " task {}", first_line(&state.instruction));
349 let _ = writeln!(
350 s,
351 " origin {}",
352 crate::run::origin_label(state.origin.as_ref())
353 );
354 if let Some(startup) = &state.startup {
355 let _ = writeln!(
356 s,
357 " startup {} branch={} stage={} complete={}",
358 startup.kind,
359 startup.branch.as_deref().unwrap_or("-"),
360 startup.stage,
361 startup.complete
362 );
363 if let Some(failure) = &startup.failure {
364 let _ = writeln!(s, " startup failure {failure}");
365 }
366 }
367 let _ = writeln!(s, " state {}", state.dir().display());
368
369 let _ = writeln!(s, "\n{}", bold("candidates"));
370 for c in &state.candidates {
371 let flag = match (&c.failed, c.empty, &c.verified_noop) {
372 (Some(e), _, _) => red(&format!("failed: {e}")),
373 (None, true, Some(_)) => cyan("agent-verified no-op (unconfirmed)"),
377 (None, true, None) => yellow("no change"),
378 _ => format!("{} files, {} commits", c.files, c.commits),
379 };
380 let crown = if state.tally.as_ref().is_some_and(|t| t.winner == c.label) {
381 bold(&green(" <- winner"))
382 } else {
383 String::new()
384 };
385 let _ = writeln!(
386 s,
387 " {} {:<12} {:<30} {:>5}s{}",
388 bold(&c.label.to_string()),
389 c.agent,
390 flag,
391 c.duration_ms / 1000,
392 crown
393 );
394 if let Some(evidence) = &c.verified_noop {
395 let _ = writeln!(s, " {}", dim(&first_line(evidence)));
396 } else if !c.summary.trim().is_empty() {
397 let _ = writeln!(s, " {}", dim(&first_line(&c.summary)));
406 }
407 }
408
409 if !state.judgements.is_empty() {
410 let _ = writeln!(s, "\n{}", bold("blind judging"));
411 for j in &state.judgements {
412 match &j.failed {
413 Some(e) => {
414 let _ = writeln!(
415 s,
416 " judge {} {}",
417 j.judge,
418 red(&format!("no ranking: {e}"))
419 );
420 }
421 None => {
422 let _ = writeln!(
423 s,
424 " judge {} {:<12} {} confidence {}",
425 j.judge,
426 j.agent,
427 bold(&j.ranking.iter().collect::<String>()),
428 j.confidence.map_or("-".to_owned(), |c| c.to_string())
429 );
430 }
431 }
432 }
433 }
434
435 if let Some(t) = &state.tally {
436 if t.deliberated {
437 let _ = writeln!(s, "\n{}", bold("deliberation"));
438 for round in &state.deliberation {
439 for turn in &round.turns {
440 let _ = writeln!(
441 s,
442 " r{} judge {} -> {}",
443 round.round,
444 turn.judge,
445 turn.tentative.map_or("-".to_owned(), |c| c.to_string())
446 );
447 }
448 }
449 }
450
451 if !state.votes.is_empty() {
452 let _ = writeln!(s, "\n{}", bold("final votes (collected privately)"));
453 for v in &state.votes {
454 let _ = writeln!(
455 s,
456 " judge {} {:<12} {}{}",
457 v.judge,
458 v.agent,
459 bold(&v.vote.unwrap_or('?').to_string()),
460 if v.changed {
461 yellow(" (changed after deliberation)")
462 } else {
463 String::new()
464 }
465 );
466 }
467 }
468
469 let _ = writeln!(s, "\n{}", bold("tally"));
470 match &t.uncontested {
476 Some(reason) => {
477 let _ = writeln!(
478 s,
479 " judging {}",
480 cyan(&format!("not needed — {reason}"))
481 );
482 }
483 None => {
484 let _ = writeln!(
485 s,
486 " judges {} present{}",
487 if t.met_quorum {
488 green(&format!("{}/{}", t.present, t.judges))
489 } else {
490 red(&format!("{}/{}", t.present, t.judges))
491 },
492 if t.quorum > 0 {
493 format!(" ({quorum} required)", quorum = t.quorum)
494 } else {
495 String::new()
496 }
497 );
498 if !t.met_quorum {
499 let _ = writeln!(
500 s,
501 " {}",
502 bold(&red("BELOW QUORUM — verdict is not trustworthy"))
503 );
504 }
505 let _ = writeln!(
506 s,
507 " first choice {}",
508 t.first_choice
509 .iter()
510 .map(|(k, v)| format!("{k}:{v}"))
511 .collect::<Vec<_>>()
512 .join(" ")
513 );
514 let _ = writeln!(
515 s,
516 " initial {}",
517 match (t.rankings, t.unanimous_initial) {
518 (0, _) => red("no usable ranking"),
519 (1, _) => yellow("one usable ranking - not a consensus"),
520 (_, true) => green("unanimous"),
521 (_, false) => yellow("split"),
522 }
523 );
524 let _ = writeln!(
525 s,
526 " after votes {} ({} judge(s) moved)",
527 if t.unanimous_final {
528 green("unanimous")
529 } else {
530 yellow("still split")
531 },
532 t.changed_votes
533 );
534 if let Some(tb) = &t.tie_break {
535 let _ = writeln!(s, " tie break {tb}");
536 }
537 }
538 }
539 if !state.quota.is_empty() {
540 let _ = writeln!(
541 s,
542 " unavailable {}",
543 state
544 .quota
545 .iter()
546 .map(|q| if q.cause == crate::agent::QuotaCause::Authentication {
547 format!("{} ({}: re-authenticate)", q.seat, q.agent)
548 } else {
549 format!("{} (usage limit)", q.seat)
550 })
551 .collect::<Vec<_>>()
552 .join(", ")
553 );
554 }
555 let _ = writeln!(s, " winner {}", bold(&green(&t.winner.to_string())));
556 }
557
558 if !state.handovers.is_empty() {
559 let _ = writeln!(s, "\n{}", bold("seats handed over"));
560 for h in &state.handovers {
561 let _ = writeln!(
562 s,
563 " {} {} {} -> {} ({}: {})",
564 yellow("~"),
565 h.seat,
566 h.from,
567 h.to,
568 h.node,
569 h.reason
570 );
571 }
572 }
573
574 if !state.withheld.is_empty() {
575 let _ = writeln!(s, "\n{}", bold("withheld from commit"));
576 for w in &state.withheld {
577 let _ = writeln!(
578 s,
579 " {} {} ({} lockfile; the directory uses {})",
580 yellow("!"),
581 w.path,
582 w.manager,
583 w.kept_by
584 );
585 }
586 }
587
588 if let Some(r) = &state.recovery {
589 let _ = writeln!(s, "\n{}", bold("recovery"));
590 let _ = writeln!(s, " {}", r.summary());
591 if r.criteria_inherited {
592 let _ = writeln!(s, " the task's own completion criteria were kept");
593 }
594 match &r.repair {
595 crate::recovery::Repair::NotStarted => {}
596 crate::recovery::Repair::WaitingForRestore { .. } => {
597 let _ = writeln!(s, " repair: waiting to restore and retry");
598 }
599 crate::recovery::Repair::Started { .. } => {
600 let _ = writeln!(s, " repair: {}", yellow("unfinished"));
601 }
602 crate::recovery::Repair::Done {
603 committed, failed, ..
604 } => {
605 let _ = match failed {
606 Some(why) => writeln!(s, " repair: {} {why}", yellow("nothing kept:")),
607 None if *committed => writeln!(s, " repair: {}", green("committed")),
608 None => writeln!(s, " repair: no commit"),
609 };
610 }
611 }
612 }
613
614 if let Some(g) = &state.graft {
615 let _ = writeln!(s, "\n{}", bold("graft"));
616 let size = g.lines.map_or_else(
617 || " changed lines unavailable".to_owned(),
618 |n| format!(" {n} changed lines"),
619 );
620 let _ = match &g.outcome {
621 crate::run::GraftOutcome::Applied => writeln!(s, " {}{size}", green("applied")),
622 crate::run::GraftOutcome::WaitingForRestore => {
623 writeln!(s, " {}{size}", yellow("waiting to restore and retry"))
624 }
625 crate::run::GraftOutcome::Running => writeln!(s, " {}{size}", yellow("unfinished")),
626 crate::run::GraftOutcome::RolledBack(why) => {
627 writeln!(s, " {} {why}{size}", yellow("rolled back:"))
628 }
629 };
630 if let Some(a) = &g.agent {
631 let chain: Vec<String> = state
632 .handovers
633 .iter()
634 .filter(|h| h.node == "graft")
635 .map(|h| format!("{} -> {}", h.from, h.to))
636 .collect();
637 let via = if chain.is_empty() {
638 String::new()
639 } else {
640 format!(" (handovers: {})", chain.join(", "))
641 };
642 let _ = writeln!(s, " by {a}{via}");
643 }
644 if !g.requested.is_empty() {
645 let view = crate::report_view::build(state, Liveness::Unknown);
646 if let Some(crate::report_view::Section::Graft { graft, .. }) = view
647 .sections
648 .iter()
649 .find(|x| matches!(x, crate::report_view::Section::Graft { .. }))
650 {
651 let _ = writeln!(
652 s,
653 " requested {} confirmed adopted {} rejected {} unconfirmed {}",
654 graft.requested.len(),
655 graft.adopted_count,
656 graft.rejected_count,
657 graft.unconfirmed_count
658 );
659 }
660 }
661 if let Some(n) = g
662 .adoption
663 .as_ref()
664 .and_then(|a| a.notes.lines().find(|l| !l.trim().is_empty()))
665 {
666 let _ = writeln!(s, " fixer notes: {}", n.trim());
667 }
668 }
669
670 if !state.reviews.is_empty() {
671 let _ = writeln!(s, "\n{}", bold("review + verification"));
672 for r in &state.reviews {
673 let raised: usize = r.reviews.iter().map(|x| x.findings.len()).sum();
674 let e2e = match r.e2e_status() {
682 E2eStatus::NotConfigured => dim("no e2e"),
683 E2eStatus::Deferred => yellow(&format!(
684 "e2e deferred{}",
685 r.e2e_defer_reason
686 .as_deref()
687 .map(|why| format!(" ({why})"))
688 .unwrap_or_default()
689 )),
690 E2eStatus::Passed => green("e2e green"),
691 E2eStatus::Failed if r.e2e.iter().any(CommandOutcome::build_failed) => {
692 yellow("e2e could not run (build/link failure)")
693 }
694 E2eStatus::Failed => red("e2e RED"),
695 E2eStatus::ResourceBlocked => yellow("e2e could not run (resource unavailable)"),
700 };
701 let e2e = if r.verify_retried {
702 format!("{e2e}, retried once")
703 } else {
704 e2e
705 };
706 let status = if r.incomplete() {
710 yellow("incomplete")
711 } else if r.clean {
712 green("clean")
713 } else {
714 yellow("open")
715 };
716 let panel = if r.incomplete() {
720 let missing: Vec<String> = r
721 .reviews
722 .iter()
723 .filter_map(|x| {
724 x.failed
725 .as_ref()
726 .map(|why| format!("review-{}: {why}", x.reviewer))
727 })
728 .collect();
729 format!(
730 " {}/{} reviewers answered ({})",
731 r.answered,
732 r.expected,
733 missing.join(", ")
734 )
735 } else {
736 String::new()
737 };
738 let verdict = r.verdict.map_or(String::new(), |v| {
744 format!(
745 ", verdict {}{}",
746 vote_tag(v),
747 if r.vote_split { " (panel split)" } else { "" }
748 )
749 });
750 let _ = writeln!(
751 s,
752 " round {} {} @ {}{}{panel} {raised} finding(s), {} blocking, {e2e}{verdict}{}",
753 r.round,
754 status,
755 short(&r.head),
756 r.verified_head.as_ref().map_or(String::new(), |head| {
757 format!(
758 " (verified @ {}{})",
759 short(head),
760 r.verified_at.map_or(String::new(), |t| format!(
761 " on {}",
762 t.to_zoned(jiff::tz::TimeZone::system())
763 .strftime("%Y-%m-%d %H:%M:%S")
764 ))
765 )
766 }),
767 r.blocking,
768 r.fix.as_ref().map_or(String::new(), |f| {
769 let tree = if r.progressed {
770 green("changed")
771 } else {
772 yellow("unchanged")
773 };
774 let cont = f
775 .continuation
776 .as_ref()
777 .map_or(String::new(), continuation_note);
778 match &f.failed {
779 Some(reason) => format!(
784 " fix: {}, tree {tree}{}{cont}",
785 yellow(&format!("adoption report lost ({reason})")),
786 if f.committed {
787 String::new()
788 } else {
789 red(" (NO COMMIT)")
790 }
791 ),
792 None => format!(
793 " fix: {} addressed / {} rejected, tree {tree}{}{cont}",
794 f.addressed.len(),
795 f.rejected.len(),
796 if f.committed {
797 String::new()
798 } else {
799 red(" (NO COMMIT)")
800 }
801 ),
802 }
803 })
804 );
805 for o in &r.e2e {
812 let label = if o.resource_blocked {
813 yellow("blocked")
814 } else if o.ok() {
815 green("pass")
816 } else {
817 red("FAIL")
818 };
819 let _ = writeln!(s, " {label} {}", o.command);
820 if !o.ok() {
821 let _ = writeln!(
822 s,
823 "{}",
824 dim(&crate::run::summarize_verify(&o.output_tail, 2_000))
825 );
826 }
827 }
828 for rec in &r.reviews {
829 if let Some(vote) = rec.vote {
830 let _ = writeln!(s, " review-{} vote {}", rec.reviewer, vote_tag(vote));
831 }
832 for f in &rec.findings {
833 let adopted = r
834 .fix
835 .as_ref()
836 .is_some_and(|fix| fix.addressed.contains(&f.id));
837 let _ = writeln!(
838 s,
839 " {} [{:?}] {}{}",
840 dim(&f.id),
841 f.severity,
842 f.title,
843 if adopted {
844 green(" fixed")
845 } else {
846 String::new()
847 }
848 );
849 }
850 }
851 if let Some(fix) = &r.fix {
852 for rej in &fix.rejected {
853 let _ = writeln!(
854 s,
855 " {} {}: {}",
856 dim(&rej.id),
857 yellow("declined"),
858 rej.why
859 );
860 }
861 }
862 if !r.reconsideration.is_empty() {
866 let _ = writeln!(s, " {}", dim("reconsideration:"));
867 for rv in &r.reconsideration {
868 match rv.vote {
869 Some(v) => {
870 let _ = writeln!(
871 s,
872 " review-{} -> {} {}",
873 rv.reviewer,
874 vote_tag(v),
875 rv.reason
876 );
877 }
878 None => {
879 let _ = writeln!(
880 s,
881 " review-{} -> {}",
882 rv.reviewer,
883 red(&format!(
884 "no revote ({})",
885 rv.failed.as_deref().unwrap_or("unknown")
886 ))
887 );
888 }
889 }
890 }
891 }
892 }
893 if state.handed_off_with_open_findings() {
894 let _ = writeln!(
895 s,
896 "\n {}",
897 yellow(&format!(
898 "handed off with {} finding(s) still open — gate and e2e were green; \
899 see above for what a person should still look at",
900 state.open_findings().len()
901 ))
902 );
903 }
904 }
905
906 if !state.merge_repairs.is_empty() {
907 let _ = writeln!(s, "\n{}", bold("merge-gate repairs"));
908 for r in &state.merge_repairs {
909 let _ = writeln!(
910 s,
911 " question {} head {} {:?}\n {}",
912 r.question,
913 crate::run::short_of(&r.head),
914 r.phase,
915 r.pr
916 );
917 if let Some(why) = &r.failure {
918 let _ = writeln!(s, " held: {why}");
919 }
920 if let Some(round) = r.reviews.last() {
921 for f in round.reviews.iter().flat_map(|rec| &rec.findings) {
922 let _ = writeln!(s, " original [{}] {:?}: {}", f.id, f.severity, f.title);
923 }
924 for (seat, _, vote) in round.final_votes() {
925 let _ = writeln!(s, " original reviewer #{seat}: {:?}", vote);
926 }
927 }
928 }
929 s.push_str(
930 " Repair never approves a merge; updated results require a fresh owner decision.\n",
931 );
932 }
933
934 if !state.operator_fixes.is_empty() {
935 let _ = writeln!(s, "\n{}", bold("operator fix(es)"));
936 for (i, req) in state.operator_fixes.iter().enumerate() {
937 let _ = writeln!(
938 s,
939 " [{}] {} finding(s) at {}{}",
940 i + 1,
941 req.findings.len(),
942 req.requested_at
943 .to_zoned(jiff::tz::TimeZone::system())
944 .strftime("%Y-%m-%d %H:%M:%S"),
945 if req.stale {
946 yellow(" stale head, --allow-stale used")
947 } else {
948 String::new()
949 }
950 );
951 let _ = writeln!(s, " reason: {}", req.reason);
952 for f in &req.findings {
953 let outcome = match &f.outcome {
954 OperatorFixOutcome::Pending => yellow("pending"),
955 OperatorFixOutcome::Addressed => green("addressed"),
956 OperatorFixOutcome::Rejected { why } => red(&format!("rejected: {why}")),
957 OperatorFixOutcome::Unreported => {
958 red("unreported — no adoption report came back")
959 }
960 };
961 let _ = writeln!(
962 s,
963 " {} [{:?}] {} {outcome}",
964 dim(&f.id),
965 f.severity,
966 f.title
967 );
968 }
969 match &req.follow_up_review_run {
970 Some(id) => {
971 let _ = writeln!(s, " re-verified by run {id}");
972 }
973 None if req.fix.as_ref().is_some_and(|fx| fx.committed) => {
974 let _ = writeln!(
975 s,
976 " {}",
977 red("committed, but the follow-up review could not be opened")
978 );
979 }
980 None => {
981 let _ = writeln!(s, " no change committed; nothing to re-verify");
982 }
983 }
984 }
985 }
986
987 if let Some(bs) = &state.base_sync {
988 let _ = writeln!(s, "\n{}", bold("base sync"));
989 let status = if let Some(a) = &bs.already_in {
990 dim(&format!(
991 "already in {} as {} ({} match) - nothing left to land",
992 state.base_branch,
993 a.names(),
994 a.proof.as_str()
995 ))
996 } else if let Some(c) = &bs.conflict {
997 red(&format!("conflict: {}", first_line(c)))
998 } else if bs.behind == 0 {
999 green("in sync")
1000 } else {
1001 yellow(&format!("{} commit(s) behind, not yet rebased", bs.behind))
1002 };
1003 let _ = writeln!(
1004 s,
1005 " {} @ {} {status}{}",
1006 state.base_branch,
1007 short(&bs.tip),
1008 if bs.attempts > 0 {
1009 format!(" ({} rebase attempt(s))", bs.attempts)
1010 } else {
1011 String::new()
1012 }
1013 );
1014 }
1015
1016 if !state.pre_gate.is_empty() {
1017 let _ = writeln!(s, "\n{}", bold("pre_gate"));
1018 for o in &state.pre_gate {
1019 let _ = writeln!(
1020 s,
1021 " {} {}",
1022 if o.ok() {
1023 green("pass")
1024 } else {
1025 yellow("warn")
1026 },
1027 o.command
1028 );
1029 if !o.ok() {
1030 let _ = writeln!(
1031 s,
1032 "{}",
1033 dim(&crate::run::summarize_verify(&o.output_tail, 2_000))
1034 );
1035 }
1036 }
1037 if let Some(c) = &state.pre_gate_commit {
1038 let _ = writeln!(s, " committed mechanical fixes @ {}", short(c));
1039 }
1040 }
1041
1042 match state.gate_status() {
1046 GateStatus::NotRun => {}
1047 GateStatus::NotConfigured => {
1048 let _ = writeln!(s, "\n{}", bold("gate"));
1049 let _ = writeln!(s, " {} no gate commands configured", dim("not tested"));
1050 }
1051 GateStatus::Passed | GateStatus::Failed => {
1052 let _ = writeln!(s, "\n{}", bold("gate"));
1053 for o in &state.gate {
1054 let _ = writeln!(
1055 s,
1056 " {} {}",
1057 if o.ok() { green("pass") } else { red("FAIL") },
1058 o.command
1059 );
1060 if !o.ok() {
1061 let _ = writeln!(
1062 s,
1063 "{}",
1064 dim(&crate::run::summarize_verify(&o.output_tail, 2_000))
1065 );
1066 }
1067 }
1068 }
1069 }
1070
1071 if let Some(m) = &state.merge {
1072 let _ = writeln!(s, "\n{}", bold("merge"));
1073 if m.mode == MergeMode::None {
1074 let _ = writeln!(
1078 s,
1079 " mode None {}",
1080 cyan("not landed — nothing to do by design")
1081 );
1082 if let Some(w) = state.winner() {
1083 let _ = writeln!(
1084 s,
1085 " branch {} still exists, unmerged into {}",
1086 w.branch, state.base_branch
1087 );
1088 }
1089 let _ = writeln!(
1095 s,
1096 " rebase onto {} before merging by hand{}",
1097 state.base_branch,
1098 if state.config.merge.style == MergeStyle::Squash {
1099 ", and pass an explicit commit message — a squash merge \
1100 otherwise inherits the candidate's placeholder subject"
1101 } else {
1102 ""
1103 }
1104 );
1105 let _ = writeln!(s, " {}", m.detail.lines().next().unwrap_or(""));
1106 } else {
1107 let _ = writeln!(
1108 s,
1109 " mode {:?} {}\n {}",
1110 m.mode,
1111 if m.empty {
1112 yellow("empty candidate — nothing to land")
1113 } else if m.ok && state.status == RunStatus::Merged {
1114 green("merged")
1115 } else if m.ok && m.mode == MergeMode::Pr {
1116 yellow("published; not merged")
1117 } else {
1118 yellow("not merged")
1119 },
1120 m.detail.lines().next().unwrap_or("")
1121 );
1122 }
1123 if let Some(pr) = &state.pr
1124 && !pr.red_at_merge.is_empty()
1125 {
1126 let _ = writeln!(
1127 s,
1128 " {} {}",
1129 yellow("merged with red checks:"),
1130 pr.red_at_merge.join(", ")
1131 );
1132 }
1133 }
1134
1135 if !state.followups.is_empty() {
1136 let _ = writeln!(s, "\n{}", bold("follow-ups"));
1137 for f in &state.followups {
1138 let _ = writeln!(
1139 s,
1140 " {} {}",
1141 crate::queue::short(&f.task),
1142 f.findings.join(", ")
1143 );
1144 }
1145 }
1146
1147 if !state.bump_decision_attempts.is_empty() {
1148 let _ = writeln!(s, "\n{}", bold("release decision attempts"));
1149 for (n, attempt) in state.bump_decision_attempts.iter().enumerate() {
1150 let _ = writeln!(s, " {}. {}: {}", n + 1, attempt.agent, attempt.outcome);
1151 }
1152 }
1153 if let Some(b) = &state.release_bump {
1154 let _ = writeln!(s, "\n{}", bold("release bump"));
1155 if let Some(why) = &b.skipped {
1156 let _ = writeln!(
1157 s,
1158 " {} {why}",
1159 bold(&yellow("SKIPPED (no release was made):"))
1160 );
1161 }
1162 if let Some(v) = &b.version {
1163 let _ = writeln!(s, " version v{v}");
1164 }
1165 if let Some(url) = &b.pr_url {
1166 let _ = writeln!(s, " pr {url}");
1167 }
1168 if b.skipped.is_none() || b.pr_url.is_some() {
1169 let _ = writeln!(
1170 s,
1171 " automerge {}",
1172 if b.automerge_enabled {
1173 green("enabled")
1174 } else if b.merged_directly {
1175 green("not needed (CI was already green; merged directly)")
1176 } else if b.local {
1177 dim(
1178 "not used (release.mode = local: merged on your approval, released by magi serve)",
1179 )
1180 } else if b.problem.is_some() {
1181 bold(&red("FAILED"))
1182 } else {
1183 dim("not enabled")
1184 }
1185 );
1186 }
1187 if let Some(p) = &b.problem {
1188 let _ = writeln!(s, " {}", p.lines().next().unwrap_or(""));
1189 }
1190 if let Some(j) = &b.release {
1191 let state = if j.finished {
1192 "finished"
1193 } else {
1194 j.failed.as_deref().unwrap_or("unfinished")
1195 };
1196 let _ = writeln!(s, " release {} at {}: {state}", j.tag(), j.commit);
1197 for l in &j.log {
1198 let code = l.code.map_or("none".to_owned(), |c| c.to_string());
1199 let _ = write!(s, " {} (exit {code})", l.name);
1200 if let Some(o) = &l.output {
1201 let _ = write!(s, " output {o}");
1202 }
1203 let _ = writeln!(s);
1204 }
1205 }
1206 if let Some(a) = &b.action_required {
1207 let _ = writeln!(s, " {} {a}", bold(&yellow("action required:")));
1208 }
1209 }
1210
1211 if !state.leaks.is_empty() {
1212 let _ = writeln!(s, "\n{}", bold(&yellow("blindness warnings")));
1213 for l in &state.leaks {
1214 let _ = writeln!(s, " {} x{} in {}", l.token, l.count, l.site);
1215 }
1216 }
1217
1218 if let Some(w) = state.winner()
1219 && !w.folded
1220 {
1221 let _ = writeln!(
1222 s,
1223 "\n{} {}\n branch {}",
1224 bold("winner worktree"),
1225 w.worktree.display(),
1226 w.branch
1227 );
1228 }
1229 s.push_str(&jobs_section(state));
1230 s
1231}
1232
1233pub fn active_seats(state: &RunState, live: Liveness) -> String {
1249 let mut s = String::new();
1250 if let Some(block) = &state.resource_block {
1251 let _ = writeln!(
1252 s,
1253 "\n{}\n {}. Resume to retry the affected operation.",
1254 bold("waiting on a resource"),
1255 block.description()
1256 );
1257 }
1258 if state.active.is_empty() {
1259 return s;
1260 }
1261 let _ = writeln!(s, "\n{}", bold("running now"));
1262 let now = jiff::Timestamp::now();
1263 match live {
1264 Liveness::Live => {}
1265 Liveness::Dead => {
1266 let _ = writeln!(
1267 s,
1268 " {}",
1269 yellow(
1270 "no live daemon claims this run right now — likely left behind by a killed process"
1271 )
1272 );
1273 }
1274 Liveness::Unknown => {
1275 let overrun = if state.active_all_overrun(now) {
1281 " — every active seat has already run past its own timeout budget"
1282 } else {
1283 ""
1284 };
1285 let _ = writeln!(
1286 s,
1287 " {}",
1288 yellow(&format!(
1289 "whether a process is still driving this run could not be confirmed{overrun}"
1290 ))
1291 );
1292 }
1293 }
1294 for (seat, a) in state.seats_active() {
1295 let retry = if a.attempt > 0 {
1296 format!(" retry {}", a.attempt)
1297 } else {
1298 String::new()
1299 };
1300 if let Some(w) = state.resource_wait.as_ref().filter(|w| w.node == a.node) {
1302 let _ = writeln!(
1303 s,
1304 " {:<12} {:<12}{retry} waiting for the {} ({}), {}s of {}s; agent not started yet",
1305 seat,
1306 a.node,
1307 w.kind,
1308 w.holder,
1309 (now.as_second() - w.since.as_second()).max(0),
1310 w.limit_secs
1311 );
1312 continue;
1313 }
1314 let _ = writeln!(
1315 s,
1316 " {:<12} {:<12}{retry} {}s elapsed, {}s left of {}s",
1317 seat,
1318 a.node,
1319 a.elapsed_secs(now),
1320 a.remaining_secs(now),
1321 a.timeout_secs
1322 );
1323 }
1324 for (task, a) in state.tasks_active() {
1325 let retry = if a.attempt > 0 {
1326 format!(" retry {}", a.attempt)
1327 } else {
1328 String::new()
1329 };
1330 let progress = match (a.index, a.total) {
1331 (Some(i), Some(t)) => format!(" ({i}/{t})"),
1332 _ => String::new(),
1333 };
1334 let _ = writeln!(
1335 s,
1336 " {:<12} {:<12}{retry}{progress} {}s elapsed, {}s left of {}s",
1337 task,
1338 a.node,
1339 a.elapsed_secs(now),
1340 a.remaining_secs(now),
1341 a.timeout_secs
1342 );
1343 if let Some(command) = &a.command {
1344 let _ = writeln!(s, " {command}");
1345 }
1346 }
1347 s
1348}
1349
1350fn fmt_secs(secs: i64) -> String {
1352 let secs = secs.max(0);
1353 let h = secs / 3600;
1354 let m = (secs % 3600) / 60;
1355 let s = secs % 60;
1356 if h > 0 {
1357 format!("{h}h{m:02}m")
1358 } else if m > 0 {
1359 format!("{m}m{s:02}s")
1360 } else {
1361 format!("{s}s")
1362 }
1363}
1364
1365fn bar(value: f64, max: f64, width: usize) -> String {
1370 if max <= 0.0 || value <= 0.0 {
1371 return String::new();
1372 }
1373 let cells = ((value / max) * width as f64)
1374 .round()
1375 .clamp(1.0, width as f64) as usize;
1376 "█".repeat(cells)
1377}
1378
1379pub fn repo_summary(repos: &[RepoStats]) -> String {
1385 let mut s = String::new();
1386 let _ = writeln!(s, "{}", bold("repos"));
1387
1388 let mut name_counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
1392 for r in repos {
1393 *name_counts.entry(r.name.as_str()).or_insert(0) += 1;
1394 }
1395
1396 for r in repos {
1397 let label = if name_counts.get(r.name.as_str()).copied().unwrap_or(0) > 1 {
1398 match r.repo.parent().and_then(|p| p.file_name()) {
1399 Some(parent) => format!("{} ({})", r.name, parent.to_string_lossy()),
1400 None => r.name.clone(),
1401 }
1402 } else {
1403 r.name.clone()
1404 };
1405 let _ = writeln!(
1406 s,
1407 " {} {} runs {:.0}% completion",
1408 label,
1409 r.stats.totals.runs,
1410 r.stats.totals.completion_rate()
1411 );
1412 }
1413 s
1414}
1415
1416pub fn stats(stats: &Stats) -> String {
1418 let t = &stats.totals;
1419 let mut s = String::new();
1420 let _ = writeln!(s, "{}", bold("runs"));
1421 let _ = writeln!(
1422 s,
1423 " {} total - {} merged, {} ready, {} blocked, {} failed ({:.0}% completion)",
1424 t.runs,
1425 t.merged,
1426 t.ready,
1427 t.blocked,
1428 t.failed,
1429 t.completion_rate()
1430 );
1431 if t.tallied > 0 {
1432 let _ = writeln!(
1433 s,
1434 " {} tallied - {} split on first choice ({:.0}%), {} deliberated, \
1435 {} of those changed a mind, {} converged to unanimous",
1436 t.tallied,
1437 t.split,
1438 t.split_rate(),
1439 t.deliberated,
1440 t.minds_changed,
1441 t.converged
1442 );
1443 }
1444
1445 if !stats.retired_hidden.is_empty() {
1446 let n = stats.retired_hidden.len();
1447 let _ = writeln!(
1448 s,
1449 "{n} retired agent{} hidden: {} (magi stats --all to show)\n",
1450 if n == 1 { "" } else { "s" },
1451 stats.retired_hidden.join(", ")
1452 );
1453 }
1454
1455 let has_competitive = stats.agents.iter().any(|a| a.competitive_entered > 0);
1456 if has_competitive {
1457 let mut comp_agents = stats.agents.clone();
1458 comp_agents.retain(|a| a.competitive_entered > 0);
1459 comp_agents.sort_by(|a, b| {
1460 let a_tier = if a.competitive_entered < 10 { 1 } else { 0 };
1461 let b_tier = if b.competitive_entered < 10 { 1 } else { 0 };
1462 a_tier
1463 .cmp(&b_tier)
1464 .then(
1465 b.competitive_win_rate()
1466 .total_cmp(&a.competitive_win_rate()),
1467 )
1468 .then(b.competitive_entered.cmp(&a.competitive_entered))
1469 .then(a.agent.cmp(&b.agent))
1470 });
1471
1472 let _ = writeln!(
1473 s,
1474 "\n{}",
1475 bold("implementation (competitive runs, 2+ candidates)")
1476 );
1477 let _ = writeln!(
1478 s,
1479 " {:<14}{:>6}{:>8}{:>8}{:>8}",
1480 "agent", "won", "entered", "rate", "empty"
1481 );
1482 for a in &comp_agents {
1483 let low_n_marker = if a.competitive_entered < 10 {
1484 format!(" {}", dim("low n"))
1485 } else {
1486 String::new()
1487 };
1488 let _ = writeln!(
1489 s,
1490 " {:<14}{:>6}{:>8}{:>7.0}%{:>8}{}",
1491 a.agent,
1492 a.competitive_wins,
1493 a.competitive_entered,
1494 a.competitive_win_rate(),
1495 a.empty,
1496 low_n_marker
1497 );
1498 }
1499 }
1500
1501 let has_solo = stats.agents.iter().any(|a| a.solo_entered > 0);
1502 if has_solo {
1503 let mut solo_agents = stats.agents.clone();
1504 solo_agents.retain(|a| a.solo_entered > 0);
1505 solo_agents.sort_by(|a, b| {
1506 let a_tier = if a.solo_entered < 10 { 1 } else { 0 };
1507 let b_tier = if b.solo_entered < 10 { 1 } else { 0 };
1508 a_tier
1509 .cmp(&b_tier)
1510 .then(b.solo_adoption_rate().total_cmp(&a.solo_adoption_rate()))
1511 .then(b.solo_entered.cmp(&a.solo_entered))
1512 .then(a.agent.cmp(&b.agent))
1513 });
1514
1515 let _ = writeln!(
1516 s,
1517 "\n{}",
1518 bold("implementation (solo runs, completion/adoption rate)")
1519 );
1520 let _ = writeln!(
1521 s,
1522 " {:<14}{:>8}{:>8}{:>8}",
1523 "agent", "adopted", "entered", "rate"
1524 );
1525 for a in &solo_agents {
1526 let low_n_marker = if a.solo_entered < 10 {
1527 format!(" {}", dim("low n"))
1528 } else {
1529 String::new()
1530 };
1531 let _ = writeln!(
1532 s,
1533 " {:<14}{:>8}{:>8}{:>7.0}%{}",
1534 a.agent,
1535 a.solo_adopted,
1536 a.solo_entered,
1537 a.solo_adoption_rate(),
1538 low_n_marker
1539 );
1540 }
1541 }
1542
1543 if !stats.reviewers.is_empty() {
1544 let _ = writeln!(s, "\n{}", bold("review"));
1545 let _ = writeln!(
1546 s,
1547 " {:<14}{:>8}{:>10}{:>11}{:>9}{:>9}{:>9}",
1548 "reviewer", "rounds", "submitted", "adopted/rd", "precision", "unique", "timeout"
1549 );
1550 for r in &stats.reviewers {
1551 let _ = writeln!(
1552 s,
1553 " {:<14}{:>8}{:>10}{:>11.2}{:>8.0}%{:>8.0}%{:>8.0}%",
1554 r.agent,
1555 r.rounds,
1556 r.submitted,
1557 r.adopted_per_round(),
1558 r.precision(),
1559 r.unique_rate(),
1560 r.timeout_rate()
1561 );
1562 }
1563 }
1564
1565 if !stats.advisors.is_empty() {
1566 let _ = writeln!(
1567 s,
1568 "\n{} {}",
1569 bold("design deliberation"),
1570 dim(
1571 "(approximate: reflection is read off word overlap with the synthesis brief, not a precise attribution)"
1572 )
1573 );
1574 let _ = writeln!(
1575 s,
1576 " {:<14}{:>7}{:>10}{:>8}{:>8}{:>9}{:>10}",
1577 "advisor", "seats", "proposed", "strong", "faint", "absent", "reflect%"
1578 );
1579 for a in &stats.advisors {
1580 let _ = writeln!(
1581 s,
1582 " {:<14}{:>7}{:>10}{:>8}{:>8}{:>9}{:>9.0}%",
1583 a.agent,
1584 a.seated,
1585 a.proposed,
1586 a.strong,
1587 a.faint,
1588 a.absent,
1589 a.reflection_rate()
1590 );
1591 }
1592 }
1593
1594 if stats.release_bumps.merged > 0 {
1595 let b = &stats.release_bumps;
1596 let _ = writeln!(s, "\n{}", bold("release bumps"));
1597 let _ = writeln!(
1598 s,
1599 " {} of {} merged run(s) recorded a bump ({:.0}%)",
1600 b.recorded,
1601 b.merged,
1602 b.coverage_rate()
1603 );
1604 let _ = writeln!(
1605 s,
1606 " {} release PR(s) opened, {} automerge enabled ({:.0}%), {} merged directly",
1607 b.pr_opened,
1608 b.automerge_enabled,
1609 b.automerge_rate(),
1610 b.merged_directly
1611 );
1612 let _ = writeln!(
1613 s,
1614 " {} clean, {} needed a human ({:.0}%)",
1615 b.clean(),
1616 b.needs_attention,
1617 b.attention_rate()
1618 );
1619 }
1620
1621 if stats.e2e.rounds > 0 || stats.e2e.deferred > 0 {
1622 let _ = writeln!(s, "\n{}", bold("verification"));
1623 let _ = writeln!(
1624 s,
1625 " {} rounds ran e2e, {} failed, {} of those with a clean static \
1626 review ({:.0}% sole detections), {} round(s) deferred it to the fixer",
1627 stats.e2e.rounds,
1628 stats.e2e.failures,
1629 stats.e2e.sole_detections,
1630 stats.e2e.sole_rate(),
1631 stats.e2e.deferred
1632 );
1633 }
1634
1635 if !stats.nodes.is_empty() {
1636 let _ = writeln!(
1637 s,
1638 "\n{}",
1639 bold("node durations (first to last event, per run)")
1640 );
1641 let max_mean = stats
1642 .nodes
1643 .iter()
1644 .map(|n| n.mean_secs())
1645 .fold(0.0_f64, f64::max);
1646 for n in &stats.nodes {
1647 let unmeasured = if n.single > 0 {
1648 format!(" {}", dim(&format!("{} unmeasured", n.single)))
1649 } else {
1650 String::new()
1651 };
1652 let _ = writeln!(
1653 s,
1654 " {:<14}{:>6} runs{:>8} mean{:>8} max {}{unmeasured}",
1655 n.node,
1656 n.runs,
1657 fmt_secs(n.mean_secs().round() as i64),
1658 fmt_secs(n.max_secs),
1659 bar(n.mean_secs(), max_mean, 20)
1660 );
1661 }
1662 }
1663 s
1664}
1665
1666#[cfg(test)]
1667mod tests {
1668 use super::*;
1669 use crate::config::Config;
1670 use crate::run::{
1671 Candidate, CommandOutcome, FixRecord, MergeOutcome, ReviewRecord, ReviewRound, RunState,
1672 Tally,
1673 };
1674 use std::collections::BTreeMap;
1675 use std::path::PathBuf;
1676 use std::sync::{Mutex, MutexGuard};
1677
1678 static SERIAL: Mutex<()> = Mutex::new(());
1680
1681 fn plain() -> MutexGuard<'static, ()> {
1682 let guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
1683 set_color(false);
1684 guard
1685 }
1686
1687 fn state() -> RunState {
1688 crate::run::pin_test_home();
1693 let mut s = RunState::new(
1694 PathBuf::from("/repo"),
1695 "main".to_owned(),
1696 "abcdef1234".to_owned(),
1697 "add retries to the uploader".to_owned(),
1698 Config::default(),
1699 );
1700 s.candidates = vec![Candidate {
1701 index: 0,
1702 label: 'A',
1703 agent: "opus".to_owned(),
1704 branch: "magi/x/A".to_owned(),
1705 worktree: PathBuf::from("/wt/A"),
1706 summary: String::new(),
1707 stat: String::new(),
1708 files: 3,
1709 commits: 2,
1710 empty: false,
1711 failed: None,
1712 verified_noop: None,
1713 duration_ms: 42_000,
1714 folded: false,
1715 }];
1716 s.tally = Some(Tally {
1717 first_choice: BTreeMap::from([('A', 3)]),
1718 borda: BTreeMap::new(),
1719 winner: 'A',
1720 rankings: 3,
1721 unanimous_initial: true,
1722 deliberated: false,
1723 changed_votes: 0,
1724 unanimous_final: true,
1725 tie_break: None,
1726 judges: 3,
1727 present: 3,
1728 quorum: 2,
1729 met_quorum: true,
1730 uncontested: None,
1731 });
1732 s
1733 }
1734
1735 #[test]
1736 fn run_report_names_the_winner_and_its_author() {
1737 let _guard = plain();
1738 let text = run(&state());
1739 assert!(text.contains("<- winner"), "{text}");
1740 assert!(text.contains("opus"));
1741 assert!(text.contains("3 files, 2 commits"));
1742 assert!(text.contains("winner A"));
1743 assert!(!text.contains('\x1b'), "colour leaked into a plain render");
1744 }
1745
1746 #[test]
1747 fn a_candidates_own_summary_is_surfaced_not_only_kept_in_run_json() {
1748 let _guard = plain();
1754 let mut s = state();
1755 s.candidates[0].summary =
1756 "investigated 6c5e/8df3: both already merged, see talk 07fe.\nmore detail below."
1757 .to_owned();
1758 let text = run(&s);
1759 assert!(
1760 text.contains("investigated 6c5e/8df3: both already merged, see talk 07fe."),
1761 "{text}"
1762 );
1763 }
1764
1765 #[test]
1766 fn a_verified_noop_run_does_not_read_as_a_failure() {
1767 let _guard = plain();
1771 let mut s = state();
1772 s.status = RunStatus::VerifiedNoop;
1773 s.tally = None;
1774 s.candidates = vec![Candidate {
1775 index: 0,
1776 label: 'A',
1777 agent: "opus".to_owned(),
1778 branch: "magi/x/A".to_owned(),
1779 worktree: PathBuf::from("/wt/A"),
1780 summary: String::new(),
1781 stat: String::new(),
1782 files: 0,
1783 commits: 0,
1784 empty: true,
1785 failed: None,
1786 verified_noop: Some("already fixed by b32cfc4, which is on main".to_owned()),
1787 duration_ms: 9_000,
1788 folded: false,
1789 }];
1790 let text = run(&s);
1791 assert!(
1792 text.contains("agent-verified no-op"),
1793 "the status and the candidate flag must both say so: {text}"
1794 );
1795 assert!(
1796 text.contains("already fixed by b32cfc4"),
1797 "the evidence itself must be readable, not just the verdict: {text}"
1798 );
1799 assert!(
1800 !text.to_lowercase().contains("failed"),
1801 "a verified no-op must never read as the failure it is not: {text}"
1802 );
1803 }
1804
1805 #[test]
1806 fn colour_is_emitted_only_when_enabled() {
1807 let _guard = plain();
1808 set_color(true);
1809 let coloured = run(&state());
1810 set_color(false);
1811 let plain = run(&state());
1812 assert!(coloured.contains('\x1b'));
1813 assert!(!plain.contains('\x1b'));
1814 assert!(coloured.len() > plain.len());
1815 }
1816
1817 #[test]
1818 fn list_line_is_single_line() {
1819 let _guard = plain();
1820 let l = line(&state());
1821 assert_eq!(l.lines().count(), 1);
1822 assert!(l.contains("add retries"));
1823 assert!(l.contains("win A (opus)"));
1824 }
1825
1826 #[test]
1827 fn an_uncontested_run_does_not_read_as_a_collapsed_panel() {
1828 let _guard = plain();
1829 let mut s = state();
1830 s.tally = Some(Tally {
1831 first_choice: BTreeMap::from([('A', 0)]),
1832 borda: BTreeMap::new(),
1833 winner: 'A',
1834 rankings: 0,
1835 unanimous_initial: false,
1836 deliberated: false,
1837 changed_votes: 0,
1838 unanimous_final: false,
1839 tie_break: None,
1840 judges: 0,
1841 present: 0,
1842 quorum: 0,
1843 met_quorum: true,
1844 uncontested: Some(
1845 "only candidate A produced a usable change; no panel was asked".to_owned(),
1846 ),
1847 });
1848 let text = run(&s);
1849 assert!(
1850 !text.contains("0/3"),
1851 "no panel sat, so the judges line must not read as one that collapsed: {text}"
1852 );
1853 assert!(!text.contains("no usable ranking"), "{text}");
1854 assert!(!text.contains("still split"), "{text}");
1855 assert!(!text.contains("BELOW QUORUM"), "{text}");
1856 assert!(
1857 text.contains("not needed"),
1858 "the report must say judging was skipped, not silent: {text}"
1859 );
1860 assert!(text.contains("winner A"));
1861 }
1862
1863 #[test]
1864 fn a_below_quorum_run_still_reads_as_a_collapsed_panel() {
1865 let _guard = plain();
1866 let mut s = state();
1867 s.tally = Some(Tally {
1868 first_choice: BTreeMap::from([('A', 1), ('B', 0)]),
1869 borda: BTreeMap::new(),
1870 winner: 'A',
1871 rankings: 1,
1872 unanimous_initial: false,
1873 deliberated: false,
1874 changed_votes: 0,
1875 unanimous_final: false,
1876 tie_break: None,
1877 judges: 3,
1878 present: 1,
1879 quorum: 2,
1880 met_quorum: false,
1881 uncontested: None,
1882 });
1883 let text = run(&s);
1884 assert!(text.contains("1/3"), "{text}");
1885 assert!(
1886 text.contains("BELOW QUORUM"),
1887 "a real collapse must still be flagged: {text}"
1888 );
1889 assert!(
1890 !text.contains("not needed"),
1891 "a collapsed panel must not be described as one that was never asked: {text}"
1892 );
1893 }
1894
1895 #[test]
1896 fn a_mode_none_merge_does_not_read_as_landed() {
1897 let _guard = plain();
1898 let mut s = state();
1899 s.merge = Some(MergeOutcome {
1900 mode: crate::config::MergeMode::None,
1901 ok: true,
1902 detail: "git -C /repo merge --no-ff magi/x/A".to_owned(),
1903 empty: false,
1904 });
1905 let text = run(&s);
1906 assert!(
1907 !text.contains(" ok"),
1908 "mode none must not be shown as a landed merge: {text}"
1909 );
1910 assert!(text.contains("not landed"), "{text}");
1911 assert!(
1912 text.contains("branch magi/x/A"),
1913 "the report must say what's left behind: {text}"
1914 );
1915 assert!(
1916 text.contains("rebase"),
1917 "the report must point at the hand-landing steps: {text}"
1918 );
1919 assert!(
1920 !text.contains("placeholder subject"),
1921 "the default merge style is `merge`, which never inherits a \
1922 placeholder subject, so the squash caveat must not appear: {text}"
1923 );
1924 }
1925
1926 #[test]
1927 fn a_mode_none_squash_merge_warns_about_the_placeholder_subject() {
1928 let _guard = plain();
1929 let mut s = state();
1930 s.config.merge.style = MergeStyle::Squash;
1931 s.merge = Some(MergeOutcome {
1932 mode: crate::config::MergeMode::None,
1933 ok: true,
1934 detail: "git -C /repo merge --squash magi/x/A && git -C /repo commit -m \"add \
1935 retries\""
1936 .to_owned(),
1937 empty: false,
1938 });
1939 let text = run(&s);
1940 assert!(
1941 text.contains("placeholder subject"),
1942 "a squash-style manual merge must warn about the missing message: {text}"
1943 );
1944 assert!(text.contains("--squash"), "{text}");
1945 }
1946
1947 #[test]
1948 fn a_ready_run_left_by_merge_mode_none_does_not_read_as_a_plain_ready() {
1949 let _guard = plain();
1950 let mut s = state();
1951 s.status = RunStatus::Ready;
1952 s.merge = Some(MergeOutcome {
1953 mode: crate::config::MergeMode::None,
1954 ok: true,
1955 detail: "git -C /repo merge --no-ff magi/x/A".to_owned(),
1956 empty: false,
1957 });
1958
1959 let list = line(&s);
1960 assert!(
1961 !list.contains(" ready "),
1962 "a mode-none run must not read as a plain ready in `magi list`: {list}"
1963 );
1964 assert!(list.contains("no-op by design"), "{list}");
1965
1966 let full = run(&s);
1967 assert!(
1968 !full.contains("magi run") || !full.lines().next().unwrap().contains(" ready"),
1969 "the header line of `magi show` must not say plain ready either: {full}"
1970 );
1971 assert!(full.contains("no-op by design"), "{full}");
1972 }
1973
1974 #[test]
1975 fn an_ordinary_ready_run_still_reads_as_ready() {
1976 let _guard = plain();
1977 let mut s = state();
1978 s.status = RunStatus::Ready;
1979 s.merge = Some(MergeOutcome {
1983 mode: crate::config::MergeMode::Pr,
1984 ok: false,
1985 detail: "https://example.com/pr/1 was closed without merging".to_owned(),
1986 empty: false,
1987 });
1988
1989 let list = line(&s);
1990 assert!(list.contains("ready"), "{list}");
1991 assert!(!list.contains("no-op by design"), "{list}");
1992 }
1993
1994 #[test]
1995 fn the_list_line_does_not_flag_an_uncontested_run_as_short_judges() {
1996 let _guard = plain();
1997 let mut s = state();
1998 s.tally = Some(Tally {
1999 first_choice: BTreeMap::from([('A', 0)]),
2000 borda: BTreeMap::new(),
2001 winner: 'A',
2002 rankings: 0,
2003 unanimous_initial: false,
2004 deliberated: false,
2005 changed_votes: 0,
2006 unanimous_final: false,
2007 tie_break: None,
2008 judges: 0,
2009 present: 0,
2010 quorum: 0,
2011 met_quorum: true,
2012 uncontested: Some("only candidate A produced a usable change".to_owned()),
2013 });
2014 let l = line(&s);
2015 assert!(
2016 !l.contains("judges") && !l.contains("quorum"),
2017 "an uncontested run must not carry the same badge a short panel gets: {l}"
2018 );
2019 }
2020
2021 #[test]
2022 fn long_instructions_are_elided() {
2023 let _guard = plain();
2024 let mut s = state();
2025 s.instruction = "x".repeat(200);
2026 assert!(line(&s).contains('…'));
2027 }
2028
2029 #[test]
2030 fn a_lost_fix_report_reads_differently_from_zero_adoption() {
2031 let _guard = plain();
2032 let mut lost = state();
2033 lost.reviews = vec![ReviewRound {
2034 round: 1,
2035 head: "abc1234".to_owned(),
2036 verified_head: None,
2037 verified_at: None,
2038 reviews: Vec::new(),
2039 e2e: Vec::new(),
2040 verify_retried: false,
2041 e2e_deferred: false,
2042 e2e_defer_reason: None,
2043 fix: Some(FixRecord {
2044 agent: "opus".to_owned(),
2045 addressed: Vec::new(),
2046 rejected: Vec::new(),
2047 notes: String::new(),
2048 committed: true,
2049 failed: Some("timed out".to_owned()),
2050 duration_ms: 0,
2051 continuation: None,
2052 }),
2053 blocking: 3,
2054 answered: 0,
2055 expected: 0,
2056 clean: false,
2057 progressed: false,
2058 vote_split: false,
2059 reconsideration: Vec::new(),
2060 verdict: None,
2061 }];
2062 let text = run(&lost);
2063 assert!(text.contains("adoption report lost (timed out)"), "{text}");
2064 assert!(
2065 !text.contains("0 addressed"),
2066 "a lost report must never read as `0 addressed`: {text}"
2067 );
2068
2069 let mut rejected_all = state();
2070 rejected_all.reviews = vec![ReviewRound {
2071 round: 1,
2072 head: "abc1234".to_owned(),
2073 verified_head: None,
2074 verified_at: None,
2075 reviews: Vec::new(),
2076 e2e: Vec::new(),
2077 verify_retried: false,
2078 e2e_deferred: false,
2079 e2e_defer_reason: None,
2080 fix: Some(FixRecord {
2081 agent: "opus".to_owned(),
2082 addressed: Vec::new(),
2083 rejected: Vec::new(),
2084 notes: String::new(),
2085 committed: true,
2086 failed: None,
2087 duration_ms: 0,
2088 continuation: None,
2089 }),
2090 blocking: 3,
2091 answered: 0,
2092 expected: 0,
2093 clean: false,
2094 progressed: false,
2095 vote_split: false,
2096 reconsideration: Vec::new(),
2097 verdict: None,
2098 }];
2099 let text2 = run(&rejected_all);
2100 assert!(
2101 text2.contains("0 addressed / 0 rejected"),
2102 "a round the fixer actually reported on keeps the count: {text2}"
2103 );
2104 }
2105
2106 #[test]
2107 fn a_split_round_shows_every_seat_vote_and_the_reconsideration() {
2108 use crate::run::ReviewRevoteRecord;
2109 use crate::verdict::ReviewVote;
2110
2111 let _guard = plain();
2112 let mut s = state();
2113 s.reviews = vec![ReviewRound {
2114 round: 1,
2115 head: "abc1234".to_owned(),
2116 verified_head: None,
2117 verified_at: None,
2118 reviews: vec![
2119 ReviewRecord {
2120 attempts: 0,
2121 reviewer: 1,
2122 agent: "alpha".to_owned(),
2123 summary: String::new(),
2124 findings: Vec::new(),
2125 vote: Some(ReviewVote::Approve),
2126 failed: None,
2127 duration_ms: 0,
2128 },
2129 ReviewRecord {
2130 attempts: 0,
2131 reviewer: 2,
2132 agent: "beta".to_owned(),
2133 summary: String::new(),
2134 findings: Vec::new(),
2135 vote: Some(ReviewVote::Reject),
2136 failed: None,
2137 duration_ms: 0,
2138 },
2139 ],
2140 e2e: Vec::new(),
2141 verify_retried: false,
2142 e2e_deferred: false,
2143 e2e_defer_reason: None,
2144 fix: None,
2145 blocking: 0,
2146 answered: 2,
2147 expected: 2,
2148 clean: false,
2149 progressed: false,
2150 vote_split: true,
2151 reconsideration: vec![ReviewRevoteRecord {
2152 reviewer: 2,
2153 agent: "beta".to_owned(),
2154 vote: Some(ReviewVote::ApproveWithFindings),
2155 reason: "the other seat's read holds up".to_owned(),
2156 failed: None,
2157 }],
2158 verdict: Some(ReviewVote::ApproveWithFindings),
2159 }];
2160 let text = run(&s);
2161 assert!(text.contains("review-1 vote"), "{text}");
2162 assert!(text.contains("review-2 vote"), "{text}");
2163 assert!(text.contains("panel split"), "{text}");
2164 assert!(text.contains("reconsideration"), "{text}");
2165 assert!(text.contains("the other seat's read holds up"), "{text}");
2166 }
2167
2168 #[test]
2169 fn an_incomplete_panel_and_a_lost_fix_report_both_stay_on_the_round_line() {
2170 let _guard = plain();
2176 let mut s = state();
2177 s.reviews = vec![ReviewRound {
2178 round: 1,
2179 head: "abc1234".to_owned(),
2180 verified_head: None,
2181 verified_at: None,
2182 reviews: vec![
2183 ReviewRecord {
2184 attempts: 0,
2185 reviewer: 1,
2186 agent: "alpha".to_owned(),
2187 summary: String::new(),
2188 findings: Vec::new(),
2189 vote: None,
2190 failed: None,
2191 duration_ms: 0,
2192 },
2193 ReviewRecord {
2194 attempts: 0,
2195 reviewer: 2,
2196 agent: "beta".to_owned(),
2197 summary: String::new(),
2198 findings: Vec::new(),
2199 vote: None,
2200 failed: Some("agent timed out".to_owned()),
2201 duration_ms: 0,
2202 },
2203 ],
2204 e2e: Vec::new(),
2205 verify_retried: false,
2206 e2e_deferred: false,
2207 e2e_defer_reason: None,
2208 fix: Some(FixRecord {
2209 agent: "opus".to_owned(),
2210 addressed: Vec::new(),
2211 rejected: Vec::new(),
2212 notes: String::new(),
2213 committed: true,
2214 failed: Some("timed out".to_owned()),
2215 duration_ms: 0,
2216 continuation: None,
2217 }),
2218 blocking: 0,
2219 answered: 1,
2220 expected: 2,
2221 clean: false,
2222 progressed: true,
2223 vote_split: false,
2224 reconsideration: Vec::new(),
2225 verdict: None,
2226 }];
2227 let text = run(&s);
2228 assert!(text.contains("incomplete"), "{text}");
2229 assert!(text.contains("1/2 reviewers answered"), "{text}");
2230 assert!(text.contains("review-2: agent timed out"), "{text}");
2231 assert!(text.contains("adoption report lost (timed out)"), "{text}");
2232 assert!(
2233 !text.contains("clean"),
2234 "a round missing half its panel must never render as clean: {text}"
2235 );
2236 }
2237
2238 #[test]
2239 fn a_build_failure_is_not_reported_as_a_test_failure() {
2240 let _guard = plain();
2241 let mut s = state();
2242 s.reviews = vec![ReviewRound {
2243 round: 1,
2244 head: "abc1234".to_owned(),
2245 verified_head: None,
2246 verified_at: None,
2247 reviews: Vec::new(),
2248 e2e: vec![CommandOutcome {
2249 command: "cargo test".to_owned(),
2250 code: Some(1),
2251 output_tail: "LINK : fatal error LNK1104: cannot open file".to_owned(),
2252 duration_ms: 100,
2253 resource_blocked: false,
2254 }],
2255 verify_retried: true,
2256 e2e_deferred: false,
2257 e2e_defer_reason: None,
2258 fix: None,
2259 blocking: 0,
2260 answered: 0,
2261 expected: 0,
2262 clean: false,
2263 progressed: false,
2264 vote_split: false,
2265 reconsideration: Vec::new(),
2266 verdict: None,
2267 }];
2268 let text = run(&s);
2269 assert!(text.contains("could not run"), "{text}");
2270 assert!(text.contains("retried once"), "{text}");
2271 assert!(!text.contains("e2e RED"), "{text}");
2272 }
2273
2274 #[test]
2275 fn a_resource_blocked_e2e_never_reads_as_red_or_as_a_build_failure() {
2276 let _guard = plain();
2277 let mut s = state();
2278 s.reviews = vec![ReviewRound {
2279 round: 1,
2280 head: "abc1234".to_owned(),
2281 verified_head: None,
2282 verified_at: None,
2283 reviews: Vec::new(),
2284 e2e: vec![CommandOutcome {
2285 command: "(waiting for the shared build cache)".to_owned(),
2286 code: None,
2287 output_tail: "held by run x node e2e seat e2e".to_owned(),
2288 duration_ms: 100,
2289 resource_blocked: true,
2290 }],
2291 verify_retried: false,
2292 e2e_deferred: false,
2293 e2e_defer_reason: None,
2294 fix: None,
2295 blocking: 0,
2296 answered: 0,
2297 expected: 0,
2298 clean: false,
2299 progressed: false,
2300 vote_split: false,
2301 reconsideration: Vec::new(),
2302 verdict: None,
2303 }];
2304 let text = run(&s);
2305 assert!(text.contains("resource unavailable"), "{text}");
2306 assert!(!text.contains("e2e RED"), "{text}");
2307 assert!(!text.contains("build/link failure"), "{text}");
2308 }
2309
2310 #[test]
2311 fn a_round_with_more_than_one_e2e_command_names_each_one() {
2312 let _guard = plain();
2317 let mut s = state();
2318 s.reviews = vec![ReviewRound {
2319 round: 1,
2320 head: "abc1234".to_owned(),
2321 verified_head: Some("abc1234".to_owned()),
2322 verified_at: Some(jiff::Timestamp::now()),
2323 reviews: Vec::new(),
2324 e2e: vec![
2325 CommandOutcome {
2326 command: "cargo test --locked --all-targets".to_owned(),
2327 code: Some(0),
2328 output_tail: String::new(),
2329 duration_ms: 0,
2330 resource_blocked: false,
2331 },
2332 CommandOutcome {
2333 command: "cargo make check".to_owned(),
2334 code: Some(1),
2335 output_tail: "clippy: unused import".to_owned(),
2336 duration_ms: 0,
2337 resource_blocked: false,
2338 },
2339 ],
2340 verify_retried: false,
2341 e2e_deferred: false,
2342 e2e_defer_reason: None,
2343 fix: None,
2344 blocking: 0,
2345 answered: 0,
2346 expected: 0,
2347 clean: false,
2348 progressed: false,
2349 vote_split: false,
2350 reconsideration: Vec::new(),
2351 verdict: None,
2352 }];
2353 let text = run(&s);
2354 assert!(text.contains("cargo test --locked --all-targets"), "{text}");
2355 assert!(text.contains("cargo make check"), "{text}");
2356 assert!(text.contains("clippy: unused import"), "{text}");
2357 }
2358
2359 #[test]
2360 fn a_declined_finding_shows_its_reason() {
2361 use crate::verdict::{Finding, Rejection, Severity};
2362
2363 let _guard = plain();
2364 let mut s = state();
2365 s.status = RunStatus::Ready;
2366 s.reviews = vec![ReviewRound {
2367 round: 1,
2368 head: "deadbee".to_owned(),
2369 verified_head: None,
2370 verified_at: None,
2371 reviews: vec![ReviewRecord {
2372 attempts: 0,
2373 reviewer: 1,
2374 agent: "alpha".to_owned(),
2375 summary: String::new(),
2376 findings: vec![Finding {
2377 id: "R1-1-1".to_owned(),
2378 severity: Severity::Major,
2379 file: None,
2380 line: None,
2381 title: "still open".to_owned(),
2382 detail: String::new(),
2383 }],
2384 vote: None,
2385 failed: None,
2386 duration_ms: 0,
2387 }],
2388 e2e: vec![CommandOutcome {
2389 command: "cargo test".to_owned(),
2390 code: Some(0),
2391 output_tail: String::new(),
2392 duration_ms: 0,
2393 resource_blocked: false,
2394 }],
2395 verify_retried: false,
2396 e2e_deferred: false,
2397 e2e_defer_reason: None,
2398 fix: Some(FixRecord {
2399 agent: "alpha".to_owned(),
2400 addressed: Vec::new(),
2401 rejected: vec![Rejection {
2402 id: "R1-1-2".to_owned(),
2403 why: "cannot be triggered from any caller".to_owned(),
2404 }],
2405 notes: String::new(),
2406 committed: true,
2407 failed: None,
2408 duration_ms: 0,
2409 continuation: None,
2410 }),
2411 blocking: 1,
2412 answered: 1,
2413 expected: 1,
2414 clean: false,
2415 progressed: true,
2416 vote_split: false,
2417 reconsideration: Vec::new(),
2418 verdict: None,
2419 }];
2420
2421 let text = run(&s);
2422 assert!(text.contains("R1-1-2"), "{text}");
2423 assert!(text.contains("cannot be triggered"), "{text}");
2424 assert!(text.contains("still open"), "{text}");
2425 assert!(
2426 text.contains("handed off"),
2427 "a mergeable run with an open round must say so: {text}"
2428 );
2429 }
2430
2431 #[test]
2432 fn a_failing_gate_command_shows_its_output() {
2433 let _guard = plain();
2434 let mut s = state();
2435 s.status = RunStatus::Blocked;
2436 s.gate = vec![CommandOutcome {
2437 command: "cargo make check".to_owned(),
2438 code: Some(101),
2439 output_tail: "error[E0308]: mismatched types".to_owned(),
2440 duration_ms: 0,
2441 resource_blocked: false,
2442 }];
2443 s.gate_ran = true;
2444
2445 let text = run(&s);
2446 assert!(text.contains("mismatched types"), "{text}");
2447 }
2448
2449 #[test]
2450 fn a_gate_with_no_commands_is_reported_as_not_tested() {
2451 let _guard = plain();
2452 let mut s = state();
2453 s.status = RunStatus::Ready;
2454 s.gate_ran = true;
2455 assert!(s.gate.is_empty());
2456
2457 let text = run(&s);
2458 assert!(
2459 text.contains("gate")
2460 && text.contains("no gate commands configured")
2461 && text.contains("not tested"),
2462 "an unconfigured gate must be explicit: {text}"
2463 );
2464 }
2465
2466 #[test]
2467 fn a_gate_that_has_not_run_yet_shows_nothing() {
2468 let _guard = plain();
2469 let s = state();
2470 assert!(!s.gate_ran);
2471 assert!(s.gate.is_empty());
2472
2473 let text = run(&s);
2474 assert!(
2475 !text.contains("no gate commands configured"),
2476 "an unattempted gate must not be shown as a pass: {text}"
2477 );
2478 }
2479
2480 #[test]
2481 fn active_seats_shows_who_has_not_answered_and_how_long_is_left() {
2482 let _guard = plain();
2483 let mut s = state();
2484 s.seat_started("judge", "judge-2", std::time::Duration::from_secs(120), 0);
2485 let text = active_seats(&s, Liveness::Live);
2486 assert!(text.contains("running now"));
2487 assert!(text.contains("judge-2"));
2488 assert!(text.contains("judge"));
2489 assert!(!text.contains("no live daemon"), "{text}");
2490 assert!(!text.contains("could not be confirmed"), "{text}");
2491 }
2492
2493 #[test]
2494 fn active_seats_flags_a_leftover_from_a_dead_process() {
2495 let _guard = plain();
2496 let mut s = state();
2497 s.seat_started("implement", "impl-B", std::time::Duration::from_secs(60), 0);
2498 let text = active_seats(&s, Liveness::Dead);
2499 assert!(
2500 text.contains("no live daemon"),
2501 "a stale entry must not read as running: {text}"
2502 );
2503 }
2504
2505 #[test]
2506 fn superseded_notice_names_the_run_that_replaced_it() {
2507 let _guard = plain();
2508 let text = superseded_notice(Some("bbbb"));
2509 assert!(text.contains("SUPERSEDED"), "{text}");
2510 assert!(text.contains("bbbb"), "{text}");
2511 }
2512
2513 #[test]
2514 fn superseded_notice_is_silent_for_the_latest_attempt() {
2515 let _guard = plain();
2516 assert_eq!(superseded_notice(None), "");
2517 }
2518
2519 #[test]
2520 fn list_line_marks_a_nonterminal_run_with_a_dead_driver_stale() {
2521 let _guard = plain();
2522 let mut s = state();
2523 s.status = RunStatus::Reviewing;
2524 let text = line_with_liveness(&s, Liveness::Dead);
2525 assert!(text.contains("STALE"), "{text}");
2526 assert!(text.contains("resume required"), "{text}");
2527 assert!(liveness_notice(&s, Liveness::Dead).contains("STALE"));
2528 }
2529
2530 #[test]
2535 fn active_seats_reports_uncertainty_without_claiming_death() {
2536 let _guard = plain();
2537 let mut s = state();
2538 s.seat_started("review", "review-1", std::time::Duration::from_secs(60), 0);
2539 let text = active_seats(&s, Liveness::Unknown);
2540 assert!(
2541 text.contains("could not be confirmed"),
2542 "an unproven state must read as uncertain, not dead: {text}"
2543 );
2544 assert!(!text.contains("no live daemon"), "{text}");
2545 }
2546
2547 #[test]
2548 fn resource_block_reports_saved_evidence_in_text_and_web() {
2549 use crate::run::{ResourceBlock, ResourceBlockCause};
2550 let legacy = serde_json::json!({
2551 "node": "fix", "kind": "shared build cache",
2552 "detail": "the run is parking", "at": "2026-10-24T09:01:51Z"
2553 });
2554 let block: ResourceBlock = serde_json::from_value(legacy).unwrap();
2555 assert_eq!(block.cause, ResourceBlockCause::Unrecorded);
2556 for (cause, detail, expected) in [
2557 (
2558 ResourceBlockCause::Unrecorded,
2559 "the run is parking",
2560 "cancelled for parking",
2561 ),
2562 (ResourceBlockCause::Unrecorded, "busy", "cause unrecorded"),
2563 (
2564 ResourceBlockCause::Other,
2565 "metadata write denied",
2566 "operation blocked",
2567 ),
2568 (
2569 ResourceBlockCause::Timeout {
2570 waited_secs: 12,
2571 limit_secs: 10,
2572 },
2573 "busy",
2574 "timed out after 12s (limit 10s)",
2575 ),
2576 (
2577 ResourceBlockCause::Cancelled {
2578 park: Some(crate::run::ParkCause::Operator),
2579 },
2580 "cancelled",
2581 "cancelled for parking",
2582 ),
2583 ] {
2584 let mut state = state();
2585 state.resource_block = Some(ResourceBlock {
2586 cause: cause.clone(),
2587 detail: detail.into(),
2588 ..block.clone()
2589 });
2590 let text = active_seats(&state, Liveness::Dead);
2591 assert!(text.contains(expected), "{text}");
2592 assert!(
2593 !text.contains("no agent was started"),
2594 "earlier nodes may have run"
2595 );
2596 assert_eq!(
2597 crate::report_view::build_with(&state, Liveness::Dead, None).active_seats,
2598 text
2599 );
2600 let saved = serde_json::to_value(&state.resource_block).unwrap();
2601 let reloaded: ResourceBlock = serde_json::from_value(saved).unwrap();
2602 assert_eq!(reloaded.cause, cause);
2603 }
2604 }
2605
2606 #[test]
2607 fn active_seats_is_empty_when_nothing_is_running() {
2608 let _guard = plain();
2609 assert_eq!(active_seats(&state(), Liveness::Live), "");
2610 }
2611
2612 #[test]
2618 fn active_seats_shows_a_running_verify_task_and_its_command() {
2619 let _guard = plain();
2620 let mut s = state();
2621 s.task_command(
2622 "e2e",
2623 "verify",
2624 0,
2625 "cargo test",
2626 2,
2627 3,
2628 std::time::Duration::from_secs(600),
2629 );
2630 let text = active_seats(&s, Liveness::Live);
2631 assert!(text.contains("e2e"), "{text}");
2632 assert!(text.contains("(2/3)"), "{text}");
2633 assert!(text.contains("cargo test"), "{text}");
2634 }
2635
2636 #[test]
2637 fn no_jobs_section_appears_when_nothing_was_ever_collected() {
2638 let _guard = plain();
2639 assert!(!run(&state()).contains("background jobs"));
2642 }
2643
2644 #[test]
2645 fn a_codex_roster_with_no_completed_jobs_yet_says_so_instead_of_staying_silent() {
2646 let _guard = plain();
2647 let mut s = state();
2648 s.config.agents.push(crate::config::AgentSpec {
2649 id: "codex-one".to_owned(),
2650 kind: crate::config::AgentKind::Codex,
2651 model: None,
2652 command: vec!["codex".to_owned()],
2653 extra_args: Vec::new(),
2654 env: BTreeMap::new(),
2655 prompt_delivery: None,
2656 max_turn_secs: None,
2657 });
2658 let text = run(&s);
2659 assert!(
2660 text.contains("background jobs"),
2661 "a run that could report this must not read the same as one that never could: \
2662 {text}"
2663 );
2664 assert!(text.contains("no completed command evidence yet"));
2665 }
2666
2667 #[test]
2668 fn recovered_running_and_unreadable_jobs_are_told_apart() {
2669 let _guard = plain();
2670 let mut s = state();
2671 s.jobs = vec![
2672 crate::run::JobRecord {
2673 node: "implement".to_owned(),
2674 round: None,
2675 seat: "impl-A".to_owned(),
2676 id: "item49".to_owned(),
2677 description: "cargo test --test graph_cached_gate".to_owned(),
2678 checked_at: jiff::Timestamp::now(),
2679 status: crate::run::JobStatus::Completed,
2680 exit_code: Some(0),
2681 result_summary: "test result: 2 passed; 0 failed".to_owned(),
2682 source: "codex".to_owned(),
2683 },
2684 crate::run::JobRecord {
2685 node: "fix".to_owned(),
2686 round: None,
2687 seat: "impl-A".to_owned(),
2688 id: "item52".to_owned(),
2689 description: "cargo test --test graph_split".to_owned(),
2690 checked_at: jiff::Timestamp::now(),
2691 status: crate::run::JobStatus::Failed,
2692 exit_code: Some(101),
2693 result_summary: "test result: 1 passed; 1 failed".to_owned(),
2694 source: "codex".to_owned(),
2695 },
2696 crate::run::JobRecord {
2697 node: "fix".to_owned(),
2698 round: None,
2699 seat: "impl-A".to_owned(),
2700 id: "item60".to_owned(),
2701 description: "cargo build".to_owned(),
2702 checked_at: jiff::Timestamp::now(),
2703 status: crate::run::JobStatus::Unknown,
2704 exit_code: None,
2705 result_summary: String::new(),
2706 source: "codex".to_owned(),
2707 },
2708 ];
2709 let text = run(&s);
2710 assert!(text.contains("background jobs"));
2711 assert!(text.contains("item49"));
2712 assert!(text.contains("item52"));
2713 assert!(text.contains("item60"));
2714 assert!(text.contains("completed"));
2718 assert!(text.contains("failed"));
2719 assert!(text.contains("unknown"));
2720 assert!(text.contains("adapter coverage"));
2722 }
2723
2724 #[test]
2725 fn a_jobs_own_round_is_shown_when_known() {
2726 let _guard = plain();
2727 let mut s = state();
2728 s.jobs = vec![crate::run::JobRecord {
2729 node: "review".to_owned(),
2730 round: Some(2),
2731 seat: "review-1".to_owned(),
2732 id: "item9".to_owned(),
2733 description: "cargo test --test graph_cached_gate".to_owned(),
2734 checked_at: jiff::Timestamp::now(),
2735 status: crate::run::JobStatus::Completed,
2736 exit_code: Some(0),
2737 result_summary: "test result: 2 passed; 0 failed".to_owned(),
2738 source: "codex".to_owned(),
2739 }];
2740 let text = run(&s);
2741 assert!(
2742 text.contains("round 2"),
2743 "the round this seat's own command ran in must be visible, distinct from magi's \
2744 own recorded verify: {text}"
2745 );
2746 }
2747
2748 #[test]
2749 fn stats_table_renders_without_runs() {
2750 let _guard = plain();
2751 let text = stats(&Stats::default());
2752 assert!(text.contains("0 total"));
2753 assert!(!text.contains("implementation"));
2754 assert!(!text.contains("node durations"));
2755 }
2756
2757 #[test]
2758 fn repo_summary_lists_run_count_and_completion_rate() {
2759 let _guard = plain();
2760 let a = crate::stats::RepoStats {
2761 repo: PathBuf::from("/repos/a"),
2762 name: "a".to_owned(),
2763 stats: Stats {
2764 totals: crate::stats::Totals {
2765 runs: 4,
2766 merged: 2,
2767 ready: 1,
2768 ..Default::default()
2769 },
2770 ..Default::default()
2771 },
2772 };
2773 let text = repo_summary(&[a]);
2774 assert!(text.contains("repos"), "{text}");
2775 assert!(text.contains("a 4 runs 75% completion"), "{text}");
2776 }
2777
2778 #[test]
2779 fn repo_summary_disambiguates_colliding_names_with_the_parent_directory() {
2780 let _guard = plain();
2781 let a = crate::stats::RepoStats {
2782 repo: PathBuf::from("/roots/one/magi"),
2783 name: "magi".to_owned(),
2784 stats: Stats::default(),
2785 };
2786 let b = crate::stats::RepoStats {
2787 repo: PathBuf::from("/roots/two/magi"),
2788 name: "magi".to_owned(),
2789 stats: Stats::default(),
2790 };
2791 let text = repo_summary(&[a, b]);
2792 assert!(text.contains("magi (one)"), "{text}");
2793 assert!(text.contains("magi (two)"), "{text}");
2794 }
2795
2796 #[test]
2797 fn stats_table_renders_advisor_reflection() {
2798 let _guard = plain();
2799 let st = Stats {
2800 advisors: vec![crate::stats::AdvisorStats {
2801 agent: "alpha".to_owned(),
2802 seated: 3,
2803 proposed: 2,
2804 absent: 1,
2805 faint: 1,
2806 strong: 1,
2807 }],
2808 ..Default::default()
2809 };
2810 let text = stats(&st);
2811 assert!(text.contains("design deliberation"));
2812 assert!(
2813 text.contains("approximate"),
2814 "must note the heuristic: {text}"
2815 );
2816 assert!(text.contains("alpha"));
2817 assert!(text.contains("50%"));
2818 }
2819
2820 #[test]
2821 fn stats_table_shows_release_bump_coverage_even_when_nothing_was_recorded() {
2822 let _guard = plain();
2827 let st = Stats {
2828 release_bumps: crate::stats::ReleaseBumpStats {
2829 merged: 3,
2830 ..Default::default()
2831 },
2832 ..Default::default()
2833 };
2834 let text = stats(&st);
2835 assert!(text.contains("release bumps"), "{text}");
2836 assert!(
2837 text.contains("0 of 3 merged run(s) recorded a bump"),
2838 "{text}"
2839 );
2840 }
2841
2842 #[test]
2843 fn stats_table_renders_release_bump_coverage_and_attention() {
2844 let _guard = plain();
2845 let st = Stats {
2846 release_bumps: crate::stats::ReleaseBumpStats {
2847 merged: 4,
2848 recorded: 2,
2849 pr_opened: 2,
2850 automerge_enabled: 1,
2851 merged_directly: 0,
2852 needs_attention: 1,
2853 },
2854 ..Default::default()
2855 };
2856 let text = stats(&st);
2857 assert!(text.contains("release bumps"), "{text}");
2858 assert!(
2859 text.contains("2 of 4 merged run(s) recorded a bump (50%)"),
2860 "{text}"
2861 );
2862 assert!(text.contains("1 clean, 1 needed a human (50%)"), "{text}");
2863 }
2864
2865 #[test]
2866 fn stats_table_renders_node_durations_with_a_bar_and_unmeasured_count() {
2867 let _guard = plain();
2868 let st = Stats {
2869 nodes: vec![
2870 crate::stats::NodeDuration {
2871 node: "implement".to_owned(),
2872 runs: 2,
2873 total_secs: 200,
2874 max_secs: 150,
2875 single: 1,
2876 },
2877 crate::stats::NodeDuration {
2878 node: "gate".to_owned(),
2879 runs: 0,
2880 total_secs: 0,
2881 max_secs: 0,
2882 single: 3,
2883 },
2884 ],
2885 ..Default::default()
2886 };
2887 let text = stats(&st);
2888 assert!(text.contains("node durations"));
2889 assert!(text.contains("implement"));
2890 assert!(text.contains("2 runs"));
2891 assert!(text.contains('█'), "the leader must draw a bar: {text}");
2892 assert!(text.contains("1 unmeasured"));
2893 assert!(text.contains("gate"));
2894 assert!(text.contains("3 unmeasured"));
2895 assert!(!text.contains('\x1b'), "colour leaked into a plain render");
2896 }
2897
2898 #[test]
2899 fn implementation_rates_use_independent_low_n_boundaries() {
2900 let _guard = plain();
2901 for (competitive_n, solo_n) in [(9, 10), (10, 9)] {
2902 let text = stats(&Stats {
2903 agents: vec![crate::stats::AgentStats {
2904 agent: "alpha".to_owned(),
2905 competitive_entered: competitive_n,
2906 competitive_wins: 1,
2907 solo_entered: solo_n,
2908 solo_adopted: 2,
2909 ..Default::default()
2910 }],
2911 ..Default::default()
2912 });
2913 let (competitive, solo) = text
2914 .split_once("implementation (solo runs, completion/adoption rate)")
2915 .unwrap();
2916 for (section, n, numerator) in [(competitive, competitive_n, 1), (solo, solo_n, 2)] {
2917 let row = section.lines().find(|line| line.contains("alpha")).unwrap();
2918 let cells: Vec<_> = row.split_whitespace().collect();
2919 assert_eq!(cells[1], numerator.to_string());
2920 assert_eq!(cells[2], n.to_string());
2921 assert_eq!(row.contains("low n"), n < 10);
2922 }
2923 }
2924 }
2925
2926 #[test]
2927 fn stats_table_renders_competitive_and_solo_tables_with_low_n() {
2928 let _guard = plain();
2929 let st = Stats {
2930 agents: vec![
2931 crate::stats::AgentStats {
2932 agent: "alpha".to_owned(),
2933 entered: 15,
2934 wins: 10,
2935 empty: 0,
2936 competitive_entered: 12,
2937 competitive_wins: 9,
2938 solo_entered: 3,
2939 solo_adopted: 1,
2940 },
2941 crate::stats::AgentStats {
2942 agent: "beta".to_owned(),
2943 entered: 5,
2944 wins: 2,
2945 empty: 1,
2946 competitive_entered: 4,
2947 competitive_wins: 2,
2948 solo_entered: 1,
2949 solo_adopted: 0,
2950 },
2951 ],
2952 ..Default::default()
2953 };
2954 let text = stats(&st);
2955 assert!(text.contains("implementation (competitive runs, 2+ candidates)"));
2956 assert!(text.contains("alpha"));
2957 assert!(text.contains("75%"));
2958 assert!(text.contains("beta"));
2959 assert!(text.contains("50%"));
2960 assert!(text.contains("implementation (solo runs, completion/adoption rate)"));
2961 assert!(text.contains("33%"));
2962 assert!(text.contains("low n"));
2963 }
2964}