Skip to main content

magi/
report.rs

1//! Terminal rendering.
2//!
3//! A run produces a lot of state; the report exists so the operator can decide
4//! what to do next without opening `run.json`. It leads with the disagreement,
5//! because that is the part that carries information: three judges agreeing
6//! tells you nothing the winner's diff does not.
7//!
8//! Colour is a six-line local implementation rather than a crate. The
9//! alternatives all decide *for* you whether the stream supports colour, which
10//! makes the output untestable — `assert!(text.contains("winner  A"))` fails on
11//! an escape sequence the test never asked for.
12use 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
25/// Turn colour on or off for every subsequent render.
26pub 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
57/// Colour for a status word.
58///
59/// `Stalled` is deliberately not green: a run whose judges were taken out by a
60/// rate limit must not look like a healthy `Ready` in a one-line listing.
61///
62/// A `Ready` reached via `[merge] mode = "none"` is a second case that must
63/// not look like a plain `Ready`: that run is done for good, never picked up
64/// by the PR-polling merge watcher or anything else, while an ordinary
65/// `Ready` (a PR closed without merging, an already-concluded re-entry) may
66/// still be a live landing candidate. See [`RunState::unmerged_by_design`].
67fn 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        // Not `Failed`'s red: every candidate agreed, with evidence, that
82        // nothing belongs in this worktree — the opposite of a run that
83        // could not do the work. See `RunStatus::VerifiedNoop`'s own doc.
84        RunStatus::VerifiedNoop => cyan(text),
85        _ => cyan(text),
86    }
87}
88
89/// Colour for a reviewer vote — the same scale a finding's severity gets:
90/// green for no reservations, yellow for proceed-but-look-at-this, red for a
91/// vote that says stop.
92fn 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
101/// One-line summary, for `magi list`.
102pub fn line(state: &RunState) -> String {
103    line_with_liveness(state, Liveness::Unknown)
104}
105
106/// [`line`] with the independent process-liveness observation available to
107/// `magi list`.
108pub 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    // A below-quorum verdict carries an explicit stamp so a row in a listing
115    // reads "stalled" and "2/3 judges" without opening the report.
116    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
147/// A plainly visible verdict for a non-terminal run whose recorded driver is
148/// gone. Kept separate from [`active_seats`] because a process can die at a
149/// node boundary with no active seat left in the saved state.
150pub 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
174/// A short badge for a [`crate::run::ContinuationRecord`], distinguishing
175/// "the seat's own report needed to be resumed" from "there was nothing to
176/// address" — a fixer that has never needed this stays silent here, exactly
177/// as a record predating the feature (`continuation: None`) does too.
178fn 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
188/// Commands seats' own CLIs reported running, across every node — see
189/// [`crate::run::JobRecord`]. Read-only: renders whatever `run.json` already
190/// holds, does not query anything live and does not spawn an agent.
191///
192/// A CLI this crate has no adapter for (every backend but Codex, as of this
193/// writing) never appears here at all — silence is "no evidence", not "no
194/// jobs ran", which the trailing coverage line exists to say once rather
195/// than per seat.
196///
197/// There is deliberately no "running" state anywhere in this. A command a
198/// CLI never reported finishing has no way to be told apart from one that
199/// never started at all: the CLI that would report it has, by construction,
200/// already stopped talking (killed by a timeout, or a park) by the time that
201/// question matters, so no event — real or guessed at — could ever answer
202/// it. Guessing at an unconfirmed event shape to manufacture a "running"
203/// entry is exactly the fabrication this feature must not do; recording that
204/// limit instead is what the task asks for here. `magi show`'s own
205/// seat-completion state ([`active_seats`]) still answers a related but
206/// different question honestly — "has this seat's own turn answered yet" —
207/// and stays the right place to look for that.
208fn jobs_section(state: &RunState) -> String {
209    let mut s = String::new();
210    if state.jobs.is_empty() {
211        // Not silent when it would matter: a run whose roster can actually
212        // report this (Codex, today) but has not yet says so explicitly, so
213        // "no adapter for this backend" and "nothing reported yet" are never
214        // the same blank space to a reader.
215        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
286/// Full report for one run.
287pub 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            // Neither red (nothing failed) nor plain yellow "no change" (that
312            // reads as an unexplained loss): the candidate gave a reason a
313            // human still has to check, not a claim magi itself confirmed.
314            (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            // The candidate's own account of what it did and why — the
336            // "## SUMMARY" `prompt::implement` asks for — was recorded on
337            // every run but never surfaced here, which left `magi show`
338            // silent about it even when the summary was the whole point (an
339            // implementer explaining *why* it wrote nothing, short of a
340            // verified no-op's own line above). One line, matching the
341            // house style other prose fields get in this report (see the
342            // review findings' `detail` below); the rest is in `run.json`.
343            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        // A tally with no panel (`uncontested`) must not fall through the
409        // judges/first-choice/after-votes lines below: they are written
410        // unconditionally and every one of them reads, in the words a panel
411        // that collapsed would also produce, as a run that lost its judges
412        // rather than one that never needed them.
413        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            // A build/link failure is not a verdict on the patch (a shared
511            // `CARGO_TARGET_DIR` link race looks exactly like one), so it
512            // must not read the same as a real test failure. And a deferred
513            // round is not a passed one: `e2e.is_empty()` alone cannot tell
514            // "not configured" from "skipped on purpose" apart, which is
515            // exactly why `e2e_status` exists rather than reading `e2e`
516            // directly here.
517            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                // Magi's own admission it could not get a command to run —
532                // never the same yellow/red a real attempt earns, since
533                // nothing here is evidence about the patch (see
534                // `E2eStatus::ResourceBlocked`'s own doc).
535                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            // Three distinct facts, not two: a round can be *open* (blocking
545            // findings still standing), *incomplete* (a seat never answered,
546            // so what the round says is missing input) or genuinely clean.
547            let status = if r.incomplete() {
548                yellow("incomplete")
549            } else if r.clean {
550                green("clean")
551            } else {
552                yellow("open")
553            };
554            // A missing seat must stay visible even when `warn` policy let
555            // the round gate as clean: the reader should never have to take
556            // "clean" on faith when the panel wasn't full.
557            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            // The verdict is the one thing this loop cannot derive from
577            // `blocking`/`e2e` alone: three seats can agree there is nothing
578            // blocking and still split on whether the patch is fine to
579            // proceed as-is, which is exactly the disagreement a vote exists
580            // to surface.
581            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                        // Never the same shape as "N addressed / M rejected": the
618                        // fixer's diff may well have landed (see the `fix` node's
619                        // own event), but whether it addressed anything is
620                        // unknown, not zero.
621                        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            // The aggregate label above (`e2e`) says the round's overall
644            // verdict; it never named the commands themselves, so a round
645            // with more than one `verify.e2e` command left no way to tell
646            // which one actually failed or was blocked without opening
647            // `run.json` by hand — the same gap `gate`'s own per-command
648            // listing above already closes for the final gate.
649            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            // Reconsideration only ever has entries when the round's initial
697            // votes split — an empty list here means the panel agreed the
698            // first time, same as an empty `deliberation` for judges.
699            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    // `state.gate.is_empty()` alone cannot tell "never ran" apart from "ran
838    // with nothing configured" — see `RunState::gate_status`'s own doc — so
839    // this reads the accessor rather than the raw list.
840    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            // `ok: true` here means "magi did nothing, as configured", not
866            // "landed" — a green `ok` next to a shell command reads as done,
867            // and the branch is still sitting unmerged.
868            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            // The squash caveat only applies to that one style: `--no-ff` and
881            // `--ff-only` never inherit a candidate's placeholder subject,
882            // since neither ever discards the pull request body `message`
883            // that `manual_merge_command` (graph.rs) already puts on the
884            // squash commit's `-m`.
885            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
963/// The seats currently mid-answer, for `magi show` and the raw report route.
964///
965/// Separate from [`run`] on purpose: [`run`] is printed straight after `magi
966/// run` / `magi review`'s own `execute()`, and by then this process has
967/// nothing left in flight to report; the TUI does not track daemon liveness
968/// either. Only a caller reading someone *else's* run — `magi show <id>`, or
969/// the web UI's raw-report route — needs this, and both already know how to
970/// ask whether a daemon is currently driving it.
971///
972/// `live` is [`RunState::liveness`]: whether a daemon's heartbeat currently
973/// names this run, or — absent that claim — whether `driver_pid` still
974/// answers alive. An [`ActiveSeat`](crate::run::ActiveSeat) left behind by a
975/// killed process is not lied about as running just because nobody has
976/// cleared it from disk yet — see that type's own docs for why an entry
977/// alone is not proof of anything.
978pub 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            // `active_all_overrun` is never treated as proof of death here —
998            // only ever mentioned alongside "could not confirm", since a
999            // seat legitimately overrunning its budget while the process
1000            // driving it tears the attempt down looks identical on disk (see
1001            // `RunState::active_all_overrun`'s own doc).
1002            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
1058/// Aggregate tables, for `magi stats`.
1059pub 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    /// `COLOR` is process-global, so these tests cannot run concurrently.
1162    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        // `run()` prints `state.dir()`, which reads the process-global home;
1172        // pinning it here keeps this test off the operator's real one. The
1173        // directory itself is never read, only its path printed, so nothing
1174        // needs to create or clean it up.
1175        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        // Recorded on every run (`prompt::implement`'s `## SUMMARY`), but
1232        // `run()` used to never print it at all — silent even when the
1233        // summary was the one place an implementer explained itself (e.g.
1234        // an investigation task's findings), and readable only by opening
1235        // `run.json` by hand.
1236        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        // Same spirit as `a_mode_none_merge_does_not_read_as_landed`: a run
1251        // that settled without landing anything must not be misreadable as
1252        // the ordinary failure it is not.
1253        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        // A PR closed without merging also ends at `Ready` (see `land.rs`),
1460        // and unlike the honest mode-none no-op it must keep reading as a
1461        // plain `ready` — the label exists to flag design, not every non-merge.
1462        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        // Two independent facts share this one line, and each arrived from a
1650        // different change: a seat that never answered, and a fixer whose
1651        // adoption report was lost. Rendering either must not shadow the
1652        // other, and neither may collapse into the plain `clean`/`open`
1653        // pair the line used to carry.
1654        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        // The aggregate `e2e RED` label says the round's overall verdict,
1792        // never which of several `verify.e2e` commands actually failed —
1793        // `magi show` must list each command by name, the same way it
1794        // already does for `gate`.
1795        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    /// A manual `magi run` / `magi review` claims no daemon, but its
1994    /// `driver_pid` still answers — this is `Unknown`, never `Dead`: see
1995    /// `RunState::liveness`'s own doc for why an unconfirmed process is never
1996    /// folded into "confirmed dead".
1997    #[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    /// A running `verify.e2e` / `verify.gate` task is shown alongside seats,
2017    /// with which command is currently running and how far through the list
2018    /// it is — the gap the addendum's second item names: a review-only run
2019    /// that spends minutes inside `cargo test` between reviewer answers and
2020    /// the fix's own commit had nothing at all to show for it before this.
2021    #[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        // The common case today (every backend but codex): silence, not a
2044        // clutter line repeated on every single `magi show`.
2045        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        // The three states this run actually has evidence for must read
2118        // differently from one another — never collapsed into a single
2119        // "ran" or "did not run".
2120        assert!(text.contains("completed"));
2121        assert!(text.contains("failed"));
2122        assert!(text.contains("unknown"));
2123        // Coverage limit stated once, not fabricated per seat.
2124        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}