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.merged_directly {
926 green("not needed (CI was already green; merged directly)")
927 } else if b.problem.is_some() {
928 bold(&red("FAILED"))
929 } else {
930 dim("not enabled")
931 }
932 );
933 if let Some(p) = &b.problem {
934 let _ = writeln!(s, " {}", p.lines().next().unwrap_or(""));
935 }
936 if let Some(a) = &b.action_required {
937 let _ = writeln!(s, " {} {a}", bold(&yellow("action required:")));
938 }
939 }
940
941 if !state.leaks.is_empty() {
942 let _ = writeln!(s, "\n{}", bold(&yellow("blindness warnings")));
943 for l in &state.leaks {
944 let _ = writeln!(s, " {} x{} in {}", l.token, l.count, l.site);
945 }
946 }
947
948 if let Some(w) = state.winner()
949 && !w.folded
950 {
951 let _ = writeln!(
952 s,
953 "\n{} {}\n branch {}",
954 bold("winner worktree"),
955 w.worktree.display(),
956 w.branch
957 );
958 }
959 s.push_str(&jobs_section(state));
960 s
961}
962
963pub fn active_seats(state: &RunState, live: Liveness) -> String {
979 if state.active.is_empty() {
980 return String::new();
981 }
982 let mut s = String::new();
983 let _ = writeln!(s, "\n{}", bold("running now"));
984 let now = jiff::Timestamp::now();
985 match live {
986 Liveness::Live => {}
987 Liveness::Dead => {
988 let _ = writeln!(
989 s,
990 " {}",
991 yellow(
992 "no live daemon claims this run right now — likely left behind by a killed process"
993 )
994 );
995 }
996 Liveness::Unknown => {
997 let overrun = if state.active_all_overrun(now) {
1003 " — every active seat has already run past its own timeout budget"
1004 } else {
1005 ""
1006 };
1007 let _ = writeln!(
1008 s,
1009 " {}",
1010 yellow(&format!(
1011 "whether a process is still driving this run could not be confirmed{overrun}"
1012 ))
1013 );
1014 }
1015 }
1016 for (seat, a) in state.seats_active() {
1017 let retry = if a.attempt > 0 {
1018 format!(" retry {}", a.attempt)
1019 } else {
1020 String::new()
1021 };
1022 let _ = writeln!(
1023 s,
1024 " {:<12} {:<12}{retry} {}s elapsed, {}s left of {}s",
1025 seat,
1026 a.node,
1027 a.elapsed_secs(now),
1028 a.remaining_secs(now),
1029 a.timeout_secs
1030 );
1031 }
1032 for (task, a) in state.tasks_active() {
1033 let retry = if a.attempt > 0 {
1034 format!(" retry {}", a.attempt)
1035 } else {
1036 String::new()
1037 };
1038 let progress = match (a.index, a.total) {
1039 (Some(i), Some(t)) => format!(" ({i}/{t})"),
1040 _ => String::new(),
1041 };
1042 let _ = writeln!(
1043 s,
1044 " {:<12} {:<12}{retry}{progress} {}s elapsed, {}s left of {}s",
1045 task,
1046 a.node,
1047 a.elapsed_secs(now),
1048 a.remaining_secs(now),
1049 a.timeout_secs
1050 );
1051 if let Some(command) = &a.command {
1052 let _ = writeln!(s, " {command}");
1053 }
1054 }
1055 s
1056}
1057
1058pub fn stats(stats: &Stats) -> String {
1060 let t = &stats.totals;
1061 let mut s = String::new();
1062 let _ = writeln!(s, "{}", bold("runs"));
1063 let _ = writeln!(
1064 s,
1065 " {} total - {} merged, {} ready, {} blocked, {} failed ({:.0}% completion)",
1066 t.runs,
1067 t.merged,
1068 t.ready,
1069 t.blocked,
1070 t.failed,
1071 t.completion_rate()
1072 );
1073 if t.tallied > 0 {
1074 let _ = writeln!(
1075 s,
1076 " {} tallied - {} split on first choice ({:.0}%), {} deliberated, \
1077 {} of those changed a mind, {} converged to unanimous",
1078 t.tallied,
1079 t.split,
1080 t.split_rate(),
1081 t.deliberated,
1082 t.minds_changed,
1083 t.converged
1084 );
1085 }
1086
1087 if !stats.agents.is_empty() {
1088 let _ = writeln!(
1089 s,
1090 "\n{}",
1091 bold("implementation (relative, on this workload)")
1092 );
1093 let _ = writeln!(
1094 s,
1095 " {:<14}{:>6}{:>8}{:>8}{:>8}",
1096 "agent", "won", "entered", "rate", "empty"
1097 );
1098 for a in &stats.agents {
1099 let _ = writeln!(
1100 s,
1101 " {:<14}{:>6}{:>8}{:>7.0}%{:>8}",
1102 a.agent,
1103 a.wins,
1104 a.entered,
1105 a.win_rate(),
1106 a.empty
1107 );
1108 }
1109 }
1110
1111 if !stats.reviewers.is_empty() {
1112 let _ = writeln!(s, "\n{}", bold("review"));
1113 let _ = writeln!(
1114 s,
1115 " {:<14}{:>8}{:>10}{:>11}{:>9}{:>9}{:>9}",
1116 "reviewer", "rounds", "submitted", "adopted/rd", "precision", "unique", "timeout"
1117 );
1118 for r in &stats.reviewers {
1119 let _ = writeln!(
1120 s,
1121 " {:<14}{:>8}{:>10}{:>11.2}{:>8.0}%{:>8.0}%{:>8.0}%",
1122 r.agent,
1123 r.rounds,
1124 r.submitted,
1125 r.adopted_per_round(),
1126 r.precision(),
1127 r.unique_rate(),
1128 r.timeout_rate()
1129 );
1130 }
1131 }
1132
1133 if stats.e2e.rounds > 0 || stats.e2e.deferred > 0 {
1134 let _ = writeln!(s, "\n{}", bold("verification"));
1135 let _ = writeln!(
1136 s,
1137 " {} rounds ran e2e, {} failed, {} of those with a clean static \
1138 review ({:.0}% sole detections), {} round(s) deferred it to the fixer",
1139 stats.e2e.rounds,
1140 stats.e2e.failures,
1141 stats.e2e.sole_detections,
1142 stats.e2e.sole_rate(),
1143 stats.e2e.deferred
1144 );
1145 }
1146 s
1147}
1148
1149#[cfg(test)]
1150mod tests {
1151 use super::*;
1152 use crate::config::Config;
1153 use crate::run::{
1154 Candidate, CommandOutcome, FixRecord, MergeOutcome, ReviewRecord, ReviewRound, RunState,
1155 Tally,
1156 };
1157 use std::collections::BTreeMap;
1158 use std::path::PathBuf;
1159 use std::sync::{Mutex, MutexGuard};
1160
1161 static SERIAL: Mutex<()> = Mutex::new(());
1163
1164 fn plain() -> MutexGuard<'static, ()> {
1165 let guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
1166 set_color(false);
1167 guard
1168 }
1169
1170 fn state() -> RunState {
1171 crate::run::set_home(std::env::temp_dir().join("magi-report-test-home"));
1176 let mut s = RunState::new(
1177 PathBuf::from("/repo"),
1178 "main".to_owned(),
1179 "abcdef1234".to_owned(),
1180 "add retries to the uploader".to_owned(),
1181 Config::default(),
1182 );
1183 s.candidates = vec![Candidate {
1184 index: 0,
1185 label: 'A',
1186 agent: "opus".to_owned(),
1187 branch: "magi/x/A".to_owned(),
1188 worktree: PathBuf::from("/wt/A"),
1189 summary: String::new(),
1190 stat: String::new(),
1191 files: 3,
1192 commits: 2,
1193 empty: false,
1194 failed: None,
1195 verified_noop: None,
1196 duration_ms: 42_000,
1197 folded: false,
1198 }];
1199 s.tally = Some(Tally {
1200 first_choice: BTreeMap::from([('A', 3)]),
1201 borda: BTreeMap::new(),
1202 winner: 'A',
1203 rankings: 3,
1204 unanimous_initial: true,
1205 deliberated: false,
1206 changed_votes: 0,
1207 unanimous_final: true,
1208 tie_break: None,
1209 judges: 3,
1210 present: 3,
1211 quorum: 2,
1212 met_quorum: true,
1213 uncontested: None,
1214 });
1215 s
1216 }
1217
1218 #[test]
1219 fn run_report_names_the_winner_and_its_author() {
1220 let _guard = plain();
1221 let text = run(&state());
1222 assert!(text.contains("<- winner"), "{text}");
1223 assert!(text.contains("opus"));
1224 assert!(text.contains("3 files, 2 commits"));
1225 assert!(text.contains("winner A"));
1226 assert!(!text.contains('\x1b'), "colour leaked into a plain render");
1227 }
1228
1229 #[test]
1230 fn a_candidates_own_summary_is_surfaced_not_only_kept_in_run_json() {
1231 let _guard = plain();
1237 let mut s = state();
1238 s.candidates[0].summary =
1239 "investigated 6c5e/8df3: both already merged, see talk 07fe.\nmore detail below."
1240 .to_owned();
1241 let text = run(&s);
1242 assert!(
1243 text.contains("investigated 6c5e/8df3: both already merged, see talk 07fe."),
1244 "{text}"
1245 );
1246 }
1247
1248 #[test]
1249 fn a_verified_noop_run_does_not_read_as_a_failure() {
1250 let _guard = plain();
1254 let mut s = state();
1255 s.status = RunStatus::VerifiedNoop;
1256 s.tally = None;
1257 s.candidates = vec![Candidate {
1258 index: 0,
1259 label: 'A',
1260 agent: "opus".to_owned(),
1261 branch: "magi/x/A".to_owned(),
1262 worktree: PathBuf::from("/wt/A"),
1263 summary: String::new(),
1264 stat: String::new(),
1265 files: 0,
1266 commits: 0,
1267 empty: true,
1268 failed: None,
1269 verified_noop: Some("already fixed by b32cfc4, which is on main".to_owned()),
1270 duration_ms: 9_000,
1271 folded: false,
1272 }];
1273 let text = run(&s);
1274 assert!(
1275 text.contains("agent-verified no-op"),
1276 "the status and the candidate flag must both say so: {text}"
1277 );
1278 assert!(
1279 text.contains("already fixed by b32cfc4"),
1280 "the evidence itself must be readable, not just the verdict: {text}"
1281 );
1282 assert!(
1283 !text.to_lowercase().contains("failed"),
1284 "a verified no-op must never read as the failure it is not: {text}"
1285 );
1286 }
1287
1288 #[test]
1289 fn colour_is_emitted_only_when_enabled() {
1290 let _guard = plain();
1291 set_color(true);
1292 let coloured = run(&state());
1293 set_color(false);
1294 let plain = run(&state());
1295 assert!(coloured.contains('\x1b'));
1296 assert!(!plain.contains('\x1b'));
1297 assert!(coloured.len() > plain.len());
1298 }
1299
1300 #[test]
1301 fn list_line_is_single_line() {
1302 let _guard = plain();
1303 let l = line(&state());
1304 assert_eq!(l.lines().count(), 1);
1305 assert!(l.contains("add retries"));
1306 assert!(l.contains("win A (opus)"));
1307 }
1308
1309 #[test]
1310 fn an_uncontested_run_does_not_read_as_a_collapsed_panel() {
1311 let _guard = plain();
1312 let mut s = state();
1313 s.tally = Some(Tally {
1314 first_choice: BTreeMap::from([('A', 0)]),
1315 borda: BTreeMap::new(),
1316 winner: 'A',
1317 rankings: 0,
1318 unanimous_initial: false,
1319 deliberated: false,
1320 changed_votes: 0,
1321 unanimous_final: false,
1322 tie_break: None,
1323 judges: 0,
1324 present: 0,
1325 quorum: 0,
1326 met_quorum: true,
1327 uncontested: Some(
1328 "only candidate A produced a usable change; no panel was asked".to_owned(),
1329 ),
1330 });
1331 let text = run(&s);
1332 assert!(
1333 !text.contains("0/3"),
1334 "no panel sat, so the judges line must not read as one that collapsed: {text}"
1335 );
1336 assert!(!text.contains("no usable ranking"), "{text}");
1337 assert!(!text.contains("still split"), "{text}");
1338 assert!(!text.contains("BELOW QUORUM"), "{text}");
1339 assert!(
1340 text.contains("not needed"),
1341 "the report must say judging was skipped, not silent: {text}"
1342 );
1343 assert!(text.contains("winner A"));
1344 }
1345
1346 #[test]
1347 fn a_below_quorum_run_still_reads_as_a_collapsed_panel() {
1348 let _guard = plain();
1349 let mut s = state();
1350 s.tally = Some(Tally {
1351 first_choice: BTreeMap::from([('A', 1), ('B', 0)]),
1352 borda: BTreeMap::new(),
1353 winner: 'A',
1354 rankings: 1,
1355 unanimous_initial: false,
1356 deliberated: false,
1357 changed_votes: 0,
1358 unanimous_final: false,
1359 tie_break: None,
1360 judges: 3,
1361 present: 1,
1362 quorum: 2,
1363 met_quorum: false,
1364 uncontested: None,
1365 });
1366 let text = run(&s);
1367 assert!(text.contains("1/3"), "{text}");
1368 assert!(
1369 text.contains("BELOW QUORUM"),
1370 "a real collapse must still be flagged: {text}"
1371 );
1372 assert!(
1373 !text.contains("not needed"),
1374 "a collapsed panel must not be described as one that was never asked: {text}"
1375 );
1376 }
1377
1378 #[test]
1379 fn a_mode_none_merge_does_not_read_as_landed() {
1380 let _guard = plain();
1381 let mut s = state();
1382 s.merge = Some(MergeOutcome {
1383 mode: crate::config::MergeMode::None,
1384 ok: true,
1385 detail: "git -C /repo merge --no-ff magi/x/A".to_owned(),
1386 });
1387 let text = run(&s);
1388 assert!(
1389 !text.contains(" ok"),
1390 "mode none must not be shown as a landed merge: {text}"
1391 );
1392 assert!(text.contains("not landed"), "{text}");
1393 assert!(
1394 text.contains("branch magi/x/A"),
1395 "the report must say what's left behind: {text}"
1396 );
1397 assert!(
1398 text.contains("rebase"),
1399 "the report must point at the hand-landing steps: {text}"
1400 );
1401 assert!(
1402 !text.contains("placeholder subject"),
1403 "the default merge style is `merge`, which never inherits a \
1404 placeholder subject, so the squash caveat must not appear: {text}"
1405 );
1406 }
1407
1408 #[test]
1409 fn a_mode_none_squash_merge_warns_about_the_placeholder_subject() {
1410 let _guard = plain();
1411 let mut s = state();
1412 s.config.merge.style = MergeStyle::Squash;
1413 s.merge = Some(MergeOutcome {
1414 mode: crate::config::MergeMode::None,
1415 ok: true,
1416 detail: "git -C /repo merge --squash magi/x/A && git -C /repo commit -m \"add \
1417 retries\""
1418 .to_owned(),
1419 });
1420 let text = run(&s);
1421 assert!(
1422 text.contains("placeholder subject"),
1423 "a squash-style manual merge must warn about the missing message: {text}"
1424 );
1425 assert!(text.contains("--squash"), "{text}");
1426 }
1427
1428 #[test]
1429 fn a_ready_run_left_by_merge_mode_none_does_not_read_as_a_plain_ready() {
1430 let _guard = plain();
1431 let mut s = state();
1432 s.status = RunStatus::Ready;
1433 s.merge = Some(MergeOutcome {
1434 mode: crate::config::MergeMode::None,
1435 ok: true,
1436 detail: "git -C /repo merge --no-ff magi/x/A".to_owned(),
1437 });
1438
1439 let list = line(&s);
1440 assert!(
1441 !list.contains(" ready "),
1442 "a mode-none run must not read as a plain ready in `magi list`: {list}"
1443 );
1444 assert!(list.contains("no-op by design"), "{list}");
1445
1446 let full = run(&s);
1447 assert!(
1448 !full.contains("magi run") || !full.lines().next().unwrap().contains(" ready"),
1449 "the header line of `magi show` must not say plain ready either: {full}"
1450 );
1451 assert!(full.contains("no-op by design"), "{full}");
1452 }
1453
1454 #[test]
1455 fn an_ordinary_ready_run_still_reads_as_ready() {
1456 let _guard = plain();
1457 let mut s = state();
1458 s.status = RunStatus::Ready;
1459 s.merge = Some(MergeOutcome {
1463 mode: crate::config::MergeMode::Pr,
1464 ok: false,
1465 detail: "https://example.com/pr/1 was closed without merging".to_owned(),
1466 });
1467
1468 let list = line(&s);
1469 assert!(list.contains("ready"), "{list}");
1470 assert!(!list.contains("no-op by design"), "{list}");
1471 }
1472
1473 #[test]
1474 fn the_list_line_does_not_flag_an_uncontested_run_as_short_judges() {
1475 let _guard = plain();
1476 let mut s = state();
1477 s.tally = Some(Tally {
1478 first_choice: BTreeMap::from([('A', 0)]),
1479 borda: BTreeMap::new(),
1480 winner: 'A',
1481 rankings: 0,
1482 unanimous_initial: false,
1483 deliberated: false,
1484 changed_votes: 0,
1485 unanimous_final: false,
1486 tie_break: None,
1487 judges: 0,
1488 present: 0,
1489 quorum: 0,
1490 met_quorum: true,
1491 uncontested: Some("only candidate A produced a usable change".to_owned()),
1492 });
1493 let l = line(&s);
1494 assert!(
1495 !l.contains("judges") && !l.contains("quorum"),
1496 "an uncontested run must not carry the same badge a short panel gets: {l}"
1497 );
1498 }
1499
1500 #[test]
1501 fn long_instructions_are_elided() {
1502 let _guard = plain();
1503 let mut s = state();
1504 s.instruction = "x".repeat(200);
1505 assert!(line(&s).contains('…'));
1506 }
1507
1508 #[test]
1509 fn a_lost_fix_report_reads_differently_from_zero_adoption() {
1510 let _guard = plain();
1511 let mut lost = state();
1512 lost.reviews = vec![ReviewRound {
1513 round: 1,
1514 head: "abc1234".to_owned(),
1515 verified_head: None,
1516 verified_at: None,
1517 reviews: Vec::new(),
1518 e2e: Vec::new(),
1519 verify_retried: false,
1520 e2e_deferred: false,
1521 e2e_defer_reason: None,
1522 fix: Some(FixRecord {
1523 agent: "opus".to_owned(),
1524 addressed: Vec::new(),
1525 rejected: Vec::new(),
1526 notes: String::new(),
1527 committed: true,
1528 failed: Some("timed out".to_owned()),
1529 duration_ms: 0,
1530 continuation: None,
1531 }),
1532 blocking: 3,
1533 answered: 0,
1534 expected: 0,
1535 clean: false,
1536 progressed: false,
1537 vote_split: false,
1538 reconsideration: Vec::new(),
1539 verdict: None,
1540 }];
1541 let text = run(&lost);
1542 assert!(text.contains("adoption report lost (timed out)"), "{text}");
1543 assert!(
1544 !text.contains("0 addressed"),
1545 "a lost report must never read as `0 addressed`: {text}"
1546 );
1547
1548 let mut rejected_all = state();
1549 rejected_all.reviews = vec![ReviewRound {
1550 round: 1,
1551 head: "abc1234".to_owned(),
1552 verified_head: None,
1553 verified_at: None,
1554 reviews: Vec::new(),
1555 e2e: Vec::new(),
1556 verify_retried: false,
1557 e2e_deferred: false,
1558 e2e_defer_reason: None,
1559 fix: Some(FixRecord {
1560 agent: "opus".to_owned(),
1561 addressed: Vec::new(),
1562 rejected: Vec::new(),
1563 notes: String::new(),
1564 committed: true,
1565 failed: None,
1566 duration_ms: 0,
1567 continuation: None,
1568 }),
1569 blocking: 3,
1570 answered: 0,
1571 expected: 0,
1572 clean: false,
1573 progressed: false,
1574 vote_split: false,
1575 reconsideration: Vec::new(),
1576 verdict: None,
1577 }];
1578 let text2 = run(&rejected_all);
1579 assert!(
1580 text2.contains("0 addressed / 0 rejected"),
1581 "a round the fixer actually reported on keeps the count: {text2}"
1582 );
1583 }
1584
1585 #[test]
1586 fn a_split_round_shows_every_seat_vote_and_the_reconsideration() {
1587 use crate::run::ReviewRevoteRecord;
1588 use crate::verdict::ReviewVote;
1589
1590 let _guard = plain();
1591 let mut s = state();
1592 s.reviews = vec![ReviewRound {
1593 round: 1,
1594 head: "abc1234".to_owned(),
1595 verified_head: None,
1596 verified_at: None,
1597 reviews: vec![
1598 ReviewRecord {
1599 attempts: 0,
1600 reviewer: 1,
1601 agent: "alpha".to_owned(),
1602 summary: String::new(),
1603 findings: Vec::new(),
1604 vote: Some(ReviewVote::Approve),
1605 failed: None,
1606 duration_ms: 0,
1607 },
1608 ReviewRecord {
1609 attempts: 0,
1610 reviewer: 2,
1611 agent: "beta".to_owned(),
1612 summary: String::new(),
1613 findings: Vec::new(),
1614 vote: Some(ReviewVote::Reject),
1615 failed: None,
1616 duration_ms: 0,
1617 },
1618 ],
1619 e2e: Vec::new(),
1620 verify_retried: false,
1621 e2e_deferred: false,
1622 e2e_defer_reason: None,
1623 fix: None,
1624 blocking: 0,
1625 answered: 2,
1626 expected: 2,
1627 clean: false,
1628 progressed: false,
1629 vote_split: true,
1630 reconsideration: vec![ReviewRevoteRecord {
1631 reviewer: 2,
1632 agent: "beta".to_owned(),
1633 vote: Some(ReviewVote::ApproveWithFindings),
1634 reason: "the other seat's read holds up".to_owned(),
1635 failed: None,
1636 }],
1637 verdict: Some(ReviewVote::ApproveWithFindings),
1638 }];
1639 let text = run(&s);
1640 assert!(text.contains("review-1 vote"), "{text}");
1641 assert!(text.contains("review-2 vote"), "{text}");
1642 assert!(text.contains("panel split"), "{text}");
1643 assert!(text.contains("reconsideration"), "{text}");
1644 assert!(text.contains("the other seat's read holds up"), "{text}");
1645 }
1646
1647 #[test]
1648 fn an_incomplete_panel_and_a_lost_fix_report_both_stay_on_the_round_line() {
1649 let _guard = plain();
1655 let mut s = state();
1656 s.reviews = vec![ReviewRound {
1657 round: 1,
1658 head: "abc1234".to_owned(),
1659 verified_head: None,
1660 verified_at: None,
1661 reviews: vec![
1662 ReviewRecord {
1663 attempts: 0,
1664 reviewer: 1,
1665 agent: "alpha".to_owned(),
1666 summary: String::new(),
1667 findings: Vec::new(),
1668 vote: None,
1669 failed: None,
1670 duration_ms: 0,
1671 },
1672 ReviewRecord {
1673 attempts: 0,
1674 reviewer: 2,
1675 agent: "beta".to_owned(),
1676 summary: String::new(),
1677 findings: Vec::new(),
1678 vote: None,
1679 failed: Some("agent timed out".to_owned()),
1680 duration_ms: 0,
1681 },
1682 ],
1683 e2e: Vec::new(),
1684 verify_retried: false,
1685 e2e_deferred: false,
1686 e2e_defer_reason: None,
1687 fix: Some(FixRecord {
1688 agent: "opus".to_owned(),
1689 addressed: Vec::new(),
1690 rejected: Vec::new(),
1691 notes: String::new(),
1692 committed: true,
1693 failed: Some("timed out".to_owned()),
1694 duration_ms: 0,
1695 continuation: None,
1696 }),
1697 blocking: 0,
1698 answered: 1,
1699 expected: 2,
1700 clean: false,
1701 progressed: true,
1702 vote_split: false,
1703 reconsideration: Vec::new(),
1704 verdict: None,
1705 }];
1706 let text = run(&s);
1707 assert!(text.contains("incomplete"), "{text}");
1708 assert!(text.contains("1/2 reviewers answered"), "{text}");
1709 assert!(text.contains("review-2: agent timed out"), "{text}");
1710 assert!(text.contains("adoption report lost (timed out)"), "{text}");
1711 assert!(
1712 !text.contains("clean"),
1713 "a round missing half its panel must never render as clean: {text}"
1714 );
1715 }
1716
1717 #[test]
1718 fn a_build_failure_is_not_reported_as_a_test_failure() {
1719 let _guard = plain();
1720 let mut s = state();
1721 s.reviews = vec![ReviewRound {
1722 round: 1,
1723 head: "abc1234".to_owned(),
1724 verified_head: None,
1725 verified_at: None,
1726 reviews: Vec::new(),
1727 e2e: vec![CommandOutcome {
1728 command: "cargo test".to_owned(),
1729 code: Some(1),
1730 output_tail: "LINK : fatal error LNK1104: cannot open file".to_owned(),
1731 duration_ms: 100,
1732 resource_blocked: false,
1733 }],
1734 verify_retried: true,
1735 e2e_deferred: false,
1736 e2e_defer_reason: None,
1737 fix: None,
1738 blocking: 0,
1739 answered: 0,
1740 expected: 0,
1741 clean: false,
1742 progressed: false,
1743 vote_split: false,
1744 reconsideration: Vec::new(),
1745 verdict: None,
1746 }];
1747 let text = run(&s);
1748 assert!(text.contains("could not run"), "{text}");
1749 assert!(text.contains("retried once"), "{text}");
1750 assert!(!text.contains("e2e RED"), "{text}");
1751 }
1752
1753 #[test]
1754 fn a_resource_blocked_e2e_never_reads_as_red_or_as_a_build_failure() {
1755 let _guard = plain();
1756 let mut s = state();
1757 s.reviews = vec![ReviewRound {
1758 round: 1,
1759 head: "abc1234".to_owned(),
1760 verified_head: None,
1761 verified_at: None,
1762 reviews: Vec::new(),
1763 e2e: vec![CommandOutcome {
1764 command: "(waiting for the shared build cache)".to_owned(),
1765 code: None,
1766 output_tail: "held by run x node e2e seat e2e".to_owned(),
1767 duration_ms: 100,
1768 resource_blocked: true,
1769 }],
1770 verify_retried: false,
1771 e2e_deferred: false,
1772 e2e_defer_reason: None,
1773 fix: None,
1774 blocking: 0,
1775 answered: 0,
1776 expected: 0,
1777 clean: false,
1778 progressed: false,
1779 vote_split: false,
1780 reconsideration: Vec::new(),
1781 verdict: None,
1782 }];
1783 let text = run(&s);
1784 assert!(text.contains("shared build cache unavailable"), "{text}");
1785 assert!(!text.contains("e2e RED"), "{text}");
1786 assert!(!text.contains("build/link failure"), "{text}");
1787 }
1788
1789 #[test]
1790 fn a_round_with_more_than_one_e2e_command_names_each_one() {
1791 let _guard = plain();
1796 let mut s = state();
1797 s.reviews = vec![ReviewRound {
1798 round: 1,
1799 head: "abc1234".to_owned(),
1800 verified_head: Some("abc1234".to_owned()),
1801 verified_at: Some(jiff::Timestamp::now()),
1802 reviews: Vec::new(),
1803 e2e: vec![
1804 CommandOutcome {
1805 command: "cargo test --locked --all-targets".to_owned(),
1806 code: Some(0),
1807 output_tail: String::new(),
1808 duration_ms: 0,
1809 resource_blocked: false,
1810 },
1811 CommandOutcome {
1812 command: "cargo make check".to_owned(),
1813 code: Some(1),
1814 output_tail: "clippy: unused import".to_owned(),
1815 duration_ms: 0,
1816 resource_blocked: false,
1817 },
1818 ],
1819 verify_retried: false,
1820 e2e_deferred: false,
1821 e2e_defer_reason: None,
1822 fix: None,
1823 blocking: 0,
1824 answered: 0,
1825 expected: 0,
1826 clean: false,
1827 progressed: false,
1828 vote_split: false,
1829 reconsideration: Vec::new(),
1830 verdict: None,
1831 }];
1832 let text = run(&s);
1833 assert!(text.contains("cargo test --locked --all-targets"), "{text}");
1834 assert!(text.contains("cargo make check"), "{text}");
1835 assert!(text.contains("clippy: unused import"), "{text}");
1836 }
1837
1838 #[test]
1839 fn a_declined_finding_shows_its_reason() {
1840 use crate::verdict::{Finding, Rejection, Severity};
1841
1842 let _guard = plain();
1843 let mut s = state();
1844 s.status = RunStatus::Ready;
1845 s.reviews = vec![ReviewRound {
1846 round: 1,
1847 head: "deadbee".to_owned(),
1848 verified_head: None,
1849 verified_at: None,
1850 reviews: vec![ReviewRecord {
1851 attempts: 0,
1852 reviewer: 1,
1853 agent: "alpha".to_owned(),
1854 summary: String::new(),
1855 findings: vec![Finding {
1856 id: "R1-1-1".to_owned(),
1857 severity: Severity::Major,
1858 file: None,
1859 line: None,
1860 title: "still open".to_owned(),
1861 detail: String::new(),
1862 }],
1863 vote: None,
1864 failed: None,
1865 duration_ms: 0,
1866 }],
1867 e2e: vec![CommandOutcome {
1868 command: "cargo test".to_owned(),
1869 code: Some(0),
1870 output_tail: String::new(),
1871 duration_ms: 0,
1872 resource_blocked: false,
1873 }],
1874 verify_retried: false,
1875 e2e_deferred: false,
1876 e2e_defer_reason: None,
1877 fix: Some(FixRecord {
1878 agent: "alpha".to_owned(),
1879 addressed: Vec::new(),
1880 rejected: vec![Rejection {
1881 id: "R1-1-2".to_owned(),
1882 why: "cannot be triggered from any caller".to_owned(),
1883 }],
1884 notes: String::new(),
1885 committed: true,
1886 failed: None,
1887 duration_ms: 0,
1888 continuation: None,
1889 }),
1890 blocking: 1,
1891 answered: 1,
1892 expected: 1,
1893 clean: false,
1894 progressed: true,
1895 vote_split: false,
1896 reconsideration: Vec::new(),
1897 verdict: None,
1898 }];
1899
1900 let text = run(&s);
1901 assert!(text.contains("R1-1-2"), "{text}");
1902 assert!(text.contains("cannot be triggered"), "{text}");
1903 assert!(text.contains("still open"), "{text}");
1904 assert!(
1905 text.contains("handed off"),
1906 "a mergeable run with an open round must say so: {text}"
1907 );
1908 }
1909
1910 #[test]
1911 fn a_failing_gate_command_shows_its_output() {
1912 let _guard = plain();
1913 let mut s = state();
1914 s.status = RunStatus::Blocked;
1915 s.gate = vec![CommandOutcome {
1916 command: "cargo make check".to_owned(),
1917 code: Some(101),
1918 output_tail: "error[E0308]: mismatched types".to_owned(),
1919 duration_ms: 0,
1920 resource_blocked: false,
1921 }];
1922 s.gate_ran = true;
1923
1924 let text = run(&s);
1925 assert!(text.contains("mismatched types"), "{text}");
1926 }
1927
1928 #[test]
1929 fn a_gate_with_no_commands_configured_shows_a_pass_not_silence() {
1930 let _guard = plain();
1931 let mut s = state();
1932 s.status = RunStatus::Ready;
1933 s.gate_ran = true;
1934 assert!(s.gate.is_empty());
1935
1936 let text = run(&s);
1937 assert!(
1938 text.contains("gate") && text.contains("no gate commands configured"),
1939 "a run gated on nothing must say so, not read as if the gate never ran: {text}"
1940 );
1941 }
1942
1943 #[test]
1944 fn a_gate_that_has_not_run_yet_shows_nothing() {
1945 let _guard = plain();
1946 let s = state();
1947 assert!(!s.gate_ran);
1948 assert!(s.gate.is_empty());
1949
1950 let text = run(&s);
1951 assert!(
1952 !text.contains("no gate commands configured"),
1953 "an unattempted gate must not be shown as a pass: {text}"
1954 );
1955 }
1956
1957 #[test]
1958 fn active_seats_shows_who_has_not_answered_and_how_long_is_left() {
1959 let _guard = plain();
1960 let mut s = state();
1961 s.seat_started("judge", "judge-2", std::time::Duration::from_secs(120), 0);
1962 let text = active_seats(&s, Liveness::Live);
1963 assert!(text.contains("running now"));
1964 assert!(text.contains("judge-2"));
1965 assert!(text.contains("judge"));
1966 assert!(!text.contains("no live daemon"), "{text}");
1967 assert!(!text.contains("could not be confirmed"), "{text}");
1968 }
1969
1970 #[test]
1971 fn active_seats_flags_a_leftover_from_a_dead_process() {
1972 let _guard = plain();
1973 let mut s = state();
1974 s.seat_started("implement", "impl-B", std::time::Duration::from_secs(60), 0);
1975 let text = active_seats(&s, Liveness::Dead);
1976 assert!(
1977 text.contains("no live daemon"),
1978 "a stale entry must not read as running: {text}"
1979 );
1980 }
1981
1982 #[test]
1983 fn list_line_marks_a_nonterminal_run_with_a_dead_driver_stale() {
1984 let _guard = plain();
1985 let mut s = state();
1986 s.status = RunStatus::Reviewing;
1987 let text = line_with_liveness(&s, Liveness::Dead);
1988 assert!(text.contains("STALE"), "{text}");
1989 assert!(text.contains("resume required"), "{text}");
1990 assert!(liveness_notice(&s, Liveness::Dead).contains("STALE"));
1991 }
1992
1993 #[test]
1998 fn active_seats_reports_uncertainty_without_claiming_death() {
1999 let _guard = plain();
2000 let mut s = state();
2001 s.seat_started("review", "review-1", std::time::Duration::from_secs(60), 0);
2002 let text = active_seats(&s, Liveness::Unknown);
2003 assert!(
2004 text.contains("could not be confirmed"),
2005 "an unproven state must read as uncertain, not dead: {text}"
2006 );
2007 assert!(!text.contains("no live daemon"), "{text}");
2008 }
2009
2010 #[test]
2011 fn active_seats_is_empty_when_nothing_is_running() {
2012 let _guard = plain();
2013 assert_eq!(active_seats(&state(), Liveness::Live), "");
2014 }
2015
2016 #[test]
2022 fn active_seats_shows_a_running_verify_task_and_its_command() {
2023 let _guard = plain();
2024 let mut s = state();
2025 s.task_command(
2026 "e2e",
2027 "verify",
2028 0,
2029 "cargo test",
2030 2,
2031 3,
2032 std::time::Duration::from_secs(600),
2033 );
2034 let text = active_seats(&s, Liveness::Live);
2035 assert!(text.contains("e2e"), "{text}");
2036 assert!(text.contains("(2/3)"), "{text}");
2037 assert!(text.contains("cargo test"), "{text}");
2038 }
2039
2040 #[test]
2041 fn no_jobs_section_appears_when_nothing_was_ever_collected() {
2042 let _guard = plain();
2043 assert!(!run(&state()).contains("background jobs"));
2046 }
2047
2048 #[test]
2049 fn a_codex_roster_with_no_completed_jobs_yet_says_so_instead_of_staying_silent() {
2050 let _guard = plain();
2051 let mut s = state();
2052 s.config.agents.push(crate::config::AgentSpec {
2053 id: "codex-one".to_owned(),
2054 kind: crate::config::AgentKind::Codex,
2055 model: None,
2056 command: vec!["codex".to_owned()],
2057 extra_args: Vec::new(),
2058 env: BTreeMap::new(),
2059 prompt_delivery: None,
2060 });
2061 let text = run(&s);
2062 assert!(
2063 text.contains("background jobs"),
2064 "a run that could report this must not read the same as one that never could: \
2065 {text}"
2066 );
2067 assert!(text.contains("no completed command evidence yet"));
2068 }
2069
2070 #[test]
2071 fn recovered_running_and_unreadable_jobs_are_told_apart() {
2072 let _guard = plain();
2073 let mut s = state();
2074 s.jobs = vec![
2075 crate::run::JobRecord {
2076 node: "implement".to_owned(),
2077 round: None,
2078 seat: "impl-A".to_owned(),
2079 id: "item49".to_owned(),
2080 description: "cargo test --test graph_cached_gate".to_owned(),
2081 checked_at: jiff::Timestamp::now(),
2082 status: crate::run::JobStatus::Completed,
2083 exit_code: Some(0),
2084 result_summary: "test result: 2 passed; 0 failed".to_owned(),
2085 source: "codex".to_owned(),
2086 },
2087 crate::run::JobRecord {
2088 node: "fix".to_owned(),
2089 round: None,
2090 seat: "impl-A".to_owned(),
2091 id: "item52".to_owned(),
2092 description: "cargo test --test graph_split".to_owned(),
2093 checked_at: jiff::Timestamp::now(),
2094 status: crate::run::JobStatus::Failed,
2095 exit_code: Some(101),
2096 result_summary: "test result: 1 passed; 1 failed".to_owned(),
2097 source: "codex".to_owned(),
2098 },
2099 crate::run::JobRecord {
2100 node: "fix".to_owned(),
2101 round: None,
2102 seat: "impl-A".to_owned(),
2103 id: "item60".to_owned(),
2104 description: "cargo build".to_owned(),
2105 checked_at: jiff::Timestamp::now(),
2106 status: crate::run::JobStatus::Unknown,
2107 exit_code: None,
2108 result_summary: String::new(),
2109 source: "codex".to_owned(),
2110 },
2111 ];
2112 let text = run(&s);
2113 assert!(text.contains("background jobs"));
2114 assert!(text.contains("item49"));
2115 assert!(text.contains("item52"));
2116 assert!(text.contains("item60"));
2117 assert!(text.contains("completed"));
2121 assert!(text.contains("failed"));
2122 assert!(text.contains("unknown"));
2123 assert!(text.contains("adapter coverage"));
2125 }
2126
2127 #[test]
2128 fn a_jobs_own_round_is_shown_when_known() {
2129 let _guard = plain();
2130 let mut s = state();
2131 s.jobs = vec![crate::run::JobRecord {
2132 node: "review".to_owned(),
2133 round: Some(2),
2134 seat: "review-1".to_owned(),
2135 id: "item9".to_owned(),
2136 description: "cargo test --test graph_cached_gate".to_owned(),
2137 checked_at: jiff::Timestamp::now(),
2138 status: crate::run::JobStatus::Completed,
2139 exit_code: Some(0),
2140 result_summary: "test result: 2 passed; 0 failed".to_owned(),
2141 source: "codex".to_owned(),
2142 }];
2143 let text = run(&s);
2144 assert!(
2145 text.contains("round 2"),
2146 "the round this seat's own command ran in must be visible, distinct from magi's \
2147 own recorded verify: {text}"
2148 );
2149 }
2150
2151 #[test]
2152 fn stats_table_renders_without_runs() {
2153 let _guard = plain();
2154 let text = stats(&Stats::default());
2155 assert!(text.contains("0 total"));
2156 assert!(!text.contains("implementation"));
2157 }
2158}