From 84b274d3c5da05257df95953b3f5115ead5420dc Mon Sep 17 00:00:00 2001 From: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 Date: Sun, 2 Aug 2026 01:37:44 -0400 Subject: [PATCH] test(terminal): make the decrease and RIS arms assert what they claim Test-only. Five gaps, four of them found independently by two reviewers, which is the part worth noting: they are the kind that pass. The decrease arm captured `first_pending` and never asserted it, compared `first_units` to the literal `12` rather than the control's own field, and ran its geometry-persistence check on a *different* terminal -- one that had only ever grown. That last one is the sharp one. It would pass an implementation that retained the debt on shrink and dropped it on the next geometry resize, which is a real shape: `Feeder::resize` sees every resize, and only the scrollback branch is conditional. Verified by writing that mutant: the arm now fails `left: 80000, right: 4040000`, and it did not before. Two things had to be true for it to bite, and it was inert without either. It runs on the terminal that actually went shallow -> deep -> shallow, and the resize carries the *shallow* depth -- handing the debt's own value back in makes `max(debt, new)` and a plain assignment agree, so the arm cannot tell them apart. I found that second one by writing the mutant, watching it survive, and looking again. The RIS table collected all six arms before asserting but then asserted them in a loop, so a failure still printed one. Collecting stops an arm being skipped; comparing the vectors is what stops the failure being truncated. With the history term deleted the failure now shows all six rows and which ones moved, rather than the first mismatch. Every comparison in the decrease arm is now against the control's own field. A constant or geometry change has to move both sides together, or the fixture quietly starts asserting the arithmetic of the day it was written. Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> --- .../crates/buzz-terminal/tests/slicing.rs | 106 +++++++++++++----- 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/desktop/src-tauri/crates/buzz-terminal/tests/slicing.rs b/desktop/src-tauri/crates/buzz-terminal/tests/slicing.rs index 3007e23ee..32da2ecdc 100644 --- a/desktop/src-tauri/crates/buzz-terminal/tests/slicing.rs +++ b/desktop/src-tauri/crates/buzz-terminal/tests/slicing.rs @@ -593,14 +593,21 @@ fn ris_is_priced_for_both_grids_and_the_scrollback_it_walks() { observed.push((label, scrollback, term.stats().completed_work)); } } - for (label, scrollback, work) in observed { - assert_eq!( - work, - 2 * cells + (scrollback * columns) as u64, - "{label}, scrollback {scrollback}: RIS must be priced on \ - configured depth, which is the same on both grids", - ); - } + // One comparison over the whole vector, not a loop of comparisons. + // Collecting first stops an arm from being *skipped*; asserting the + // vectors is what stops a failure from being *truncated* to the first + // mismatch. Otherwise the alt-screen receipt still never prints, which + // was the point of collecting. + let expected: Vec<_> = observed + .iter() + .map(|&(label, scrollback, _)| { + (label, scrollback, 2 * cells + (scrollback * columns) as u64) + }) + .collect(); + assert_eq!( + observed, expected, + "RIS must be priced on configured depth, identically on both grids", + ); } /// CBT is charged for exactly the cells it scans -- an equality, in both @@ -982,10 +989,12 @@ fn a_scrollback_change_reprices_the_densest_atom_and_the_slicing() { // which is the fence working -- but none of them is the discriminator: // they separate only when the two depths straddle the slice floor, and // `completed_work` separates at every positive depth gap. - let debt = |from: Size, to: Size| { - let (mut term, _a) = Terminal::new(from, Fences::ALL); - term.resize(deep); - term.resize(to); + // + // Every comparison is against the fresh control's own field, never a + // literal: a constant or geometry change must move both sides together, + // or the fixture starts asserting the arithmetic of the day it was + // written. + let measure = |term: &mut Terminal| { term.reset_stats(); let mut drains = 1; let mut more = term.feed(&b"\x1bc".repeat(200)); @@ -1003,39 +1012,74 @@ fn a_scrollback_change_reprices_the_densest_atom_and_the_slicing() { term.stats().completed_work, ) }; - let shrunk = debt(shallow, shallow); - let fresh = run(shallow, None); - assert_eq!(shrunk.3, 200, "no unit may be lost on the way down either"); + // The terminal under test stays alive past its measurement, so the + // geometry arm below runs on the feeder that actually shrank rather than + // on a lookalike that only ever grew. + let (mut shrunk_term, _a) = Terminal::new(shallow, Fences::ALL); + shrunk_term.resize(deep); + shrunk_term.resize(shallow); + let shrunk = measure(&mut shrunk_term); + + let (mut fresh_term, _a) = Terminal::new(shallow, Fences::ALL); + let fresh = measure(&mut fresh_term); + + assert_eq!( + shrunk.3, fresh.3, + "no unit may be lost on the way down either" + ); assert!( - shrunk.4 > fresh.1, + shrunk.4 > fresh.4, "a feeder that has been deep must still price deep after shrinking: \ {} against a fresh shallow {}. Equality here is the signature of a \ feeder that dropped the debt, which is indistinguishable from one \ that never had it", shrunk.4, + fresh.4, + ); + assert!( + shrunk.0 <= fresh.0, + "narrower slices retire fewer atoms per drain: {} against {}", + shrunk.0, + fresh.0, + ); + assert!( + shrunk.1 >= fresh.1, + "and leave more pending after the first call: {} against {}", + shrunk.1, fresh.1, ); assert!( - shrunk.0 <= 12, - "narrower slices retire fewer atoms per drain" + shrunk.2 >= fresh.2, + "and take more drains to finish: {} against {}", + shrunk.2, + fresh.2, ); - assert!(shrunk.2 >= fresh.2, "and take more drains to do it"); - // A later resize on a different axis must not disturb the third one -- // A later resize on a different axis must not disturb the third one -- - // the split was permanent, with columns and lines tracking correctly - // while a stale depth persisted forever. - term.resize(Size { - columns: deep.columns * 2, - ..deep + // The debt survives a later resize on a different axis. Two things make + // this arm bite, and it was inert without either: + // + // * It runs on the terminal that actually went shallow -> deep -> + // shallow. A lookalike that only ever grew passes it while an + // implementation that retains on shrink and drops on the next geometry + // change fails. + // * The resize carries the *shallow* depth. Passing the debt's own value + // back in means `max(debt, new)` and a plain assignment agree, so the + // arm cannot tell them apart -- which is how it survived a mutant that + // retained only when columns and lines were unchanged. + shrunk_term.resize(Size { + columns: shallow.columns * 2, + screen_lines: shallow.screen_lines, + scrollback: shallow.scrollback, }); - term.reset_stats(); - term.feed_fully(b"c"); + shrunk_term.reset_stats(); + shrunk_term.feed_fully(b"\x1bc"); assert_eq!( - term.stats().completed_work, - 2 * (deep.columns * 2 * deep.screen_lines) as u64 - + (deep.scrollback * deep.columns * 2) as u64, - "a columns resize must keep the scrollback it was already given", + shrunk_term.stats().completed_work, + 2 * (shallow.columns * 2 * shallow.screen_lines) as u64 + + (deep.scrollback * shallow.columns * 2) as u64, + "a columns resize must keep the deep scrollback debt, not fall back \ + to the current shallow depth", ); }