docs(comments): rewrite comments across all crates to the guidelines

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).
This commit is contained in:
2026-07-23 16:55:39 -04:00
parent ff0fb56c67
commit a02b555e66
1458 changed files with 10729 additions and 21750 deletions
@@ -252,7 +252,7 @@ async fn bench_cell(
let mut harness = spawn_ready(binary, rows, cols, &content, surface)?;
// Image mode keeps one PNG on the pasteboard for the whole cell (the
// pager re-reads the unchanged clipboard on every Ctrl+V).
// pager re-reads the `unchanged` clipboard on every Ctrl+V).
let tmp = tempfile::tempdir().context("tempdir for the clipboard PNG")?;
if mode == Mode::Image {
let png = write_fixture_png(tmp.path())?;
@@ -135,7 +135,8 @@ async fn run() -> Result<ExitCode> {
let res = scenario.run(&mut harness, &content).await;
// Best-effort cleanup regardless of scenario outcome.
// Cleanup runs regardless of scenario outcome, so failures are reported
// below rather than leaking a live PTY.
let _ = harness.quit();
match res {
@@ -160,7 +161,6 @@ async fn run() -> Result<ExitCode> {
}
}
// Emit JSON to stdout for downstream consumption.
let json = serde_json::to_string_pretty(&results).context("serialize results")?;
println!("{json}");
@@ -1,11 +1,10 @@
//! `scroll-matrix` — scroll validation matrix sweep for kigi-tui.
//!
//! Runs matrix cells (`scroll_matrix::CELLS`) against a real pager binary in
//! a PTY, prints the per-cell verdict table, writes `report.json` into the
//! artifacts dir (next to each cell's recorder capture), and exits nonzero
//! iff any cell failed or an xfail cell passed. The curated tier also runs
//! in CI as `tests/scroll_matrix_curated.rs`; this binary is the local
//! entry point for the full sweep and for one-off cell reruns (`--filter`).
//! Runs matrix cells (`scroll_matrix::CELLS`) against a real pager binary in a
//! PTY and exits nonzero iff any cell failed or an xfail cell passed. The
//! curated tier also runs in CI as `tests/scroll_matrix_curated.rs`; this binary
//! is the local entry point for the full sweep and one-off cell reruns
//! (`--filter`).
use std::path::PathBuf;
use std::process::ExitCode;
@@ -113,8 +112,8 @@ async fn run() -> Result<ExitCode> {
Ok(ExitCode::from(exit_code(&reports)))
}
/// Run every cell, preserving table order. `jobs == 1` runs inline (the
/// representative-timing default); higher values fan out over a semaphore.
/// Reports come back in table order regardless of completion order. `jobs == 1`
/// runs inline, which keeps gesture timing representative.
async fn run_cells(
cells: &[&'static MatrixCell],
jobs: usize,
@@ -187,7 +187,7 @@ impl ContentController {
self.server.request_bodies()
}
// ── Mock storage controls (park-on-401 e2e) ────────────────────────────
// Mock storage controls (park-on-401 e2e)
/// Flip the mock `/v1/storage` 401 gate (the auth-outage window).
pub fn set_storage_unauthorized(&self, unauthorized: bool) {
@@ -9,7 +9,6 @@ use std::time::{Duration, Instant};
use crate::{ContentController, PtyHarness};
/// Pump PTY output until every label is absent from the visible screen.
pub fn wait_for_labels_absent(h: &mut PtyHarness, labels: &[&str], timeout: Duration) {
let _ = h.wait_until("screen labels to disappear", timeout, |h| {
labels.iter().all(|label| !h.contains_text(label))
@@ -19,7 +19,6 @@ use std::process::{Command, Stdio};
use anyhow::{Context, Result, bail};
/// Copy `text` to the host clipboard via `pbcopy`.
#[cfg(not(target_os = "windows"))]
pub fn pbcopy(text: &str) -> Result<()> {
let mut cmd = Command::new("pbcopy");
@@ -152,7 +151,6 @@ pub fn set_clipboard_png(path: &Path) -> Result<()> {
Ok(())
}
/// Write a small solid-color PNG under `dir` and return its path.
pub fn write_fixture_png(dir: &Path) -> Result<PathBuf> {
let path = dir.join("host_clipboard_fixture.png");
let buf: image::ImageBuffer<image::Rgba<u8>, Vec<u8>> =
@@ -2,14 +2,10 @@
//! inspection of the durable session log so reattach tests can assert on
//! what actually persisted.
//!
//! Every other leader test is single-client/single-leader; [`LeaderCluster`]
//! is the missing abstraction for "one leader, several pager clients sharing
//! its session". One [`ContentController`] gives one shared `$HOME` (hence one
//! elected leader) plus a fixed leader socket beneath its `KIGI_SHARE_DIR`; clients
//! One [`ContentController`] gives one shared `$HOME` (hence one elected
//! leader) plus a fixed leader socket beneath its `KIGI_SHARE_DIR`; clients
//! spawn with the `--leader`/`--leader-socket` flags so they all attach to the
//! SAME leader. It also exposes the leader's durable `updates.jsonl` log so a
//! reattach test can assert on the persisted, replayable turn-completion
//! records — the genuine end-to-end signal behind durable turn completion.
//! SAME leader.
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
@@ -29,8 +25,6 @@ pub struct LeaderCluster {
}
impl LeaderCluster {
/// Start the cluster: one [`ContentController`] (one shared `$HOME` =>
/// one leader) and a fixed leader socket under its `KIGI_SHARE_DIR`.
pub async fn start(rows: u16, cols: u16) -> Result<Self> {
let content = ContentController::start()
.await
@@ -50,21 +44,19 @@ impl LeaderCluster {
})
}
/// Spawn the leader-electing client (`--leader --leader-socket <S>` plus
/// `extra_args`); it starts a fresh session and brings up the leader.
/// Spawn the client that starts a fresh session and brings up the leader.
pub fn spawn_leader(&self, extra_args: &[&str]) -> Result<PtyHarness> {
self.spawn_client(&[], extra_args)
}
/// Attach another client that resumes the shared session through the SAME
/// leader (`--leader --leader-socket <S> --resume` plus `extra_args`).
/// leader.
pub fn attach(&self, extra_args: &[&str]) -> Result<PtyHarness> {
self.spawn_client(&["--resume"], extra_args)
}
/// Spawn a client wired to the shared leader socket. `mode_args` carries
/// the per-role flag (`--resume` for attachers); `extra_args` is the
/// caller's.
/// `mode_args` carries the per-role flag (`--resume` for attachers);
/// `extra_args` is the caller's.
fn spawn_client(&self, mode_args: &[&str], extra_args: &[&str]) -> Result<PtyHarness> {
let socket = self.socket.to_str().context("socket path is utf-8")?;
let mut args: Vec<&str> = vec!["--leader", "--leader-socket", socket];
@@ -74,22 +66,18 @@ impl LeaderCluster {
.context("spawn pager client on shared leader")
}
/// The shared content controller (mock inference server + sandbox env).
pub fn content(&self) -> &ContentController {
&self.content
}
/// The cluster's sessions root: `KIGI_SHARE_DIR/sessions` (layout below is
/// `sessions/<encoded-cwd>/<session-id>/updates.jsonl`).
/// Root of `sessions/<encoded-cwd>/<session-id>/updates.jsonl`.
fn sessions_dir(&self) -> PathBuf {
self.content.home().join(".kigi").join("sessions")
}
/// The session-update payload of every record across every `updates.jsonl`
/// under the cluster's [`sessions_dir`](Self::sessions_dir) — i.e. the
/// `params.update` object of each persisted envelope line, so a caller can
/// match on its `sessionUpdate` tag directly. Scans ALL sessions under the
/// cluster (fine for the single-session clusters these tests build).
/// The `params.update` payload of every record across every
/// `updates.jsonl` under the cluster, so a caller can match on the
/// `sessionUpdate` tag directly.
///
/// Infallible by design: a file that vanishes mid-walk, or whose appended
/// tail tore across a multi-byte UTF-8 boundary (so `read_to_string`
@@ -99,7 +87,6 @@ impl LeaderCluster {
collect_updates_files(&self.sessions_dir(), &mut files);
let mut out = Vec::new();
for file in files {
// Skip a vanished file or a torn multi-byte tail; the next poll retries.
if let Ok(text) = std::fs::read_to_string(&file) {
out.extend(parse_update_payloads(&text));
}
@@ -107,10 +94,8 @@ impl LeaderCluster {
out
}
/// Poll [`session_updates`](Self::session_updates) until a record with
/// `sessionUpdate == "turn_completed"` appears, returning that (inner)
/// update payload, or error on timeout. Scans ALL sessions under the
/// cluster (fine for the single-session clusters these tests build).
/// Poll [`session_updates`](Self::session_updates) until a
/// `turn_completed` record appears, or error on timeout.
pub fn wait_for_turn_completed(&self, timeout: Duration) -> Result<Value> {
let deadline = Instant::now() + timeout;
loop {
@@ -140,18 +125,14 @@ impl LeaderCluster {
}
}
/// Whether a session-update payload is a `turn_completed` terminal.
fn is_turn_completed(update: &Value) -> bool {
update.get("sessionUpdate").and_then(Value::as_str) == Some("turn_completed")
}
/// Parse the `params.update` payload out of each non-blank line of an
/// `updates.jsonl` body, assuming the enveloped on-disk shape current sessions
/// always write (`{..,"params":{"update":{..}}}`). A line that is blank, fails
/// to parse (a torn trailing line that is still valid UTF-8), or carries no
/// `params.update` is skipped — never failing the batch. (A torn *multi-byte*
/// tail instead fails the file read upstream, skipping the whole file for that
/// poll; see [`LeaderCluster::session_updates`].)
/// Parse the `params.update` payload out of each line of an `updates.jsonl`
/// body, whose on-disk shape is `{..,"params":{"update":{..}}}`. A line that is
/// blank, fails to parse (a torn trailing line that is still valid UTF-8), or
/// carries no `params.update` is skipped rather than failing the batch.
fn parse_update_payloads(text: &str) -> Vec<Value> {
text.lines()
.filter_map(|line| {
@@ -200,7 +200,7 @@ impl PtyHarness {
Self::new_in_dir(binary, rows, cols, extra_args, &env_refs, cwd)
}
// ── PTY control ──────────────────────────────────────────────────
// PTY control
/// Inject raw key bytes into the PTY.
pub fn inject_keys(&mut self, keys: &[u8]) -> Result<()> {
@@ -214,7 +214,7 @@ impl PtyHarness {
Ok(())
}
// ── Update: receive PTY output inline → feed both parsers ────────
// Update: receive PTY output inline → feed both parsers
/// Receive PTY output for up to `timeout`, feeding each chunk to both
/// the screen state tracker and the frame timing parser as it arrives.
@@ -273,7 +273,7 @@ impl PtyHarness {
self.pty.is_running()
}
// ── Screen state queries ─────────────────────────────────────────
// Screen state queries
/// Return structured plain-text screen contents.
pub fn screen_output(&self) -> ScreenOutput {
@@ -463,7 +463,7 @@ impl PtyHarness {
std::fs::write(path, out).with_context(|| format!("write cast {}", path.display()))
}
// ── Scrollback queries (minimal mode commits blocks into native history) ──
// Scrollback queries (minimal mode commits blocks into native history)
/// The terminal's scrollback history as text (oldest line first).
pub fn scrollback_text(&self) -> String {
@@ -542,7 +542,7 @@ impl PtyHarness {
self.screen.cursor_position()
}
// ── Frame timing queries ─────────────────────────────────────────
// Frame timing queries
/// Return all recorded frame timings.
pub fn frame_timings(&self) -> &[FrameTiming] {
@@ -564,7 +564,7 @@ impl PtyHarness {
self.timing.reset();
}
// ── Lifecycle ────────────────────────────────────────────────────
// Lifecycle
/// Send 'q' and wait for the child process to exit (5s timeout, then kill).
pub fn quit(&mut self) -> Result<()> {
@@ -37,7 +37,8 @@ pub struct PtyController {
child: Box<dyn portable_pty::Child + Send>,
writer: Box<dyn Write + Send>,
reader_rx: mpsc::Receiver<Vec<u8>>,
#[allow(dead_code)] // Kept alive to hold the PTY open; used by resize().
// Kept alive to hold the PTY open; used by resize().
#[allow(dead_code)]
master: Box<dyn portable_pty::MasterPty + Send>,
}
@@ -83,7 +83,7 @@ impl BenchResults {
}
}
// ── Baseline comparison ────────────────────────────────────────────────────
// Baseline comparison
/// Regression threshold: fail if a scenario's p99 frame time grows by more
/// than this fraction (0.15 = 15%). Matches the RFC's proposal.
@@ -45,8 +45,6 @@ fn all_user_message_blobs(content: &ContentController) -> Vec<String> {
.collect()
}
/// Mid-turn queue via Enter, then empty Enter cancels the running turn and
/// runs that row as the next turn (cancel-and-send).
pub async fn assert_empty_enter_force_sends_top_queued() -> Result<()> {
let content = ContentController::start()
.await
@@ -79,13 +77,11 @@ pub async fn assert_empty_enter_force_sends_top_queued() -> Result<()> {
.context("queued text visible")?;
harness.inject_keys(b"\r").context("empty Enter send-now")?;
// Cancel-and-send: the shell cancels turn 1 (its held completion is
// irrelevant — the abort wins) and promotes the row to run as turn 2.
// Release the gate so any completion race resolves rather than hangs.
// Release the gate so a completion racing the abort resolves rather than
// hangs.
content.release_agent_completions();
// The promoted row renders as a standard user prompt block (" " prefix
// distinguishes the committed block from the prefix-less queue row) with
// the new turn's reply below it.
// The " " prefix distinguishes the committed prompt block from the
// prefix-less queue row.
harness
.wait_for_text(
"\u{276F} please also check the logs",
@@ -96,8 +92,6 @@ pub async fn assert_empty_enter_force_sends_top_queued() -> Result<()> {
.wait_for_text("TURNTWO", Duration::from_secs(40))
.context("promoted turn reply")?;
// A send-now cancel is silent: no "Turn cancelled by user" marker may
// appear between the partial turn-1 output and the promoted prompt.
if harness.contains_text("Turn cancelled by user") {
bail!(
"send-now cancel must not render a cancelled marker\n{}",
@@ -12,8 +12,7 @@ const IDLE_WINDOW: Duration = Duration::from_secs(3);
pub async fn run(harness: &mut PtyHarness, _content: &ContentController) -> Result<BenchResults> {
wait_for_welcome(harness).await?;
// Let the splash screen animation settle (some of the pager's intro
// screens do a brief fade/animation).
// Let the splash screen's fade animation settle before timing idle frames.
harness.update(Duration::from_secs(1));
harness.reset_timing();
@@ -1,8 +1,6 @@
//! `large_codeblock` — render a large syntax-highlighted Rust code block
//! and scroll through it.
//!
//! What it stresses: `syntect` highlighting cache, wrapping of long source
//! lines, ScratchBuffer copy for a single oversized entry.
//! `large_codeblock` — render a large syntax-highlighted Rust code block and
//! scroll through it. Stresses the `syntect` highlighting cache, wrapping of
//! long source lines, and the ScratchBuffer copy for a single oversized entry.
use std::time::{Duration, Instant};
@@ -1,8 +1,7 @@
//! `mixed_interaction` — scroll while streaming. The real-world worst case.
//!
//! What it stresses: simultaneous cache invalidation (from streaming) and
//! full viewport re-render (from scrolling). Surfaces `dirty_heights` /
//! scroll-offset interactions.
//! `mixed_interaction` — scroll while streaming, the real-world worst case.
//! Stresses simultaneous cache invalidation (from streaming) and full viewport
//! re-render (from scrolling), surfacing `dirty_heights` / scroll-offset
//! interactions.
use std::time::{Duration, Instant};
@@ -16,8 +15,7 @@ const SCROLL_KEYS: usize = 80;
const KEY_INTERVAL: Duration = Duration::from_millis(40);
pub async fn run(harness: &mut PtyHarness, content: &ContentController) -> Result<BenchResults> {
// Same payload shape as streaming_render, but we scroll through it
// while it's still arriving.
// Same payload shape as `streaming_render`.
let mut body = String::from("mixed-bench ");
for i in 0..TARGET_WORDS {
body.push_str("tok");
@@ -21,7 +21,6 @@ pub mod resize_storm;
pub mod scroll_stress;
pub mod streaming_render;
/// Enumerates every benchmark scenario that can be dispatched by name.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Scenario {
@@ -40,7 +39,6 @@ pub enum Scenario {
}
impl Scenario {
/// Every scenario, in dispatch order.
pub const ALL: &'static [Scenario] = &[
Scenario::ScrollStress,
Scenario::StreamingRender,
@@ -62,7 +60,6 @@ impl Scenario {
}
}
/// Dispatch to the scenario implementation.
pub async fn run(
self,
harness: &mut PtyHarness,
@@ -79,11 +76,10 @@ impl Scenario {
}
}
/// Wait for the pager to render the initial welcome screen. All scenarios
/// that prompt / stream content rely on the pager being past startup.
/// Scenarios that prompt or stream content require the pager to be past startup.
pub(crate) async fn wait_for_welcome(harness: &mut PtyHarness) -> Result<()> {
// Menu label on the normal welcome (and gate menus): capital Q — see
// `kigi-tui` `views/welcome/mod.rs` (`"Quit"` in `render_menu`).
// Capital-Q menu label appears on the normal welcome and on gate menus — see
// `render_menu` in `kigi-tui` `views/welcome/mod.rs`.
harness
.wait_for_text("Quit", Duration::from_secs(15))
.map_err(|e| anyhow::anyhow!("pager failed to reach welcome screen: {e}"))
@@ -7,8 +7,8 @@
//! no pager-side disk logic. Approving then leaves plan mode and starts the
//! implement turn.
//!
//! This FAILS without the shell re-park (PR2 product change): no reverse-request
//! reaches the resumed pager, so no approval chrome appears.
//! Without the shell re-park no reverse-request reaches the resumed pager, so no
//! approval chrome appears and this scenario fails.
use std::path::Path;
use std::time::Duration;
@@ -21,8 +21,8 @@ use crate::{ContentController, PtyHarness, pager_binary};
const DEFAULT_ROWS: u16 = 50;
const DEFAULT_COLS: u16 = 120;
const WELCOME_TIMEOUT: Duration = Duration::from_secs(20);
/// Distinct per-turn sentinels: turn 1 seeds the session before quit; turn 2 is
/// the implement turn the shell injects after the resumed approval is approved.
/// Turn 1 seeds the session before quit; turn 2 is the implement turn the shell
/// injects once the resumed approval is approved.
const SETUP_SENTINEL: &str = "GBT3703SETUP";
const IMPLEMENT_SENTINEL: &str = "GBT3703IMPLEMENTED";
@@ -35,15 +35,12 @@ const PLAN_BODY: &str = "\
3. Resume and expect restored approval chrome
";
/// Regression: the shell re-parks `exit_plan_mode` on resume; pressing
/// approve leaves plan mode and starts the implement turn.
pub async fn assert_plan_approval_restored_after_resume() -> Result<()> {
let content = ContentController::start()
.await
.context("start ContentController")?;
// One response per agent turn (FIFO, 2+-tool requests only — aux requests
// never steal one). Turn 1 is consumed by the first pager; turn 2 by the
// implement turn the shell starts after approval.
// One response per agent turn, FIFO; only 2+-tool requests draw from the
// queue, so aux requests never steal a turn.
content.set_turns([
format!("{SETUP_SENTINEL}: drafted a plan for the user to review."),
format!("{IMPLEMENT_SENTINEL}: implementing the approved plan."),
@@ -90,10 +87,8 @@ pub async fn assert_plan_approval_restored_after_resume() -> Result<()> {
)
.context("spawn resumed pager")?;
// The shell re-parks `exit_plan_mode` on resume, so approval chrome can open
// immediately and cover chat history. Prefer the chrome markers (product
// signal) over SETUP_SENTINEL, which may not be visible under the plan viewer.
// Without the shell re-park this times out.
// Approval chrome can open immediately and cover chat history, so key on the
// chrome markers rather than SETUP_SENTINEL, which the plan viewer may hide.
resumed
.wait_for_text("request changes", WELCOME_TIMEOUT)
.context("restored approval 'request changes' after --continue")?;
@@ -104,8 +99,8 @@ pub async fn assert_plan_approval_restored_after_resume() -> Result<()> {
if !screen.contains("approve") {
bail!("expected approval primary action after resume\n{screen}");
}
// History was seeded before quit; plan body from disk is a stronger signal
// that the session was restored when chrome already covers the transcript.
// Any one of these proves the session was restored: with chrome covering the
// transcript the plan body from disk is often the only visible evidence.
if !screen.contains("GBT3703Repro")
&& !screen.contains(SETUP_SENTINEL)
&& !screen.contains("Seed plan file on disk")
@@ -116,7 +111,6 @@ pub async fn assert_plan_approval_restored_after_resume() -> Result<()> {
bail!("pager panicked\n{screen}");
}
// Approve: the shell leaves plan mode and injects the implement turn.
resumed.inject_keys(b"a").context("press 'a' to approve")?;
resumed
.wait_for_text(IMPLEMENT_SENTINEL, Duration::from_secs(30))
@@ -126,9 +120,8 @@ pub async fn assert_plan_approval_restored_after_resume() -> Result<()> {
Ok(())
}
/// Mark the persisted session as having a parked plan approval: write `plan.md`
/// and flip `awaiting_plan_approval` to `true` in `plan_mode.json` for every
/// session dir under the sandbox home.
/// Parks a plan approval in every session dir under the sandbox home, since the
/// id of the session the first pager created is not known here.
fn seed_parked_approval(home: &Path) -> Result<usize> {
let sessions_root = home.join(".kigi").join("sessions");
if !sessions_root.is_dir() {
@@ -163,12 +156,9 @@ fn seed_parked_approval(home: &Path) -> Result<usize> {
Ok(seeded)
}
/// Round-trip the shell-written `plan_mode.json` and flip `awaiting_plan_approval`
/// to `true`, preserving every other field. Falls back to a fresh Active
/// snapshot if the shell wrote nothing. The shape mirrors
/// `kigi_shell::session::plan_mode::PlanModeSnapshot`; we only touch the one
/// field (robust to schema growth) rather than depend on the heavy shell crate
/// from this test-only harness.
/// The shape mirrors `kigi_shell::session::plan_mode::PlanModeSnapshot`, but this
/// test-only harness edits it as raw JSON rather than pull in the heavy shell
/// crate; preserving unknown fields keeps that decoupling safe as the schema grows.
fn write_awaiting_plan_mode(path: &Path) -> Result<()> {
let mut value: serde_json::Value = std::fs::read_to_string(path)
.ok()
@@ -18,7 +18,6 @@ pub async fn run(harness: &mut PtyHarness, _content: &ContentController) -> Resu
harness.reset_timing();
let start = Instant::now();
// Oscillate between a narrow and a wide layout.
for i in 0..RESIZES {
let (rows, cols) = if i % 2 == 0 { (35, 100) } else { (55, 160) };
harness.resize(rows, cols)?;
@@ -19,7 +19,6 @@ pub async fn run(harness: &mut PtyHarness, content: &ContentController) -> Resul
// 1. Prime the mock server with a large markdown response.
content.set_response(long_markdown_response(LINES));
// 2. Wait for the pager's splash screen.
wait_for_welcome(harness).await?;
// 3. Submit a prompt so the mock inference server returns the big response.
@@ -18,15 +18,13 @@ pub async fn run(harness: &mut PtyHarness, content: &ContentController) -> Resul
wait_for_welcome(harness).await?;
// Kick off the streamed response.
harness.inject_keys(b"stream\r")?;
// Wait for first delta to hit the screen so we're measuring the
// Wait for the first delta to hit the screen so the measurement covers the
// steady-state streaming path, not startup latency.
harness.wait_for_text("stream-bench", Duration::from_secs(20))?;
harness.reset_timing();
// Collect frames during the streaming window — the mock server paces
// itself naturally via HTTP/SSE; we just let the pipe drain.
// No pacing needed here: the mock server paces the stream over HTTP/SSE.
let start = Instant::now();
while start.elapsed() < STREAM_WINDOW {
harness.update(Duration::from_millis(100));
@@ -41,8 +39,8 @@ pub async fn run(harness: &mut PtyHarness, content: &ContentController) -> Resul
fn build_response(words: usize) -> String {
let mut s = String::with_capacity(words * 10);
// Sentinel token that wait_for_text keys on — guaranteed to appear
// near the very start of the stream.
// Leading sentinel that `wait_for_text` keys on, so it lands in the first
// few deltas of the stream.
s.push_str("stream-bench ");
for i in 0..words {
s.push_str("word");
@@ -1,4 +1,4 @@
//! Layer 2a: Screen state tracking via `alacritty_terminal` (ptyctl).
//! Layer 2a: screen state tracking via `alacritty_terminal` (ptyctl).
//!
//! Parses raw PTY output through a headless terminal emulator and provides
//! queries for what the user would see on screen.
@@ -6,8 +6,6 @@
use ptyctl::styled::StyledLine;
use ptyctl::term::{ScreenOpts, ScreenOutput, SessionListener, Terminal};
/// Tracks the virtual terminal screen state by feeding raw PTY output
/// through an `alacritty_terminal`-based headless terminal (via ptyctl).
pub struct ScreenTracker {
terminal: Terminal,
/// Receives terminal-generated replies (cursor-position reports, device
@@ -18,7 +16,6 @@ pub struct ScreenTracker {
}
impl ScreenTracker {
/// Create a new tracker for a terminal with the given dimensions.
pub fn new(rows: u16, cols: u16) -> Self {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let listener = SessionListener::new(tx);
@@ -28,14 +25,11 @@ impl ScreenTracker {
}
}
/// Feed raw PTY output bytes into the terminal emulator.
pub fn feed(&mut self, bytes: &[u8]) {
self.terminal.feed(bytes);
}
/// Drain any terminal-generated replies queued while parsing fed input
/// (cursor-position reports answering `ESC[6n`, device attributes, color
/// queries, …), concatenated in order. Empty when nothing was queued.
/// Replies queued while parsing fed input, concatenated in order.
///
/// These MUST be written back to the PTY or programs that probe the
/// terminal will hang or time out — most relevant here, the inline
@@ -51,23 +45,22 @@ impl ScreenTracker {
out
}
/// Return structured screen contents (no escape codes).
/// Structured screen contents, with escape codes stripped.
pub fn output(&self) -> ScreenOutput {
self.terminal.screen_content(&ScreenOpts::default())
}
/// Return the full text contents of the screen (no escape codes).
/// Full screen text, with escape codes stripped.
pub fn contents(&self) -> String {
self.output().lines.join("\n")
}
/// Check whether the screen contains the given text substring.
pub fn contains(&self, text: &str) -> bool {
self.contents().contains(text)
}
/// Return the current cursor position as `(row, col)` (0-indexed, matching
/// the original vt100 convention used by existing tests).
/// Cursor position as `(row, col)`, 0-indexed to match the vt100
/// convention the tests are written against.
pub fn cursor_position(&self) -> (u16, u16) {
let pos = self.terminal.cursor_position();
// ptyctl cursor is 1-indexed; the harness API is 0-indexed.
@@ -77,35 +70,32 @@ impl ScreenTracker {
)
}
/// Resize the virtual terminal to new dimensions.
pub fn resize(&mut self, rows: u16, cols: u16) {
self.terminal.resize(cols, rows);
}
/// Return the full screen with style information for visual artifacts.
pub fn styled(&self) -> Vec<StyledLine> {
self.terminal.screen_styled(&ScreenOpts::default())
}
/// Render the current screen as an HTML document.
pub fn html(&self) -> String {
self.terminal.screen_html(&ScreenOpts::default())
}
/// Access the underlying ptyctl `Terminal` for advanced queries
/// (styled output, scrollback, terminal modes, etc.).
/// Escape hatch for queries this wrapper does not expose (scrollback
/// details, terminal modes, ).
pub fn terminal(&self) -> &Terminal {
&self.terminal
}
/// Number of lines in the terminal's scrollback history — content that has
/// scrolled *above* the visible screen. This is where minimal mode's
/// committed conversation blocks land (printed via `insert_before`).
/// Number of history lines that have scrolled *above* the visible screen.
/// This is where minimal mode's committed conversation blocks land
/// (printed via `insert_before`).
pub fn scrollback_count(&self) -> usize {
self.terminal.scrollback_count()
}
/// The full scrollback history as text, oldest line first.
/// Scrollback history as text, oldest line first.
pub fn scrollback_text(&self) -> String {
let n = self.terminal.scrollback_count();
self.terminal
@@ -116,8 +106,7 @@ impl ScreenTracker {
.join("\n")
}
/// Scrollback history plus the visible screen, joined oldest→newest:
/// everything a user could see by scrolling up. Minimal-mode committed
/// Scrollback plus visible screen, oldest→newest. Minimal-mode committed
/// content may be in either region depending on how much has accumulated,
/// so assertions on committed output should use this.
pub fn full_text(&self) -> String {
@@ -130,7 +119,6 @@ impl ScreenTracker {
}
}
/// Whether scrollback + visible screen contains `text`.
pub fn full_contains(&self, text: &str) -> bool {
self.full_text().contains(text)
}
@@ -140,36 +128,28 @@ impl ScreenTracker {
mod tests {
use super::*;
/// Lines pushed above a small screen must be readable via the scrollback
/// helpers — the property minimal-mode e2e tests rely on to assert that a
/// committed block reached native scrollback.
#[test]
fn scrolled_off_lines_are_captured_by_scrollback_helpers() {
// 3-row screen; print 8 numbered lines so the first ones scroll off.
let mut s = ScreenTracker::new(3, 20);
for i in 1..=8 {
s.feed(format!("line{i}\r\n").as_bytes());
}
// The earliest lines are no longer on the visible screen…
assert!(
!s.contains("line1"),
"line1 should have scrolled off-screen"
);
// …but they are in scrollback, and full_text sees everything.
assert!(s.scrollback_count() >= 5, "expected scrolled-off history");
assert!(s.scrollback_text().contains("line1"));
assert!(s.full_contains("line1"));
assert!(s.full_contains("line8"));
}
/// A DSR cursor-position query (`ESC[6n`) must produce a forwardable reply
/// (a CPR `ESC[<row>;<col>R`) — the mechanism minimal-mode tests rely on so
/// the inline viewport's startup cursor query completes. Without forwarding,
/// `--minimal` silently downgrades to full-screen inline.
/// Without a forwardable CPR reply the inline viewport's startup cursor
/// query never completes and `--minimal` silently downgrades to
/// full-screen inline.
#[test]
fn drain_responses_answers_cursor_position_query() {
let mut s = ScreenTracker::new(24, 80);
// Nothing queued before any query is fed.
assert!(s.drain_responses().is_empty());
s.feed(b"\x1b[6n");
@@ -18,8 +18,7 @@ const SGR_LEFT_BUTTON: u16 = 0;
const SGR_MIDDLE_BUTTON: u16 = 1;
const SGR_RIGHT_BUTTON: u16 = 2;
const SGR_DRAG_BUTTON: u16 = 32;
/// SGR wheel button codes (bit 6 / +64 marks a wheel event). Public so PTY
/// tests share this single definition instead of respelling 64/65.
/// SGR wheel button codes (bit 6 / +64 marks a wheel event).
pub const SGR_SCROLL_UP: u16 = 64;
pub const SGR_SCROLL_DOWN: u16 = 65;
@@ -38,10 +37,8 @@ pub struct ScriptedScenario {
pub terminal: TerminalConfig,
#[serde(default)]
pub environment: EnvironmentConfig,
/// Optional ephemeral workspace materialized into a temp dir and used as the
/// pager's cwd. Lets a scenario exercise repo-local behavior — e.g. the
/// folder-trust prompt, which only renders when `cwd` has a repo-local
/// config (`.mcp.json`). `None` inherits the test process cwd.
/// Ephemeral workspace used as the pager's cwd; `None` inherits the test
/// process cwd.
#[serde(default)]
pub workspace: Option<WorkspaceConfig>,
#[serde(default)]
@@ -51,7 +48,6 @@ pub struct ScriptedScenario {
}
impl ScriptedScenario {
/// Load a scenario from JSON.
pub fn from_json_file(path: &Path) -> Result<Self> {
let text = fs::read_to_string(path)
.with_context(|| format!("read scenario file {}", path.display()))?;
@@ -59,7 +55,6 @@ impl ScriptedScenario {
.with_context(|| format!("parse scenario JSON {}", path.display()))
}
/// Load a scenario from YAML.
pub fn from_yaml_file(path: &Path) -> Result<Self> {
let text = fs::read_to_string(path)
.with_context(|| format!("read scenario file {}", path.display()))?;
@@ -67,7 +62,6 @@ impl ScriptedScenario {
.with_context(|| format!("parse scenario YAML {}", path.display()))
}
/// Load a scenario from JSON or YAML based on file extension.
pub fn from_file(path: &Path) -> Result<Self> {
match path.extension().and_then(|ext| ext.to_str()) {
Some("yaml" | "yml") => Self::from_yaml_file(path),
@@ -86,7 +80,6 @@ impl ScriptedScenario {
}
}
/// Terminal dimensions for a scripted run.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerminalConfig {
#[serde(default = "default_rows")]
@@ -113,7 +106,6 @@ impl Default for TerminalConfig {
}
}
/// Environment filters and extra environment variables for a scenario.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EnvironmentConfig {
/// Optional OS allow-list. Values are Rust `std::env::consts::OS` strings
@@ -124,10 +116,8 @@ pub struct EnvironmentConfig {
/// `std::env::consts::ARCH` strings such as `aarch64` or `x86_64`.
#[serde(default)]
pub arch: Vec<String>,
/// Additional env vars set on the pager process.
#[serde(default)]
pub env: Vec<EnvVar>,
/// Extra CLI args passed to the pager binary.
#[serde(default)]
pub args: Vec<String>,
/// Optional `config.toml` written into the run's isolated `$KIGI_SHARE_DIR`
@@ -144,7 +134,6 @@ impl EnvironmentConfig {
}
}
/// One environment variable assignment.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvVar {
pub key: String,
@@ -216,11 +205,9 @@ impl Default for MockConfig {
}
}
/// A single executable scenario step.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum ScenarioStep {
/// Wait until the screen contains text.
WaitForText {
text: String,
#[serde(default = "default_wait_timeout_ms")]
@@ -230,7 +217,7 @@ pub enum ScenarioStep {
AssertContains { text: String },
/// Assert text is absent from the current screen.
AssertNotContains { text: String },
/// Type literal text into the TUI.
/// Type literal text, not the key notation [`ScenarioStep::Keys`] parses.
TypeText { text: String },
/// Inject keys using ptyctl notation, for example `<Enter>`, `<Esc>`, `jj`.
Keys { keys: String },
@@ -257,11 +244,10 @@ pub enum ScenarioStep {
#[serde(default)]
button: MouseButton,
},
/// Simulate a double-click at one coordinate.
/// Coordinates are 0-indexed.
DoubleClick { row: u16, col: u16 },
/// Simulate a triple-click at one coordinate.
/// Coordinates are 0-indexed.
TripleClick { row: u16, col: u16 },
/// Simulate mouse-wheel scrolling at a coordinate.
Scroll {
row: u16,
col: u16,
@@ -280,7 +266,6 @@ pub enum ScenarioStep {
Drag { from: MousePoint, to: MousePoint },
/// Alias for drag used by text-selection scenarios.
SelectText { from: MousePoint, to: MousePoint },
/// Click visible text by finding it on screen.
ClickText {
text: String,
#[serde(default)]
@@ -288,13 +273,11 @@ pub enum ScenarioStep {
#[serde(default)]
button: MouseButton,
},
/// Double-click visible text by finding it on screen.
DoubleClickText {
text: String,
#[serde(default)]
occurrence: usize,
},
/// Triple-click visible text by finding it on screen.
TripleClickText {
text: String,
#[serde(default)]
@@ -315,7 +298,6 @@ pub enum ScenarioStep {
DropTextAt { target: TargetLocator, text: String },
/// Simulate drag/drop of image files onto the prompt as terminal paste payload.
DropImagesPrompt { images: Vec<String> },
/// Trigger a copy shortcut/key sequence from the TUI.
Copy {
#[serde(default = "default_copy_keys")]
keys: String,
@@ -359,7 +341,7 @@ pub enum ScenarioStep {
/// a Mermaid diagram) was rendered as text, never transmitted as an inline
/// image.
AssertNoKittyGraphics {},
/// Assert that a submitted request included at least this many image payloads.
/// Assert a submitted request carried at least `min` image payloads.
AssertRequestImageCount { min: usize },
/// Assert every submitted image has inline bytes, the expected MIME type, and decodes.
AssertInlineImages {
@@ -473,7 +455,6 @@ pub enum TargetLocator {
},
}
/// Runner configuration for scripted scenarios.
#[derive(Debug, Clone)]
pub struct ScriptedRunConfig {
pub binary: PathBuf,
@@ -563,9 +544,7 @@ impl ScriptedScenarioRunner {
.map(String::as_str)
.collect();
// Materialize an optional ephemeral workspace (temp dir + files + git
// init) and run the pager there. Bound for the whole run so the dir
// outlives the pager process; `None` inherits the test process cwd.
// Bound for the whole run so the temp dir outlives the pager process.
let workspace_dir = match scenario.workspace.as_ref() {
Some(ws) => Some(materialize_workspace(ws)?),
None => None,
@@ -652,17 +631,13 @@ impl ScriptedScenarioRunner {
}
}
/// Create a temp dir for a scenario [`WorkspaceConfig`]: write its files
/// (creating parent dirs) and optionally `git init` it. The returned `TempDir`
/// must be held for the whole run so the directory outlives the pager process.
/// The returned `TempDir` must be held for the whole run so the directory
/// outlives the pager process.
fn materialize_workspace(workspace: &WorkspaceConfig) -> Result<tempfile::TempDir> {
let dir = tempfile::tempdir().context("create scenario workspace temp dir")?;
for (rel_path, contents) in &workspace.files {
// Fail closed: a `files` key must be a relative path that stays inside
// the workspace. Reject absolute paths and any root/prefix/`..`
// component so a scenario can never write outside the tempdir. This is
// author-controlled test YAML (not a security boundary), but it's the
// shared materialization path, so guard it.
// Fail closed: reject absolute paths and any root/prefix/`..` component
// so a scenario can never write outside the tempdir.
let rel = Path::new(rel_path);
if rel.is_absolute()
|| rel.components().any(|c| {
@@ -685,9 +660,7 @@ fn materialize_workspace(workspace: &WorkspaceConfig) -> Result<tempfile::TempDi
if workspace.git_init {
// A real repo root makes `workspace_key` / repo-local discovery
// deterministic regardless of where the system temp dir lives. Shelled
// out (not `git2`) because this harness crate has no `git2` dependency;
// the subprocess is careful — nulled streams + `status.success()` check
// + `bail!` on failure.
// out (not `git2`) because this harness crate has no `git2` dependency.
let status = std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(dir.path())
@@ -2028,15 +2001,13 @@ mod tests {
fn workspace_rejects_paths_outside_the_tempdir() {
use std::collections::BTreeMap;
// A normal relative key (the folder-trust scenario's own `.mcp.json`)
// is accepted.
let ok = WorkspaceConfig {
git_init: false,
files: BTreeMap::from([(".mcp.json".to_string(), "{}".to_string())]),
};
assert!(materialize_workspace(&ok).is_ok());
// Absolute and `..`-traversing keys are rejected before any write.
// Rejected before any write.
for bad in ["/etc/evil", "../escape", "sub/../../escape"] {
let ws = WorkspaceConfig {
git_init: false,
@@ -2256,7 +2227,6 @@ mod tests {
}]
}]
})];
// exact form still works (default behaviour)
assert_inline_images(
&bodies,
1,
@@ -2265,7 +2235,6 @@ mod tests {
&DimensionAssertion::Exact(8),
)
.expect("exact match");
// range form succeeds when width >= 1
assert_inline_images(
&bodies,
1,
@@ -2280,7 +2249,7 @@ mod tests {
},
)
.expect("range match");
// range form fails when width must be >= 28 but actual is 8
// The fixture is 8x8, so a `min: 28` range must fail.
assert!(
assert_inline_images(
&bodies,
@@ -120,7 +120,7 @@ const AUTO_MUX_NODROP: &[InvariantId] = &[
];
const AUTO_SCREEN: &[InvariantId] = &[Ord, Cap, DropEq, Cadence, ConsA, Accel, Carry, Cfg, Screen];
/// The jerk suite: core + the two smoothness invariants the finalize-decel
/// fix made hold (formerly this cell's xfail set).
/// fix made hold (once this cell's xfail set).
const JERK: &[InvariantId] = &[
Ord,
Cap,
@@ -158,7 +158,7 @@ const fn cell(
/// The matrix. Ids are `<class>_<config>_<gesture>[_qualifier]`.
#[rustfmt::skip]
pub const CELLS: &[MatrixCell] = &[
// ── Curated (CI tier, 8 cells) ─────────────────────────────────────
// Curated (CI tier, 8 cells)
cell("c1_auto_g3_flood_speed100", Tier::Curated, &[SPEED100],
ExpectedProfile { speed: 6.0, ..C1 }, GestureId::G3Flood, SessionKind::Settled, AUTO_QUIET),
cell("c2_auto_g3_flood_speed100", Tier::Curated, &[ITERM, SPEED100],
@@ -179,7 +179,7 @@ pub const CELLS: &[MatrixCell] = &[
// (I-SMOOTH-COAST, I-NO-DROP) are ordinary pass rows now.
cell("c1_auto_g4_jerk_xfail", Tier::Curated, &[],
C1, GestureId::G4Jerk, SessionKind::Settled, JERK),
// ── Full tier (local sweep; representative subset — see trim note)
// Full tier (local sweep; representative subset — see trim note)
cell("c1_auto_g1", Tier::Full, &[], C1,
GestureId::G1Notch, SessionKind::Settled, AUTO_NODROP),
cell("c1_auto_g2", Tier::Full, &[], C1,
@@ -250,7 +250,8 @@ mod tests {
if let Some(lines) = get("KIGI_SCROLL_LINES") {
let lines: u16 = lines.parse().unwrap();
wheel_lpt = lines;
trackpad_lpt = lines; // one knob overrides both paths
// one knob overrides both paths
trackpad_lpt = lines;
}
// speed_to_multiplier re-derivation for the settings used in rows.
let speed = match get("KIGI_SCROLL_SPEED") {
@@ -35,9 +35,8 @@ pub const MIN_LINES_PER_WHEEL_STREAM: i64 = 1;
/// One SGR wheel report: sleep `pre_delay_ms`, then emit `button`.
///
/// `button` is [`SGR_SCROLL_UP`]/[`SGR_SCROLL_DOWN`] — `u16` because those
/// harness consts are `u16` (the design sketch said `u8`; deviating keeps
/// one shared definition instead of a cast at every emission site).
/// `button` is `u16` because the [`SGR_SCROLL_UP`]/[`SGR_SCROLL_DOWN`]
/// harness consts are, sparing a cast at every emission site.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct WheelStep {
/// Host-side sleep before emitting this report (0 for the first step).
@@ -190,7 +189,6 @@ pub enum GestureId {
}
impl GestureId {
/// Every gesture, for exhaustive table sweeps (tests, the A13 runner).
pub const ALL: [GestureId; 12] = [
GestureId::G1Notch,
GestureId::G2NotchTrain,
@@ -256,7 +254,7 @@ impl GestureId {
}
}
/// `(up, down)` report counts — direction-sum test primitive.
/// `(up, down)` report counts.
pub fn direction_counts(steps: &[WheelStep]) -> (usize, usize) {
let up = steps.iter().filter(|s| s.button == SGR_SCROLL_UP).count();
(up, steps.len() - up)
@@ -266,8 +264,6 @@ pub fn direction_counts(steps: &[WheelStep]) -> (usize, usize) {
mod tests {
use super::*;
/// Streams split on gaps > STREAM_GAP_MS or direction flips — recompute
/// from the table and compare to the declared `expected_streams`.
fn streams_in(steps: &[WheelStep]) -> usize {
let mut streams = 1;
for pair in steps.windows(2) {
@@ -298,18 +294,15 @@ mod tests {
#[test]
fn notch_structure_and_gaps() {
// G2/ept3: notch starts every 3 events carry the 50ms gap, intra-notch 0.
for (i, step) in G2_NOTCH_TRAIN_EPT3.iter().enumerate() {
let expected = if i > 0 && i % 3 == 0 { 50 } else { 0 };
assert_eq!(step.pre_delay_ms, expected, "G2 ept3 step {i}");
}
// G9b: same shape at 55ms — under the 80ms gap, one stream.
let notch_gaps = G9B_MUX_BATCH
.iter()
.filter(|s| s.pre_delay_ms == 55)
.count();
assert_eq!(notch_gaps, 7, "8 notches → 7 inter-notch gaps");
// G5: dup 4ms after each notch head, notch heads 60ms apart.
for (i, step) in G5_GHOSTTY_DUP.iter().enumerate() {
let expected = if i == 0 {
0
@@ -345,8 +338,6 @@ mod tests {
#[test]
fn delays_agree_with_stream_gap_thresholds() {
// Single-stream gestures never pause past the 80ms finalize gap and
// never flip; multi-stream ones split exactly as declared.
for gesture in GestureId::ALL {
for ept in [1u16, 3] {
let steps = gesture.steps(ept);
@@ -359,13 +350,10 @@ mod tests {
);
}
}
// The G11 pause is what splits it: strictly past the finalize gap.
assert!(G11_CARRY_EPT3[3].pre_delay_ms > STREAM_GAP_MS);
assert!(G11_CARRY_EPT1[1].pre_delay_ms > STREAM_GAP_MS);
// G1/ept3 is a first tick inside the wheel-promotion window.
let g1_span: u64 = G1_NOTCH_EPT3.iter().map(|s| s.pre_delay_ms).sum();
assert!(g1_span <= WHEEL_TICK_DETECT_MAX_MS);
// G5's dup spacing must sit under the interval-window floor.
assert!((G5_GHOSTTY_DUP[1].pre_delay_ms as f64) < ACCEL_MIN_INTERVAL_MS);
}
}
@@ -463,7 +463,7 @@ mod tests {
use super::super::log::{group_streams, parse_jsonl_str};
use super::*;
// ── JSONL fixture builders ─────────────────────────────────────────
// JSONL fixture builders
// Raw strings through parse_jsonl_str so every fixture also exercises
// the wire schema (same stance as log.rs's producer-shaped constants).
@@ -498,7 +498,8 @@ mod tests {
kind: &str,
events: (u64, u64),
accel_avg: (f32, Option<f64>),
lines: (f32, i64, i64, i64), // desired, applied_total, flushed, backlog_after
// desired, applied_total, flushed, backlog_after
lines: (f32, i64, i64, i64),
msf: Option<f64>,
dropped: Option<i64>,
) -> String {
@@ -710,7 +711,8 @@ mod tests {
fn cfg_rejects_echo_profile_mismatch() {
assert!(check(InvariantId::Cfg, &C1, &canonical()).is_pass());
let mut fixture = canonical();
fixture[0] = start(0.0, 0.0, "auto", 6.0); // speed echo ≠ expected 1.0
// speed echo ≠ expected 1.0
fixture[0] = start(0.0, 0.0, "auto", 6.0);
assert_violated(check(InvariantId::Cfg, &C1, &fixture), "speed");
let expected_wheel = ExpectedProfile {
mode: "wheel",
@@ -292,7 +292,8 @@ mod tests {
fn jsonl(lines: &[&str]) -> String {
let mut out = lines.join("\n");
out.push('\n'); // producer's writeln! always terminates lines
// producer's writeln! always terminates lines
out.push('\n');
out
}
@@ -35,7 +35,6 @@ pub enum CellStatus {
}
impl CellStatus {
/// Fixed-width table label.
pub fn as_str(self) -> &'static str {
match self {
CellStatus::Pass => "PASS",
@@ -46,7 +45,6 @@ impl CellStatus {
}
}
/// One invariant row of a [`CellReport`].
#[derive(Clone, Debug, Serialize)]
pub struct InvariantReport {
/// Design vocabulary id (`I-ORD`, …).
@@ -57,7 +55,6 @@ pub struct InvariantReport {
pub detail: Option<String>,
}
/// Verdict of one matrix cell run.
#[derive(Clone, Debug, Serialize)]
pub struct CellReport {
pub cell_id: String,
@@ -88,8 +85,7 @@ pub fn exit_code(reports: &[CellReport]) -> u8 {
u8::from(failed)
}
/// Write `report.json` (pretty, array of [`CellReport`]) into `dir`,
/// creating it as needed; returns the file path.
/// Write the reports as `dir/report.json`, creating `dir` as needed.
pub fn write_report_json(reports: &[CellReport], dir: &Path) -> Result<PathBuf> {
std::fs::create_dir_all(dir)
.with_context(|| format!("create artifacts dir {}", dir.display()))?;
@@ -208,7 +204,6 @@ mod tests {
"XFail is green"
);
assert_eq!(exit_code(&[pass.clone(), fail]), 1);
// The fixed-bug tripwire: an xfail cell that PASSES must break the run.
assert_eq!(exit_code(&[pass, xfail, xpass]), 1, "XPass must be nonzero");
}
@@ -267,12 +262,10 @@ mod tests {
let lines: Vec<&str> = table.lines().collect();
assert_eq!(lines.len(), 3, "header + one row per cell:\n{table}");
// Every row's STATUS column starts where the header's does.
let status_col = lines[0].find("STATUS").expect("header STATUS");
assert_eq!(&lines[1][status_col..status_col + 4], "PASS");
assert_eq!(&lines[2][status_col..status_col + 4], "FAIL");
// Failure detail is carried (truncated, newlines flattened).
assert!(lines[2].contains("I-CAP: flushed 40"), "table:\n{table}");
assert!(lines[2].contains("..."), "long detail truncated:\n{table}");
assert!(
@@ -68,7 +68,6 @@ const COMPLETION_TIMEOUT: Duration = Duration::from_secs(30);
/// streaming preamble plus its post-gesture tail drain — finishes in ~20s.
const CELL_HARD_CAP: Duration = Duration::from_secs(60);
/// `tier` label for [`CellReport`].
fn tier_label(tier: Tier) -> &'static str {
match tier {
Tier::Curated => "curated",
@@ -145,7 +144,6 @@ fn panic_message(err: tokio::task::JoinError) -> String {
}
}
/// Everything [`run_cell`] needs from a completed (non-aborted) cell body.
struct CellRun {
outcomes: Vec<(InvariantId, InvariantResult)>,
streams: usize,
@@ -311,11 +309,9 @@ fn check_screen(
InvariantResult::Pass
}
/// XPASS row detail: the actionable half of the xfail contract.
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";
/// Classify evaluated invariants into per-row statuses and the cell verdict.
/// 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`.
@@ -370,8 +366,8 @@ mod tests {
}
}
/// The wire bytes of the shared wheel position, pinned to the
/// `40;12` encoding documented on `WHEEL_ROW`/`WHEEL_COL`.
/// 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");
@@ -382,9 +378,7 @@ mod tests {
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);
// Up then partially back down: net movement, no clamp involved.
assert_eq!(simulate_clamped_travel(300, [-5, 3]), -2);
// Pure down at the pin stays put.
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);
@@ -422,13 +416,11 @@ mod tests {
fn classify_precedence_fail_over_xpass_over_xfail_over_pass() {
use InvariantId::{Cap, NoDrop, Ord, SmoothCoast};
// All pass, nothing xfailed → Pass.
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");
// The declared bug violates, everything else passes → XFail.
let jerk = [
(Ord, InvariantResult::Pass),
(SmoothCoast, violated("coast")),
@@ -439,7 +431,7 @@ mod tests {
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 (fixed-bug tripwire)…
// One xfail row passing flips the cell to XPass: the fixed-bug tripwire.
let half_fixed = [
(SmoothCoast, InvariantResult::Pass),
(NoDrop, violated("dropped 74")),
@@ -448,7 +440,7 @@ mod tests {
assert_eq!(status, CellStatus::XPass);
assert!(rows[0].detail.as_deref().unwrap().contains("promote"));
// …but any non-xfail violation dominates everything.
// A non-xfail violation dominates everything.
let broken = [
(Cap, violated("flushed 40 exceeds cap 25")),
(SmoothCoast, InvariantResult::Pass),
@@ -67,7 +67,7 @@ const MARKER_PREFIX: &str = "MARKER-";
/// off-screen while the tail is in flight (witness for "still streaming").
pub const STREAM_END_SENTINEL: &str = "STREAMDONE";
/// Unique numbered marker line (`MARKER-0042`), zero-padded so no marker is
/// Unique numbered marker line (`MARKER-0042`), zero-`padded` so no marker is
/// a substring of another within a [`marker_response`] transcript.
pub fn marker_line(n: usize) -> String {
format!("{MARKER_PREFIX}{n:04}")
@@ -6,11 +6,10 @@
use std::time::{Duration, Instant};
// Use `vte` re-exported from `alacritty_terminal` (via ptyctl) instead of
// a separate direct dependency.
// `vte` is re-exported by `alacritty_terminal` (pulled in via ptyctl), so no
// direct dependency on it is needed.
use alacritty_terminal::vte;
/// Timing data for a single rendered frame.
#[derive(Debug, Clone)]
pub struct FrameTiming {
/// Wall-clock duration from BeginSynchronizedUpdate to EndSynchronizedUpdate.
@@ -19,11 +18,6 @@ pub struct FrameTiming {
pub chars: usize,
}
/// Parses raw PTY output for synchronized-update frame boundaries and
/// records per-frame timing data.
///
/// Composes a `vte::Parser` with an internal `FrameTimingHandler` that
/// implements `vte::Perform`.
pub struct FrameTimingParser {
vte_parser: vte::Parser,
handler: FrameTimingHandler,
@@ -37,22 +31,20 @@ impl FrameTimingParser {
}
}
/// Feed raw PTY output bytes, parsing for frame boundaries.
pub fn feed(&mut self, bytes: &[u8]) {
self.vte_parser.advance(&mut self.handler, bytes);
}
/// Return all recorded frame timings.
pub fn timings(&self) -> &[FrameTiming] {
&self.handler.timings
}
/// Return the total number of completed frames.
/// Counts only frames whose end marker has been seen; a frame still open
/// is not included.
pub fn frame_count(&self) -> u64 {
self.handler.timings.len() as u64
}
/// Reset all recorded timing data.
pub fn reset(&mut self) {
self.handler.timings.clear();
self.handler.frame_start = None;
@@ -66,7 +58,6 @@ impl Default for FrameTimingParser {
}
}
/// Internal VTE `Perform` implementation that detects frame boundaries.
struct FrameTimingHandler {
frame_start: Option<Instant>,
timings: Vec<FrameTiming>,
@@ -91,7 +82,8 @@ impl vte::Perform for FrameTimingHandler {
_ignore: bool,
action: char,
) {
// Only interested in CSI ? <param> h/l (private mode set/reset).
// Synchronized-update markers are private modes, so anything without
// the `?` intermediate cannot be one.
if intermediates != b"?" {
return;
}
@@ -104,12 +96,10 @@ impl vte::Perform for FrameTimingHandler {
if param_val == 2026 {
match action {
'h' => {
// BeginSynchronizedUpdate: frame starts.
self.frame_start = Some(Instant::now());
self.current_frame_chars = 0;
}
'l' => {
// EndSynchronizedUpdate: frame ends.
if let Some(start) = self.frame_start.take() {
self.timings.push(FrameTiming {
duration: start.elapsed(),
@@ -5,7 +5,8 @@
//! ```
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore] // opt-in: real pager binary in a PTY (CI runs with --ignored)
// opt-in: real pager binary in a PTY (CI runs with --ignored)
#[ignore]
async fn empty_enter_force_sends_top_queued() {
kigi_pager_pty_harness::scenarios::empty_enter_send_now::assert_empty_enter_force_sends_top_queued()
.await
@@ -34,7 +34,8 @@ const SIGINT_CANARY: &str = "SIGINTCANARY7";
const ACK: &str = "ACKSENTINEL";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore] // opt-in: spawns the real pager binary in a PTY (CI runs with --ignored)
// opt-in: spawns the real pager binary in a PTY (CI runs with --ignored)
#[ignore]
async fn prompt_history_durable_after_double_ctrl_c_and_recallable_on_resume() {
run().await.expect("prompt-history durable-quit e2e");
}
@@ -43,7 +44,8 @@ async fn prompt_history_durable_after_double_ctrl_c_and_recallable_on_resume() {
/// same graceful quit: the prompt stays durable and the process exits 0.
#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore] // opt-in: spawns the real pager binary in a PTY (CI runs with --ignored)
// opt-in: spawns the real pager binary in a PTY (CI runs with --ignored)
#[ignore]
async fn prompt_history_durable_after_real_sigint_graceful_quit() {
run_sigint().await.expect("sigint graceful-quit e2e");
}
@@ -197,7 +199,8 @@ fn submit_and_settle(
pager
.wait_for_text(ACK, Duration::from_secs(30))
.context("turn response rendered")?;
pager.update(Duration::from_millis(1000)); // let the short turn finish (idle)
// let the short turn finish (idle)
pager.update(Duration::from_millis(1000));
Ok(pager)
}
@@ -98,7 +98,8 @@ async fn scroll_up_from_follow_bottom_then_back_down() -> Result<()> {
}
}
// Wheel over mid-screen scrollback (1-indexed SGR coords).
harness.inject_keys(&sgr_scroll(64, 15, 40, 50))?; // 64 = wheel up
// 64 = wheel up
harness.inject_keys(&sgr_scroll(64, 15, 40, 50))?;
harness.update(Duration::from_millis(200));
harness.update(Duration::from_millis(300));
let mid = harness.screen_contents();
@@ -126,7 +127,8 @@ async fn scroll_up_from_follow_bottom_then_back_down() -> Result<()> {
bail!("pager exited while PageDown scrolling");
}
}
harness.inject_keys(&sgr_scroll(65, 15, 40, 50))?; // 65 = wheel down
// 65 = wheel down
harness.inject_keys(&sgr_scroll(65, 15, 40, 50))?;
harness.update(Duration::from_millis(200));
harness.update(Duration::from_millis(400));
let back_bottom = harness.screen_contents();
@@ -89,7 +89,7 @@ async fn c1_auto_g8_midstream() {
assert_cell_passes("c1_auto_g8_midstream").await;
}
/// The formerly-declared bug: the finalize-decel fix landed, so the G4 jerk
/// The once-declared bug: the finalize-decel fix landed, so the G4 jerk
/// cell passes outright — I-SMOOTH-COAST (post-input motion at most one
/// tapered cap) and I-NO-DROP (finalize discards nothing) moved from the
/// xfail set to ordinary pass rows. The cell id keeps its historical name