Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
458 lines
18 KiB
Rust
458 lines
18 KiB
Rust
//! The per-cell executor: spawn the primed pager, replay the gesture,
|
|
//! synchronize on the recorder, judge the invariants, classify the verdict.
|
|
//!
|
|
//! ## Blocking model
|
|
//!
|
|
//! A cell's body ([`run_cell_inner`]) is host-paced end to end — PTY drains,
|
|
//! `thread::sleep` gesture gaps, finalize polling — so [`run_cell`] runs it
|
|
//! on `spawn_blocking` (driving the async session spawns via
|
|
//! `Handle::block_on`, which requires a **multi-thread** runtime) and applies
|
|
//! the per-cell hard cap with `tokio::time::timeout` on the join handle.
|
|
//! That also converts setup panics (the session preambles assert with the
|
|
//! screen contents) into a `Fail` report instead of killing the whole sweep.
|
|
//! A capped cell's blocking task cannot be aborted mid-syscall: it is left
|
|
//! to unwind on its own bounded waits (its `Drop`s kill the pager child and
|
|
//! mock server), while the sweep moves on.
|
|
//!
|
|
//! ## Harness-side invariants
|
|
//!
|
|
//! [`InvariantId::Screen`] and [`InvariantId::Quiet`] are checked here (the
|
|
//! log alone can't see the viewport or repaints — `invariants.rs` panics on
|
|
//! them by contract): I-QUIET counts frames in a post-finalize watermark
|
|
//! window, I-SCREEN replays the per-stream `applied_total`s through a
|
|
//! bottom-clamped travel simulation and compares the topmost visible marker
|
|
//! against the session baseline.
|
|
|
|
use std::path::Path;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
|
|
use super::cells::{MatrixCell, Tier};
|
|
use super::invariants::{InvariantId, InvariantResult, check_log_invariant};
|
|
use super::log::{StreamGroup, group_streams, parse_jsonl, wait_for_finalize_count};
|
|
use super::report::{CellReport, CellStatus, InvariantReport, InvariantStatus};
|
|
use super::session::{
|
|
SessionKind, WHEEL_COL, WHEEL_ROW, spawn_marker_session, topmost_visible_marker,
|
|
};
|
|
|
|
/// Transcript height for every cell: comfortably taller than the 50-row PTY
|
|
/// (the preamble's scrollable-baseline guard) with enough headroom that the
|
|
/// small gestures judged by I-SCREEN never clamp at the transcript top.
|
|
/// Deliberate travel clamping (floods can deliver hundreds of rows) is fine:
|
|
/// no log-side invariant reads the viewport.
|
|
const MARKER_COUNT: usize = 400;
|
|
|
|
/// Recorder-synchronization budget: every gesture table spans < 1.5s and a
|
|
/// stream finalizes 80ms after its last event, so 5s only trips on a real
|
|
/// wedge (or a stall so long the cell is unusable anyway).
|
|
const FINALIZE_TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
/// Post-finalize settle: consume the gesture-era PTY backlog so the quiet
|
|
/// window below counts only NEW frames (the harness parses chunks lazily in
|
|
/// `update`, not at arrival).
|
|
const PIPELINE_DRAIN: Duration = Duration::from_millis(300);
|
|
|
|
/// I-QUIET observation window after the drain + watermark reset.
|
|
const QUIET_WINDOW: Duration = Duration::from_millis(500);
|
|
|
|
/// I-QUIET allowance: a straggler cadence/finalize paint mid-pipeline may
|
|
/// land after the watermark; repaint CHURN (the A2 symptom) paints dozens.
|
|
const QUIET_MAX_FRAMES: u64 = 2;
|
|
|
|
/// Streaming-session teardown budget: the paced tail is ~7s at spawn time,
|
|
/// so the released turn completes well inside this.
|
|
const COMPLETION_TIMEOUT: Duration = Duration::from_secs(30);
|
|
|
|
/// Per-cell hard cap (spawn → verdict). The slowest legitimate cell — the
|
|
/// streaming preamble plus its post-gesture tail drain — finishes in ~20s.
|
|
const CELL_HARD_CAP: Duration = Duration::from_secs(60);
|
|
|
|
fn tier_label(tier: Tier) -> &'static str {
|
|
match tier {
|
|
Tier::Curated => "curated",
|
|
Tier::Full => "full",
|
|
}
|
|
}
|
|
|
|
/// One SGR (DECSET 1006) wheel press report at the shared in-scrollback
|
|
/// position — 0-based [`WHEEL_ROW`]/[`WHEEL_COL`] encode 1-based on the wire
|
|
/// (same bytes as the pager e2e `sgr_mouse` helper).
|
|
fn sgr_wheel_report(button: u16) -> String {
|
|
format!("\x1b[<{button};{};{}M", WHEEL_COL + 1, WHEEL_ROW + 1)
|
|
}
|
|
|
|
/// Run one matrix cell against `binary`, capturing the recorder JSONL to
|
|
/// `artifacts_dir/<cell_id>.jsonl` (kept for post-mortems). Never panics and
|
|
/// never hangs past [`CELL_HARD_CAP`]; every abnormality becomes a `Fail`
|
|
/// report with a phase note. Requires a multi-thread tokio runtime (see the
|
|
/// module docs).
|
|
pub async fn run_cell(cell: &MatrixCell, binary: &Path, artifacts_dir: &Path) -> CellReport {
|
|
let started = Instant::now();
|
|
let cell = *cell;
|
|
let log_path = artifacts_dir.join(format!("{}.jsonl", cell.id));
|
|
|
|
let outcome = match std::fs::create_dir_all(artifacts_dir)
|
|
.with_context(|| format!("create artifacts dir {}", artifacts_dir.display()))
|
|
{
|
|
Err(err) => Err(format!("artifacts setup: {err:#}")),
|
|
Ok(()) => {
|
|
let handle = tokio::runtime::Handle::current();
|
|
let (binary, inner_log) = (binary.to_path_buf(), log_path.clone());
|
|
let task = tokio::task::spawn_blocking(move || {
|
|
handle.block_on(run_cell_inner(cell, &binary, &inner_log))
|
|
});
|
|
match tokio::time::timeout(CELL_HARD_CAP, task).await {
|
|
Err(_) => Err(format!(
|
|
"hard cap: cell still running after {CELL_HARD_CAP:?} (phase unknown; \
|
|
the cell task is left to unwind on its own bounded waits)"
|
|
)),
|
|
Ok(Err(join_err)) => Err(format!("panic: {}", panic_message(join_err))),
|
|
Ok(Ok(Err(err))) => Err(format!("{err:#}")),
|
|
Ok(Ok(Ok(run))) => Ok(run),
|
|
}
|
|
}
|
|
};
|
|
|
|
let (status, invariants, streams, note) = match outcome {
|
|
Ok(run) => {
|
|
let (status, invariants) = classify(&run.outcomes, cell.xfail);
|
|
(status, invariants, run.streams, None)
|
|
}
|
|
Err(note) => (CellStatus::Fail, Vec::new(), 0, Some(note)),
|
|
};
|
|
CellReport {
|
|
cell_id: cell.id.to_owned(),
|
|
tier: tier_label(cell.tier).to_owned(),
|
|
status,
|
|
invariants,
|
|
log_path: log_path.display().to_string(),
|
|
streams,
|
|
duration_ms: started.elapsed().as_millis() as u64,
|
|
note,
|
|
}
|
|
}
|
|
|
|
fn panic_message(err: tokio::task::JoinError) -> String {
|
|
match err.try_into_panic() {
|
|
Ok(payload) => payload
|
|
.downcast_ref::<&str>()
|
|
.map(|s| (*s).to_owned())
|
|
.or_else(|| payload.downcast_ref::<String>().cloned())
|
|
.unwrap_or_else(|| "non-string panic payload".to_owned()),
|
|
Err(err) => format!("cell task failed without panicking: {err}"),
|
|
}
|
|
}
|
|
|
|
struct CellRun {
|
|
outcomes: Vec<(InvariantId, InvariantResult)>,
|
|
streams: usize,
|
|
}
|
|
|
|
async fn run_cell_inner(cell: MatrixCell, binary: &Path, log_path: &Path) -> Result<CellRun> {
|
|
// Stale-capture guard: the recorder opens its file lazily on the first
|
|
// record, so a leftover capture from a previous run would satisfy the
|
|
// finalize wait with the OLD gesture's records.
|
|
if log_path.exists() {
|
|
std::fs::remove_file(log_path)
|
|
.with_context(|| format!("setup: remove stale capture {}", log_path.display()))?;
|
|
}
|
|
let log_value = log_path
|
|
.to_str()
|
|
.context("setup: artifacts path is not UTF-8")?;
|
|
let mut env: Vec<(&str, &str)> = cell.env.to_vec();
|
|
env.push(("KIGI_SCROLL_LOG", log_value));
|
|
|
|
// Live bindings on purpose: `content` owns the mock server (and the
|
|
// streaming completion gate) — see the session module's footgun docs.
|
|
let (mut harness, content, baseline) =
|
|
spawn_marker_session(binary, cell.session, MARKER_COUNT, &env).await;
|
|
|
|
// Replay the gesture table: sleep each step's pre-delay (host-side lower
|
|
// bound — jitter only stretches gaps), then emit its report. Port of the
|
|
// pager e2e `send_wheel_sequence` loop onto `WheelStep`.
|
|
for step in cell.gesture.steps(cell.expected.ept) {
|
|
if step.pre_delay_ms > 0 {
|
|
std::thread::sleep(Duration::from_millis(step.pre_delay_ms));
|
|
}
|
|
harness
|
|
.inject_keys(sgr_wheel_report(step.button).as_bytes())
|
|
.context("gesture: inject wheel report")?;
|
|
}
|
|
|
|
wait_for_finalize_count(log_path, cell.gesture.expected_streams(), FINALIZE_TIMEOUT)
|
|
.context("gesture: finalize wait")?;
|
|
|
|
// Drain the gesture-era backlog, then watermark → the quiet window
|
|
// counts only post-finalize frames; the marker read afterwards sees the
|
|
// fully painted final viewport (I-SCREEN's input).
|
|
harness.update(PIPELINE_DRAIN);
|
|
harness.reset_timing();
|
|
harness.update(QUIET_WINDOW);
|
|
let quiet_frames = harness.frame_count();
|
|
let marker_after = topmost_visible_marker(&harness);
|
|
|
|
// Streaming teardown: the CALLER owns the gate release (session-module
|
|
// contract) — release after the gesture so the pager exits a completed
|
|
// turn, and prove the release took (the held gate is the alternative
|
|
// explanation for almost any streaming-cell wedge).
|
|
if cell.session == SessionKind::Streaming {
|
|
content.release_agent_completions();
|
|
let deadline = Instant::now() + COMPLETION_TIMEOUT;
|
|
while harness.contains_text("Responding") {
|
|
if Instant::now() >= deadline {
|
|
bail!("teardown: turn never completed after the gate release");
|
|
}
|
|
harness.update(Duration::from_millis(200));
|
|
}
|
|
}
|
|
harness.quit().context("teardown: quit pager")?;
|
|
drop(content);
|
|
|
|
// The pager exited (recorder flushed + closed), so the capture is
|
|
// complete and torn-tail-free by construction.
|
|
let records = parse_jsonl(log_path).context("verdict: parse capture")?;
|
|
let groups = group_streams(&records).context("verdict: group streams")?;
|
|
|
|
let outcomes = cell
|
|
.invariants
|
|
.iter()
|
|
.map(|&id| {
|
|
let result = if id.is_log_side() {
|
|
check_log_invariant(id, &cell.expected, &groups)
|
|
} else {
|
|
match id {
|
|
InvariantId::Screen => {
|
|
check_screen(cell.session, baseline, marker_after, &groups)
|
|
}
|
|
InvariantId::Quiet => check_quiet(quiet_frames),
|
|
_ => unreachable!("is_log_side covers every other id"),
|
|
}
|
|
};
|
|
(id, result)
|
|
})
|
|
.collect();
|
|
Ok(CellRun {
|
|
outcomes,
|
|
streams: groups.len(),
|
|
})
|
|
}
|
|
|
|
/// I-QUIET: no repaint churn after the last finalize — at most
|
|
/// [`QUIET_MAX_FRAMES`] frames land in the watermark window.
|
|
fn check_quiet(quiet_frames: u64) -> InvariantResult {
|
|
if quiet_frames > QUIET_MAX_FRAMES {
|
|
return InvariantResult::Violated {
|
|
detail: format!(
|
|
"{quiet_frames} frames painted in the {QUIET_WINDOW:?} post-finalize window \
|
|
(> {QUIET_MAX_FRAMES}) — repaint churn after the gesture ended"
|
|
),
|
|
};
|
|
}
|
|
InvariantResult::Pass
|
|
}
|
|
|
|
/// Net signed lines a stream delivered: its last flush-bearing record's
|
|
/// cumulative `applied_total` (finalize when present, else the last flush of
|
|
/// a trailing in-flight stream).
|
|
fn stream_applied(group: &StreamGroup<'_>) -> i64 {
|
|
group
|
|
.flush_bearing()
|
|
.last()
|
|
.map_or(0, |record| record.applied_total)
|
|
}
|
|
|
|
/// Replay per-stream deliveries through the viewport's clamps: `0` is the
|
|
/// bottom pin (down-deliveries there don't move — G7's point), and the
|
|
/// return is clamped to `-baseline` because travel above marker 0 pins the
|
|
/// topmost visible marker at 0.
|
|
fn simulate_clamped_travel(baseline: usize, applied: impl IntoIterator<Item = i64>) -> i64 {
|
|
let mut pos: i64 = 0;
|
|
for delta in applied {
|
|
pos = (pos + delta).min(0);
|
|
}
|
|
pos.max(-(baseline as i64))
|
|
}
|
|
|
|
/// I-SCREEN: the viewport visibly moved/clamped exactly as the recorder
|
|
/// says it should have — topmost-marker delta equals the bottom-clamped
|
|
/// replay of the per-stream `applied_total`s (up is negative, matching the
|
|
/// producer's `ScrollDirection` sign and the marker index direction).
|
|
fn check_screen(
|
|
session: SessionKind,
|
|
baseline: usize,
|
|
marker_after: Option<usize>,
|
|
groups: &[StreamGroup<'_>],
|
|
) -> InvariantResult {
|
|
if session == SessionKind::Streaming {
|
|
// The live bottom keeps growing mid-stream, so "marker delta ==
|
|
// applied" has no stable frame of reference — a cell-table bug.
|
|
return InvariantResult::Violated {
|
|
detail: "I-SCREEN attached to a streaming session (no stable baseline)".to_owned(),
|
|
};
|
|
}
|
|
let Some(after) = marker_after else {
|
|
return InvariantResult::Violated {
|
|
detail: "no marker visible after the gesture".to_owned(),
|
|
};
|
|
};
|
|
let expected =
|
|
baseline as i64 + simulate_clamped_travel(baseline, groups.iter().map(stream_applied));
|
|
if after as i64 != expected {
|
|
return InvariantResult::Violated {
|
|
detail: format!(
|
|
"topmost marker {baseline} -> {after} after the gesture, but the clamped \
|
|
replay of the streams' applied totals lands at {expected}"
|
|
),
|
|
};
|
|
}
|
|
InvariantResult::Pass
|
|
}
|
|
|
|
const XPASS_DETAIL: &str = "expected to violate (xfail) but PASSED — the pinned bug got fixed \
|
|
or the cell rotted; promote the invariant out of the xfail set";
|
|
|
|
/// Precedence: any `Fail` (non-xfail violation) fails the cell; else any
|
|
/// `XPass` fails it (fixed/rotted xfail must be promoted, not absorbed);
|
|
/// else any `XFail` marks the expected failure; else `Pass`.
|
|
fn classify(
|
|
outcomes: &[(InvariantId, InvariantResult)],
|
|
xfail: &[InvariantId],
|
|
) -> (CellStatus, Vec<InvariantReport>) {
|
|
let rows: Vec<InvariantReport> = outcomes
|
|
.iter()
|
|
.map(|(id, result)| {
|
|
let expected_to_fail = xfail.contains(id);
|
|
let (status, detail) = match (result, expected_to_fail) {
|
|
(InvariantResult::Pass, false) => (InvariantStatus::Pass, None),
|
|
(InvariantResult::Pass, true) => {
|
|
(InvariantStatus::XPass, Some(XPASS_DETAIL.to_owned()))
|
|
}
|
|
(InvariantResult::Violated { detail }, false) => {
|
|
(InvariantStatus::Fail, Some(detail.clone()))
|
|
}
|
|
(InvariantResult::Violated { detail }, true) => {
|
|
(InvariantStatus::XFail, Some(detail.clone()))
|
|
}
|
|
};
|
|
InvariantReport {
|
|
id: id.as_str().to_owned(),
|
|
status,
|
|
detail,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
let has = |status: InvariantStatus| rows.iter().any(|row| row.status == status);
|
|
let status = if has(InvariantStatus::Fail) {
|
|
CellStatus::Fail
|
|
} else if has(InvariantStatus::XPass) {
|
|
CellStatus::XPass
|
|
} else if has(InvariantStatus::XFail) {
|
|
CellStatus::XFail
|
|
} else {
|
|
CellStatus::Pass
|
|
};
|
|
(status, rows)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn violated(detail: &str) -> InvariantResult {
|
|
InvariantResult::Violated {
|
|
detail: detail.to_owned(),
|
|
}
|
|
}
|
|
|
|
/// Pins the wire bytes to the `40;12` encoding documented on
|
|
/// `WHEEL_ROW`/`WHEEL_COL`.
|
|
#[test]
|
|
fn sgr_report_encodes_one_based_wire_coords() {
|
|
assert_eq!(sgr_wheel_report(64), "\x1b[<64;40;12M");
|
|
assert_eq!(sgr_wheel_report(65), "\x1b[<65;40;12M");
|
|
}
|
|
|
|
#[test]
|
|
fn clamped_travel_models_bottom_pin_and_top_clamp() {
|
|
// G7's shape: down-deliveries at the pin don't move, the up tail does.
|
|
assert_eq!(simulate_clamped_travel(300, [10, -7]), -7);
|
|
assert_eq!(simulate_clamped_travel(300, [-5, 3]), -2);
|
|
assert_eq!(simulate_clamped_travel(300, [10]), 0);
|
|
// Down past the pin then up: the overshoot must not bank as credit.
|
|
assert_eq!(simulate_clamped_travel(300, [25, -4]), -4);
|
|
// Travel beyond the transcript top pins the topmost marker at 0.
|
|
assert_eq!(simulate_clamped_travel(30, [-500]), -30);
|
|
}
|
|
|
|
#[test]
|
|
fn quiet_allows_the_straggler_allowance_only() {
|
|
assert!(check_quiet(0).is_pass());
|
|
assert!(check_quiet(QUIET_MAX_FRAMES).is_pass());
|
|
let result = check_quiet(QUIET_MAX_FRAMES + 1);
|
|
assert!(
|
|
matches!(result, InvariantResult::Violated { ref detail } if detail.contains("churn"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn screen_rejects_streaming_sessions_and_marker_loss() {
|
|
let streaming = check_screen(SessionKind::Streaming, 100, Some(100), &[]);
|
|
assert!(
|
|
matches!(streaming, InvariantResult::Violated { ref detail } if detail.contains("streaming"))
|
|
);
|
|
let lost = check_screen(SessionKind::BottomPinned, 100, None, &[]);
|
|
assert!(
|
|
matches!(lost, InvariantResult::Violated { ref detail } if detail.contains("no marker"))
|
|
);
|
|
// Empty capture ⇒ no movement expected; a matching marker passes.
|
|
assert!(check_screen(SessionKind::BottomPinned, 100, Some(100), &[]).is_pass());
|
|
let moved = check_screen(SessionKind::BottomPinned, 100, Some(97), &[]);
|
|
assert!(matches!(moved, InvariantResult::Violated { .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn classify_precedence_fail_over_xpass_over_xfail_over_pass() {
|
|
use InvariantId::{Cap, NoDrop, Ord, SmoothCoast};
|
|
|
|
let (status, rows) = classify(&[(Ord, InvariantResult::Pass)], &[]);
|
|
assert_eq!(status, CellStatus::Pass);
|
|
assert_eq!(rows[0].status, InvariantStatus::Pass);
|
|
assert_eq!(rows[0].id, "I-ORD");
|
|
|
|
let jerk = [
|
|
(Ord, InvariantResult::Pass),
|
|
(SmoothCoast, violated("coast")),
|
|
(NoDrop, violated("dropped 74")),
|
|
];
|
|
let (status, rows) = classify(&jerk, &[SmoothCoast, NoDrop]);
|
|
assert_eq!(status, CellStatus::XFail);
|
|
assert_eq!(rows[1].status, InvariantStatus::XFail);
|
|
assert_eq!(rows[1].detail.as_deref(), Some("coast"));
|
|
|
|
// One xfail row passing flips the cell to XPass: the fixed-bug tripwire.
|
|
let half_fixed = [
|
|
(SmoothCoast, InvariantResult::Pass),
|
|
(NoDrop, violated("dropped 74")),
|
|
];
|
|
let (status, rows) = classify(&half_fixed, &[SmoothCoast, NoDrop]);
|
|
assert_eq!(status, CellStatus::XPass);
|
|
assert!(rows[0].detail.as_deref().unwrap().contains("promote"));
|
|
|
|
// A non-xfail violation dominates everything.
|
|
let broken = [
|
|
(Cap, violated("flushed 40 exceeds cap 25")),
|
|
(SmoothCoast, InvariantResult::Pass),
|
|
];
|
|
let (status, _) = classify(&broken, &[SmoothCoast]);
|
|
assert_eq!(status, CellStatus::Fail);
|
|
}
|
|
|
|
#[test]
|
|
fn tier_labels_match_the_report_vocabulary() {
|
|
assert_eq!(tier_label(Tier::Curated), "curated");
|
|
assert_eq!(tier_label(Tier::Full), "full");
|
|
}
|
|
}
|