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
@@ -77,7 +77,7 @@ async fn campaign_leader_mode_remote_dismiss_on_model_pick() {
.unwrap_or(false)
};
// ── Phase 1+2: nudge on a new session; a pick records the dismissal in
// Phase 1+2: nudge on a new session; a pick records the dismissal in
// the TUI process. Retries fresh TUI spawns (same leader) so a missed
// 2s prefetch window on a loaded runner can't wedge the test.
let mut recorded = false;
@@ -116,7 +116,7 @@ async fn campaign_leader_mode_remote_dismiss_on_model_pick() {
"leader-mode TUI must record the remote campaign dismissal in {state_path:?}"
);
// ── Phase 3: the dismissal is durable and the pick is persisted. The
// Phase 3: the dismissal is durable and the pick is persisted. The
// user's choice must be in config.toml (campaign value never laundered
// in), and the dismissed id on disk is what every future resolution —
// leader or not — filters on (`dismissed_id_is_dropped_from_override`
@@ -26,12 +26,9 @@ pub(crate) const PROMPT: &str = "go";
/// Response sentinel the mock server streams back.
pub(crate) const MOCK_RESPONSE_SENTINEL: &str = "MOCKRESPONSE";
/// Cold leader-client bring-up budget. History: 60s → 120s → 240s while these
/// cases ran interleaved with the full `pty_e2e` suite (each leader case
/// spawns multiple full pager processes; suite-wide contention pushed cold
/// bring-up past two minutes). Now that the leader cases run in their own
/// serialized target the budget can come back down after a green soak week —
/// tracked in the leader test-infra plan.
/// Cold leader-client bring-up budget, generous because each leader case
/// spawns multiple full pager processes; can come back down after a green
/// soak week — tracked in the leader test-infra plan.
pub(crate) const LEADER_TIMEOUT: Duration = Duration::from_secs(240);
/// Streamed-turn deadline in leader mode (same contention rationale).
@@ -28,7 +28,6 @@ async fn leader_n_clients_shared_session() {
.content()
.set_response(format!("{} first turn payload.", turn_sentinel(1)));
// The driver spawns the leader and runs turn 1.
let mut a = cluster.spawn_leader(&[]).expect("spawn driver");
a.wait_for_text(WELCOME_SCREEN_SENTINEL, LEADER_TIMEOUT)
.expect("driver welcome");
@@ -60,7 +59,6 @@ async fn leader_n_clients_shared_session() {
viewers.push(v);
}
// Turn 2 driven from the driver streams live into EVERY viewer (fan-out).
cluster
.content()
.set_response(format!("{} second turn payload.", turn_sentinel(2)));
@@ -40,8 +40,7 @@ async fn leader_reattach_cancellation_roundtrips_durable_log() {
.expect("A welcome");
a.inject_keys(format!("{PROMPT}\r").as_bytes())
.expect("A submit turn");
// Wait until the turn is clearly streaming (sentinel visible); this also
// closes the leader's rewind window so cancel is not confused with rewind.
// Also closes the leader's rewind window, so cancel is not confused with rewind.
a.wait_for_text(&turn_sentinel(1), STREAM_TIMEOUT)
.expect("A turn streaming");
@@ -20,7 +20,6 @@ async fn leader_reattach_completion_roundtrips_durable_log() {
.content()
.set_response(format!("{} completed turn payload.", turn_sentinel(1)));
// A elects the leader and drives one turn to completion.
let mut a = cluster.spawn_leader(&[]).expect("spawn leader client A");
a.wait_for_text(WELCOME_SCREEN_SENTINEL, LEADER_TIMEOUT)
.expect("A welcome");
@@ -53,8 +52,6 @@ async fn leader_reattach_completion_roundtrips_durable_log() {
// must see no new inference request while C catches up.
let inference_before_reattach = inference_request_count(cluster.content());
// Fresh reattach AFTER A exits: it replays the completed transcript via the
// durable rail through the surviving leader.
drop(a);
let mut c = cluster.attach(&[]).expect("spawn fresh reattach client C");
c.wait_for_text(&turn_sentinel(1), LEADER_TIMEOUT)
@@ -46,7 +46,6 @@ async fn leader_two_clients_shared_session() {
"A's turn must appear in B exactly once (duplicated replay?)\nB screen:\n{b_screen}"
);
// Turn 2 driven from A streams live into the attached viewer B.
cluster
.content()
.set_response(format!("{} second turn payload.", turn_sentinel(2)));
@@ -149,7 +149,8 @@ fn pty_shift_tab_cycles_to_auto_mode_banner() {
harness.is_running() || !early.is_empty(),
"pager must have produced output even on login screen"
);
let _ = harness.inject_keys(b"\x11"); // ctrl+q if bound
// ctrl+q if bound
let _ = harness.inject_keys(b"\x11");
return;
}
@@ -23,7 +23,6 @@ async fn agent_response() {
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
.expect("welcome text");
// Type the prompt and submit with Enter.
harness
.inject_keys(format!("{PROMPT}\r").as_bytes())
.expect("submit prompt");
@@ -34,7 +34,6 @@ async fn agent_type_mismatch_modal_on_model_switch() {
.inject_keys(b"/model cursor-model\r")
.expect("type model switch");
// The question modal should appear.
harness
.wait_for_text("requires starting a new session", Duration::from_secs(15))
.expect("agent type mismatch modal should appear");
@@ -34,7 +34,6 @@ async fn agent_type_mismatch_no_keeps_current_session() {
.inject_keys(b"/model cursor-model\r")
.expect("type model switch");
// Wait for the modal.
harness
.wait_for_text("requires starting a new session", Duration::from_secs(15))
.expect("modal appeared");
@@ -46,7 +45,6 @@ async fn agent_type_mismatch_no_keeps_current_session() {
// Modal should dismiss. Wait a moment for UI to settle.
harness.update(Duration::from_millis(500));
// The original response should still be visible (same session).
assert!(
harness.contains_text(MOCK_RESPONSE_SENTINEL),
"original response should still be visible after cancelling\nscreen:\n{}",
@@ -34,7 +34,6 @@ async fn agent_type_mismatch_yes_starts_new_session() {
.inject_keys(b"/model cursor-model\r")
.expect("type model switch");
// Wait for the modal.
harness
.wait_for_text("requires starting a new session", Duration::from_secs(15))
.expect("modal appeared");
@@ -2,8 +2,6 @@
#[allow(unused_imports)]
use super::common::*;
// ── Auto-compact: the top padding row disappears on tiny terminals ─────────
//
// The default layout reserves a blank top padding row (`outer_vpad`) above the
// status bar. When the terminal is `AUTO_COMPACT_MAX_ROWS` (20) or shorter,
// the render-value compact flag is derived ON (auto-compact) and the padding
@@ -85,7 +85,8 @@ async fn bash_full_output_double_click_fold_pty() {
"double-click must collapse the ! block; got:\n{}",
harness.screen_contents()
);
harness.update(Duration::from_millis(500)); // let the multi-click window lapse
// Let the multi-click window lapse.
harness.update(Duration::from_millis(500));
harness
.inject_keys(dbl.as_bytes())
.expect("double-click to expand");
@@ -100,7 +100,7 @@ async fn bash_mode_file_completion_shell_like() {
.wait_for_text(MOCK_RESPONSE_SENTINEL, Duration::from_secs(30))
.expect("session ready");
// ── Leg 1: Tab fills the common prefix, second Tab opens the list ───
// Leg 1: Tab fills the common prefix, second Tab opens the list.
harness
.inject_keys(b"!cat al")
.expect("type bash prefix with shared-prefix candidates");
@@ -127,7 +127,7 @@ async fn bash_mode_file_completion_shell_like() {
harness.inject_keys(b"\x1b").expect("Esc closes dropdown");
harness.inject_keys(b"\x15").expect("Ctrl+U clears draft");
// ── Leg 2: quoted dropdown dir accept drill-down insta-accept ───
// Leg 2: quoted dropdown, dir accept, drill-down insta-accept.
harness
.inject_keys(b"cat \"no")
.expect("type quoted prefix");
@@ -155,7 +155,7 @@ async fn bash_mode_file_completion_shell_like() {
.wait_for_text("Notes Archive/inner_note.txt\"", Duration::from_secs(10))
.expect("single inner candidate accepted immediately, quote closed");
// ── The completed command actually RUNS through the real shell ──────
// The completed command actually runs through the real shell.
harness.inject_keys(b"\r").expect("Enter runs the command");
harness
.wait_for_text(INNER_SENTINEL, Duration::from_secs(30))
@@ -87,7 +87,7 @@ async fn bracketed_ime_paste_skips_clipboard_image_linux() {
harness
}
// ── Otty: IME-style bracketed paste, image-only clipboard → no image ──
// Otty: IME-style bracketed paste, image-only clipboard → no image.
let mut harness = spawn_on_dashboard(&base_env, &[("TERM_PROGRAM", "otty")]);
harness
.inject_keys(format!("\x1b[200~{IME_PAYLOAD}\x1b[201~").as_bytes())
@@ -102,7 +102,7 @@ async fn bracketed_ime_paste_skips_clipboard_image_linux() {
harness.screen_contents()
);
// ── Otty (positive control): payload == clipboard caption → image ──
// Otty (positive control): payload == clipboard caption → image.
std::fs::write(&text_file, CAPTION.as_bytes()).expect("genuine-paste clipboard text");
harness
.inject_keys(format!("\x1b[200~{CAPTION}\x1b[201~").as_bytes())
@@ -120,8 +120,8 @@ async fn bracketed_ime_paste_skips_clipboard_image_linux() {
);
harness.quit().expect("clean quit");
// ── No TERM_PROGRAM (any other terminal): historical behavior intact —
// the same mismatched bracketed payload still attaches the image ──
// No TERM_PROGRAM (any other terminal): historical behavior intact —
// the same mismatched bracketed payload still attaches the image.
std::fs::write(&text_file, b"").expect("reset clipboard text");
let mut harness = spawn_on_dashboard(&base_env, &[]);
harness
@@ -51,7 +51,7 @@ async fn campaign_nudges_default_until_dismissed_by_model_pick() {
PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs).expect("spawn pager")
};
// ── Phase 1: a fresh boot shows the campaign model, not the config one. ──
// Phase 1: a fresh boot shows the campaign model, not the config one.
{
let mut h = spawn(&campaign_env);
h.wait_for_text(CAMPAIGN_MODEL, WELCOME_TIMEOUT)
@@ -69,7 +69,7 @@ async fn campaign_nudges_default_until_dismissed_by_model_pick() {
h.quit().expect("clean quit");
}
// ── Phase 2: a session + explicit `/model` pick dismisses the campaign. ──
// Phase 2: a session + explicit `/model` pick dismisses the campaign.
{
let mut h = spawn(&campaign_env);
h.wait_for_text(CAMPAIGN_MODEL, WELCOME_TIMEOUT)
@@ -102,7 +102,7 @@ async fn campaign_nudges_default_until_dismissed_by_model_pick() {
h.quit().expect("clean quit");
}
// ── Phase 3: reboot with the SAME campaign env -> the config model wins. ──
// Phase 3: reboot with the SAME campaign env -> the config model wins.
{
let mut h = spawn(&campaign_env);
h.wait_for_text(CONFIG_MODEL, WELCOME_TIMEOUT)
@@ -60,7 +60,7 @@ async fn campaign_remote_settings_nudge_and_dismiss() {
PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs).expect("spawn pager")
};
// ── Phase 1+2: the campaign applies to a new session; a pick dismisses. ──
// Phase 1+2: the campaign applies to a new session; a pick dismisses.
{
let mut h = spawn();
h.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
@@ -100,7 +100,7 @@ async fn campaign_remote_settings_nudge_and_dismiss() {
h.quit().expect("clean quit");
}
// ── Phase 3: reboot against the SAME settings → the config model wins. ──
// Phase 3: reboot against the SAME settings → the config model wins.
{
let mut h = spawn();
h.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
@@ -2,13 +2,11 @@
#[allow(unused_imports)]
use super::common::*;
/// 22. **Send-now delivery vs. explicit cancel.** (Historical name: the chord
/// used to buffer a same-turn interjection that a cancel could discard.)
/// The chord is now cancel-and-send: text + Ctrl+Enter mid-stream silently
/// cancels the running turn and delivers the text as its OWN next turn (no
/// interjection preamble on the wire). A later explicit Ctrl+C still renders
/// its "Turn cancelled by user" marker — the consumed send-now expectation
/// must never suppress a real user cancel.
/// 22. **Send-now delivery vs. explicit cancel.** Text + Ctrl+Enter mid-stream
/// silently cancels the running turn and delivers the text as its OWN next
/// turn (no interjection preamble on the wire). A later explicit Ctrl+C still
/// renders its "Turn cancelled by user" marker — the consumed send-now
/// expectation must never suppress a real user cancel.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn cancel_discards_buffered_interjection() {
@@ -59,14 +59,12 @@ async fn cancel_then_resend_prompt_appears_once() {
)
.expect("rewound prompt restored after history trim");
// Resend the restored text as a fresh turn.
content.set_chunk_delay(None);
harness.inject_keys(b"\r").expect("Enter resends");
harness
.wait_for_text("RESENT_REPLY", Duration::from_secs(90))
.expect("resent turn reply");
// Exactly once in scrollback (block back, composer empty again).
harness
.wait_until(
"resent prompt rendered exactly once",
+19 -35
View File
@@ -35,8 +35,6 @@ pub(crate) const PROMPT: &str = "go";
/// unambiguous sentinel word that we can `wait_for_text` on.
pub(crate) const MOCK_RESPONSE_SENTINEL: &str = "MOCKRESPONSE";
// ── Undo-tip e2e helpers ────────────────────────────────────────────────
/// Suffix of the undo-tip banner, now "Input cleared · ctrl+z to undo" on all
/// platforms (terminals don't forward Cmd+Z to a raw-mode TUI). Asserting the
/// suffix keeps the check chord-agnostic regardless.
@@ -131,7 +129,6 @@ pub(crate) fn tall_response(sentinel: &str, rows: usize) -> String {
s
}
// ── Fake session-auth (OAuth) seeding ───────────────────────────────────
// `seed_fake_oauth` / `oauth_env_for_pager` live in
// `kigi_pager_pty_harness::flows` (re-exported above).
@@ -181,8 +178,6 @@ pub(crate) fn spawn_polling_session_with_env(
harness
}
// ── Agent type mismatch e2e tests ──────────────────────────────────────
/// Start the mock server with two models that have different agent types,
/// and return a `ContentController` configured for agent-type-mismatch
/// testing. The default model is `"default-model"` (no agent type → uses
@@ -196,8 +191,6 @@ pub(crate) async fn start_dual_agent_type_content() -> ContentController {
.expect("start content with dual agent types")
}
// ── Folder-trust welcome sub-state e2e ──────────────────────────────────
/// Title line of the folder-trust question (see `render_welcome_trust`).
pub(crate) const TRUST_QUESTION_SENTINEL: &str = "Do you trust the contents of this directory";
@@ -239,7 +232,6 @@ pub(crate) fn folder_is_trusted(content: &ContentController, repo: &std::path::P
store.is_trusted(&kigi_workspace::trust::workspace_key(repo))
}
// ── Leader mode e2e ─────────────────────────────────────────────────────
// The leader cluster cases (and their LEADER_TIMEOUT/STREAM_TIMEOUT/
// submit_turn/inference_request_count helpers) moved to the dedicated
// `tests/leader_pty_e2e` target; only the helpers non-leader tests still
@@ -254,8 +246,6 @@ pub(crate) fn turn_sentinel(n: u8) -> String {
// `wait_for_labels_absent` lives in `kigi_pager_pty_harness::flows`
// (re-exported above).
// ── MCP menu loading e2e tests ──────────────────────────────────────────
/// Seeded server name; it only renders once the MCP list fetch resolves.
pub(crate) const MCP_TEST_SERVER: &str = "ptytestmcp";
@@ -317,8 +307,6 @@ pub(crate) async fn drive_mcp_menu_load(content: &ContentController, cwd: &std::
harness.quit().expect("clean quit");
}
// ── Queue + interjection e2e tests ──────────────────────────────────────
// Ctrl+Enter / Ctrl+; as CSI-u, parsed without kitty protocol negotiation.
pub(crate) const CTRL_ENTER: &[u8] = b"\x1b[13;5u";
@@ -394,8 +382,6 @@ pub(crate) const SEND_NOW_TIP_SENTINEL: &str = "to send now";
// careful environment variable setup to avoid static caching issues with
// KIGI_SHARE_DIR.
// ── Mouse reporting toggle (opt-in scrollback Ctrl+R) ───────────────────
/// Sticky banner shown while mouse reporting is off (must match pager copy).
pub(crate) const MOUSE_OFF_STICKY: &str =
"Ctrl+r to enable mouse reporting and restore TUI features";
@@ -507,8 +493,6 @@ pub(crate) async fn drive_to_scrollback_with_turn(
let _ = harness.wait_for_text("Space:prompt", Duration::from_secs(5));
}
// ── Tool header selection (path/command operand only) ───────────────────
/// Unique filename marker so the Read header is unambiguous on screen.
pub(crate) const READ_HDR_FILE: &str = "read_hdr_sel_target.txt";
@@ -788,10 +772,14 @@ pub(crate) fn sgr_mouse(btn: u16, row: u16, col: u16, suffix: char) -> String {
/// SGR mouse drag from (row,col) inclusive to (row,end_col) inclusive.
pub(crate) fn mouse_drag_line(row: u16, from_col: u16, to_col: u16) -> String {
let mut out = String::new();
out.push_str(&sgr_mouse(0, row, from_col, 'M')); // press
out.push_str(&sgr_mouse(32, row, (from_col + to_col) / 2, 'M')); // drag mid
out.push_str(&sgr_mouse(32, row, to_col, 'M')); // drag end
out.push_str(&sgr_mouse(0, row, to_col, 'm')); // release
// press
out.push_str(&sgr_mouse(0, row, from_col, 'M'));
// drag mid
out.push_str(&sgr_mouse(32, row, (from_col + to_col) / 2, 'M'));
// drag end
out.push_str(&sgr_mouse(32, row, to_col, 'M'));
// release
out.push_str(&sgr_mouse(0, row, to_col, 'm'));
out
}
@@ -802,9 +790,12 @@ pub(crate) fn mouse_drag_line(row: u16, from_col: u16, to_col: u16) -> String {
/// ("It works if mouseup occurs outside the terminal element"), microsoft/vscode#192518.
pub(crate) fn mouse_drag_no_release(row: u16, from_col: u16, to_col: u16) -> String {
let mut out = String::new();
out.push_str(&sgr_mouse(0, row, from_col, 'M')); // press
out.push_str(&sgr_mouse(32, row, (from_col + to_col) / 2, 'M')); // drag mid
out.push_str(&sgr_mouse(32, row, to_col, 'M')); // drag end
// press
out.push_str(&sgr_mouse(0, row, from_col, 'M'));
// drag mid
out.push_str(&sgr_mouse(32, row, (from_col + to_col) / 2, 'M'));
// drag end
out.push_str(&sgr_mouse(32, row, to_col, 'M'));
out
}
@@ -1008,8 +999,6 @@ pub(crate) fn seed_read_file_tool_call(content: &ContentController, abs_path: &P
content.set_response(READ_HDR_SENTINEL);
}
// ── Minimal (scrollback-native) mode e2e helpers ────────────────────────
/// Args that launch the pager in the experimental scrollback-native minimal
/// mode, standalone — minimal is single-session (K14: no leader/multi-client),
/// so `--no-leader` keeps the test off the shared-daemon path.
@@ -1088,16 +1077,14 @@ pub(crate) fn wait_minimal_ready(harness: &mut PtyHarness) {
/// into it), so quit is Ctrl+Q pressed twice (it requires confirmation). Falls
/// back to the harness kill path if the chord doesn't take.
pub(crate) fn quit_minimal(harness: &mut PtyHarness) {
let _ = harness.inject_keys(b"\x11"); // Ctrl+Q — arms the confirm
let _ = harness.inject_keys(b"\x11");
harness.update(Duration::from_millis(80));
let _ = harness.inject_keys(b"\x11"); // Ctrl+Q — confirms
let _ = harness.inject_keys(b"\x11");
if harness.wait_exit_code(Duration::from_secs(5)).is_none() {
let _ = harness.quit(); // kill fallback
let _ = harness.quit();
}
}
// ── kigi wrap e2e ───────────────────────────────────────────────────────
/// `kigi wrap` run budget. Same contention math as the requirements-version
/// test: the child's cold exec of the huge debug binary can land its first
/// write well past 30s under the parallel pty_e2e suite.
@@ -1130,7 +1117,8 @@ pub(crate) fn run_wrap(wrap_args: &[&str], extra_env: &[(&str, &str)]) -> (Optio
.wait_for_exit_and_drain(WRAP_TIMEOUT, WRAP_DRAIN_TIMEOUT)
.ok();
if code.is_none() {
let _ = harness.quit(); // kill a hung child so the suite doesn't leak it
// Kill a hung child so the suite doesn't leak it.
let _ = harness.quit();
}
let raw = String::from_utf8_lossy(harness.raw_output()).into_owned();
@@ -1159,8 +1147,6 @@ pub(crate) fn fake_argv_echo_shell() -> (tempfile::TempDir, String) {
(dir, path_str)
}
// ── Shared polling / failure-dump / cast helpers ────────────────────────
/// Poll `probe` every 100ms until it yields `Some` or `timeout` elapses.
#[cfg(unix)]
pub(crate) fn poll_for<T>(timeout: Duration, mut probe: impl FnMut() -> Option<T>) -> Option<T> {
@@ -1237,8 +1223,6 @@ pub(crate) fn write_cast_if_requested(harness: &PtyHarness, file_name: &str) {
}
}
// ── Clipboard paste e2e tests ───────────────────────────────────────────
/// Serialized `content` of every user message across recorded requests, in
/// order. Like [`all_user_messages`] but multimodal-tolerant: array-form
/// content (e.g. text plus an image block attached from a macOS dev machine's
@@ -59,8 +59,8 @@ async fn ctrl_c_cancel_during_stream_recovers_cleanly() {
"'Turn cancelled' must appear exactly once\nscreen:\n{screen}"
);
// Recovery: the pane must accept and run a new prompt (no
// TurnCancelling latch, which previously required a restart here).
// Recovery: the pane must accept and run a new prompt (a stuck
// TurnCancelling latch would otherwise force a restart here).
content.set_chunk_delay(None);
content.set_response("RECOVERYSENTINEL post-cancel turn ran.");
harness
@@ -10,7 +10,7 @@ const CTRL_BACKSLASH: &[u8] = b"\x1b[92;5u";
/// Attach the (only) agent row as a session overlay from the dashboard list.
/// Down clamps at the last focusable, so three Downs land on the row regardless
/// of the current cursor (fresh open = New Agent button; after a back-out =
/// previously-selected row); Enter then attaches the peeked row. Waits until the
/// the row selected before it); Enter then attaches the peeked row. Waits until the
/// overlay is up: the dashboard list ("+ New Agent") is gone and the agent's
/// transcript (MOCKRESPONSE) is shown.
fn attach_overlay(h: &mut PtyHarness) {
@@ -22,7 +22,8 @@ use super::common::*;
const STALE_MARKER: &str = "STALE_OUT_OF_BAND_ROW_ZZZ";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore] // opt-in PTY e2e (run with `-- --ignored`)
// opt-in PTY e2e (run with `-- --ignored`)
#[ignore]
async fn out_of_band_stale_row_heals_on_focus_gained() {
let content = ContentController::start()
.await
@@ -9,9 +9,8 @@ const CHROMEHOLD_BOTTOM: &str = "CHROMEHOLD_BOTTOM";
/// PTY: a mouse-down on a blank row INSIDE a block's area whose drag never
/// touches selectable text stays a whole-block drag: release copies the
/// whole message (both paragraphs), exactly as before the deferred-anchor
/// latch existed. Covers the never-enter branch the anchor-on-entry
/// conversion must not disturb.
/// whole message (both paragraphs). Covers the never-enter branch that
/// anchor-on-entry must not disturb.
///
/// `SSH_CONNECTION` forces the OSC 52 clipboard route for readback.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -157,7 +157,6 @@ async fn edit_hl_inplace_refresh_pty() {
}
};
// Welcome
let welcome_deadline = Instant::now() + WELCOME_TIMEOUT;
loop {
sample(&mut harness);
@@ -255,7 +254,6 @@ async fn edit_hl_inplace_refresh_pty() {
harness.screen_contents()
);
// Final full dumps.
let final_screen = harness.screen_contents();
let final_html = harness.screen_html();
fs::write(PathBuf::from(ARTIFACT_DIR).join("final.txt"), &final_screen).ok();
@@ -47,7 +47,6 @@ async fn esc_esc_clears_idle_prompt_and_records_history() {
.wait_for_text("press again to clear", Duration::from_secs(15))
.expect("first idle Esc must show the clear confirm hint");
// Second Esc fires the clear.
harness.inject_keys(keys::ESC).expect("second esc");
wait_for_labels_absent(&mut harness, &[draft], Duration::from_secs(5));
assert!(
@@ -55,7 +54,6 @@ async fn esc_esc_clears_idle_prompt_and_records_history() {
"second Esc must clear the draft\nscreen:\n{}",
harness.screen_contents()
);
// The confirm hint must be gone once the pending fired.
assert!(
!harness.contains_text("press again to clear"),
"clear-confirm hint must clear after the second Esc fires\nscreen:\n{}",
@@ -11,7 +11,6 @@ use super::common::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn file_path_with_space_emits_full_osc8_hyperlink() {
// Synthetic macOS app-bundle path with a space in the final segment.
const PATH_PREFIX: &str = "/Users/alice/src/app/release/mac-arm64/Demo";
const FULL_PATH: &str = "/Users/alice/src/app/release/mac-arm64/Demo App.app";
// file:// URL percent-encodes the space; this is what OSC 8 must carry.
@@ -31,8 +31,6 @@ async fn folder_trust_cwd_is_home_git_repo_no_prompt() {
)
.expect("spawn pager");
// Normal welcome boots; the trust question never appears (unrecordable key =>
// Trusted), so the session can proceed and never re-prompts.
harness
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
.expect("normal welcome renders");
@@ -40,8 +40,10 @@ async fn folder_trust_question_renders_and_accept_persists_grant() {
// but the dispatch chokepoint must still refuse to create a session while
// trust is Pending. Two presses (NewSession requires confirmation) must
// leave us on the trust question, NOT in a session.
harness.inject_keys(b"\x0e").expect("inject Ctrl+N"); // arm
harness.inject_keys(b"\x0e").expect("inject Ctrl+N"); // confirm
// arm
harness.inject_keys(b"\x0e").expect("inject Ctrl+N");
// confirm
harness.inject_keys(b"\x0e").expect("inject Ctrl+N");
harness.update(Duration::from_millis(400));
assert!(
harness.contains_text(TRUST_QUESTION_SENTINEL),
@@ -4,7 +4,7 @@ use super::common::*;
#[allow(unused_imports)]
use super::scroll::*;
// ── A5: env-forced scroll settings reach the live scroll config ───────────
// A5: env-forced scroll settings reach the live scroll config.
//
// `KIGI_SCROLL_MODE=wheel` + `KIGI_SCROLL_LINES=1` on a `TERM_PROGRAM=zed`
// harness (Zed profile: ept=1, wheel_lines=3) must price a 3-event burst at
@@ -17,7 +17,6 @@ async fn initial_prompt_positional_auto_submits() {
));
let binary = pager_binary().expect("resolve pager binary");
// Pass the prompt as a positional argument, exactly like `kigi "go"`.
let mut harness =
PtyHarness::spawn_with_content(&binary, DEFAULT_ROWS, DEFAULT_COLS, &content, &[PROMPT])
.expect("spawn pager with initial prompt");
@@ -3,7 +3,6 @@
use super::common::*;
/// 19. **Send-now chord delivers the composer text as its own next turn.**
/// (Historical name: the chord used to interject into the SAME turn.)
/// Ctrl+Enter with text mid-stream is cancel-and-send: the running turn is
/// cancelled silently and the text runs as the next turn — a standard
/// `<user_query>` prompt with no interjection preamble, rendered as a " "
@@ -64,7 +64,8 @@ async fn managed_policy_gate_refusal_reaches_real_terminal() {
if exit_code.is_none() {
exit_code = harness.wait_exit_code(Duration::ZERO);
if exit_code.is_some() {
harness.update(Duration::from_millis(200)); // final drain after exit
// Final drain after exit.
harness.update(Duration::from_millis(200));
break;
}
}
@@ -14,10 +14,9 @@ use crate::common::*;
#[ignore]
async fn minimal_commits_response_to_scrollback() {
let content = ContentController::start().await.expect("start content");
// Sentinel on the FIRST rendered row; 80 code-block rows >> the 50-row
// screen, so the head scrolls into native scrollback once the block commits.
// (Prose lines would markdown-reflow into one short paragraph that fits on
// screen — see `tall_response`.)
// 80 code-block rows overflow the 50-row screen, pushing the sentinel line
// into scrollback. (Prose would markdown-reflow to fit on screen instead —
// see `tall_response`.)
content.set_response(tall_response(MOCK_RESPONSE_SENTINEL, 80));
let mut harness = spawn_minimal(&content);
@@ -12,9 +12,9 @@ use crate::common::*;
#[ignore]
async fn minimal_committed_content_survives_overlay_grow() {
let content = ContentController::start().await.expect("start content");
// Sentinel on the first rendered row; 80 code-block rows overflow the screen
// so the head reaches native scrollback (prose would reflow to fit on screen
// and never scroll — see `tall_response`).
// 80 code-block rows overflow the screen, pushing the sentinel line into
// scrollback (prose would reflow to fit on screen and never scroll —
// see `tall_response`).
content.set_response(tall_response(MOCK_RESPONSE_SENTINEL, 80));
let mut harness = spawn_minimal(&content);
@@ -28,8 +28,6 @@ async fn minimal_continue_reprints_transcript() {
.expect("turn 1 committed to scrollback");
quit_minimal(&mut first);
// Resume the same session. The transcript is reprinted into native
// scrollback (no separate history pane in minimal).
let mut resumed = spawn_minimal_in_dir(
&content,
DEFAULT_ROWS,
@@ -46,7 +44,6 @@ async fn minimal_continue_reprints_transcript() {
)
});
// A follow-up turn still works in the resumed session.
content.set_response(format!("{} resumed payload.", turn_sentinel(2)));
resumed
.inject_keys(b"again\r")
@@ -14,7 +14,7 @@ async fn minimal_ctrl_c_arms_and_quits() {
let mut harness = spawn_minimal(&content);
wait_minimal_ready(&mut harness);
// First Ctrl+C (CSI legacy ETX). Arms quit + shows the hint under the prompt.
// \x03 is the legacy ETX byte for Ctrl+C.
harness.inject_keys(b"\x03").expect("inject Ctrl+C");
harness
.wait_for_text("again to quit", Duration::from_secs(5))
@@ -25,7 +25,6 @@ async fn minimal_ctrl_c_arms_and_quits() {
)
});
// Second Ctrl+C within the confirm window exits the process.
harness.inject_keys(b"\x03").expect("inject Ctrl+C again");
let code = harness.wait_exit_code(Duration::from_secs(5));
assert!(
@@ -48,11 +48,8 @@ async fn minimal_ctrl_o_send_now_queued_apple_terminal() {
.wait_for_text("1 queued", Duration::from_secs(10))
.expect("queue indicator");
// Empty composer + queue: Ctrl+O must yield to send-now, not transcript.
// Cancel-and-send: the shell silently cancels turn 1 (its held completion
// is irrelevant — the abort wins) and the row commits as a standard " "
// prompt block for its own turn. Turn 1 is still gated open here, so the
// queued row cannot have promoted FIFO.
// Turn 1 is still gated open here, so the queued row cannot have
// promoted FIFO — this exercises the cancel-and-send path specifically.
harness.inject_keys(CTRL_O).expect("Ctrl+O send-now");
// Generous deadline: with turn 1 gated open there is no promotion race
// left to mask — this wait is pure render latency, which under heavy
@@ -60,9 +60,8 @@ async fn minimal_double_esc_committed_queued_prompt_single_render() {
harness.full_text()
);
// Cancel the promoted turn pre-first-token. The committed block forces
// the standard cancel path (rewind skipped): marker renders, composer
// stays empty, and the prompt count does NOT grow.
// Cancel pre-first-token, while the committed block is still the only
// copy on screen.
harness.inject_keys(keys::CTRL_C).expect("Ctrl+C cancel");
harness
.wait_for_full_text("Turn cancelled by user", Duration::from_secs(15))
@@ -4,9 +4,9 @@ use crate::common::*;
/// Minimal mode paints the welcome card edge-to-edge (no outer horizontal pad).
/// The live region's status / prompt / info rows and committed user+agent blocks
/// must share that left edge — previously they sat at `block_pad_left + accent`
/// (= 3 columns of blank gutter), which looked misaligned against the welcome
/// box. Assert every non-blank visible row either is welcome-card chrome
/// must share that left edge — a `block_pad_left + accent` offset (3 columns
/// of blank gutter) reads as misaligned against the welcome box. Assert every
/// non-blank visible row either is welcome-card chrome
/// (border / logo interior) or starts at column 0.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
@@ -56,8 +56,8 @@ async fn minimal_flush_left_no_hpad() {
);
// Slash menu: typing a command prefix opens the dropdown below the prompt.
// Its rows must be flush-left too — previously the panel sat at the layout
// hpad and the item rows one further column in (` /transcript` at col 3).
// Its rows must be flush-left too — a panel at the layout hpad with item
// rows one further column in (` /transcript` at col 3) is misaligned.
harness
.inject_keys(b"/tra")
.expect("type slash command prefix");
@@ -78,7 +78,7 @@ async fn minimal_flush_left_no_hpad() {
// Permission modal: a scripted `run_terminal_command` tool call (no --yolo)
// opens the prompt-replacing permission modal. Its rows must be flush-left
// too — previously the whole modal sat at the layout hpad (2 columns in).
// too — the modal must not sit at the layout hpad (2 columns in).
// The accent `┃` paints the modal's first column, so a correct row has zero
// leading spaces.
content.set_response("PERMISSION_SETTLED — turn finished after the allow.");
@@ -133,8 +133,8 @@ async fn minimal_flush_left_no_hpad() {
/// For each `needle`, find the first screen line containing it and assert that
/// line has no leading ASCII spaces (flush-left). Welcome-card rows start with
/// box-drawing chars at col 0 already; this targets the previously-padded
/// live/committed content.
/// box-drawing chars at col 0 already; this targets live/committed content
/// that must not carry a leading pad.
fn assert_flush_left_live_rows(screen: &str, needles: &[&str], phase: &str) {
for needle in needles {
let line = screen
@@ -43,7 +43,6 @@ async fn minimal_new_session_keeps_history_and_resets() {
harness.scrollback_text()
);
// `/new` → fresh session: commits a second welcome card and resets the frontier.
inject_keys_paced(&mut harness, b"/new");
harness.inject_keys(b"\r").expect("submit /new");
@@ -59,14 +58,12 @@ async fn minimal_new_session_keeps_history_and_resets() {
harness.full_text()
);
// Prior turn's committed lines remain in native scrollback (not wiped).
assert!(
harness.contains_full_text(&turn_sentinel(1)),
"prior turn must remain in native scrollback after /new\nfull:\n{}",
harness.full_text()
);
// A fresh turn streams in the new session.
content.set_response(format!("{} new session payload.", turn_sentinel(2)));
harness
.inject_keys(b"hi\r")
@@ -51,7 +51,6 @@ async fn minimal_queue_indicator_shows_while_running() {
.wait_for_full_text("#1 second queued prompt", Duration::from_secs(10))
.expect("the snapshot lists the queued text");
// The queued prompt eventually promotes and runs as turn 2.
harness
.wait_for_full_text("STEPTWO", Duration::from_secs(40))
.expect("queued prompt promoted and ran");
@@ -24,7 +24,6 @@ async fn minimal_resize_preserves_committed_scrollback() {
.inject_keys(format!("{PROMPT}\r").as_bytes())
.expect("submit prompt");
// Precondition: the committed head must be in native scrollback first.
let deadline = Instant::now() + Duration::from_secs(40);
while Instant::now() < deadline && !harness.scrollback_text().contains(MOCK_RESPONSE_SENTINEL) {
harness.update(Duration::from_millis(100));
@@ -56,7 +55,6 @@ async fn minimal_resize_preserves_committed_scrollback() {
harness.full_text()
);
// The prompt is still functional: a second turn streams after the resize.
content.set_response(format!("{} after resize.", turn_sentinel(2)));
harness
.inject_keys(b"again\r")
@@ -23,7 +23,6 @@ async fn minimal_settings_modal_opens_and_closes() {
.wait_for_text("Appearance", Duration::from_secs(10))
.expect("settings editor renders inline");
// Esc closes it; the idle prompt status returns and the editor is gone.
harness.inject_keys(keys::ESC).expect("close settings");
harness
.wait_for_text(MINIMAL_IDLE_SENTINEL, Duration::from_secs(10))
@@ -40,26 +40,22 @@ async fn minimal_short_response_stays_on_screen() {
let rows = DEFAULT_ROWS;
// 1) The short response is on the visible screen …
assert!(
harness.screen_contents().contains(MOCK_RESPONSE_SENTINEL),
"short response must stay on the visible screen\nscreen:\n{}",
harness.screen_contents()
);
// 2) … and was NOT force-pushed into native scrollback. Content-anchored: a
// response that fits stays put; only content taller than the screen scrolls
// (proven separately by `minimal_commits_response_to_scrollback`).
// Content-anchored: a response that fits stays put; only content taller than
// the screen scrolls (proven separately by `minimal_commits_response_to_scrollback`).
assert!(
!harness.scrollback_text().contains(MOCK_RESPONSE_SENTINEL),
"short response must not be pushed into scrollback\nscrollback:\n{}",
harness.scrollback_text()
);
// 3) The prompt sits directly after the (short) conversation, HIGH on the
// screen, with the rest of the window left blank below it — NOT pinned to
// the bottom with a big gap above (the regression). The cursor is always
// on the focused prompt, so its row is the robust signal: bottom-pin puts
// it near `rows - 1`; content-anchored keeps it in the upper portion.
// The cursor is always on the focused prompt, so its row is the robust
// signal: bottom-pin would put it near `rows - 1`; content-anchored keeps
// it in the upper portion, directly after the short conversation.
let (cursor_row, _cursor_col) = harness.cursor_position();
assert!(
cursor_row < rows - 12,
@@ -68,10 +64,8 @@ async fn minimal_short_response_stays_on_screen() {
harness.screen_contents()
);
// 4) Nothing is rendered near the bottom of the screen: the last non-blank
// row (the prompt's info bar) is well above the last row. Found explicitly
// (not via trailing padding) so the check is independent of how the
// emulator represents empty rows.
// Find the last non-blank row explicitly (not via trailing padding) so the
// check is independent of how the emulator represents empty rows.
let screen = harness.screen_contents();
let last_non_blank = screen
.lines()
@@ -97,7 +97,6 @@ async fn minimal_transcript_expands_collapsed_thinking() {
)
});
// And the inline TUI survives the suspend/restore round trip.
harness
.wait_for_text(MINIMAL_IDLE_SENTINEL, Duration::from_secs(10))
.expect("inline TUI restored after the pager exited");
@@ -56,8 +56,6 @@ async fn minimal_transcript_opens_in_pager() {
harness.full_text()
);
// The pager process survives the suspend/restore round-trip and returns to
// the idle prompt.
harness
.wait_for_text(MINIMAL_IDLE_SENTINEL, Duration::from_secs(10))
.expect("inline TUI restored after the pager exited");
@@ -4,7 +4,8 @@ use super::common::*;
#[allow(unused_imports)]
use super::scroll::*;
// ── Regression: wheel-path cap — a misclassified flood must not teleport ──
// Regression: wheel-path cap — a misclassified flood must not teleport
// the viewport.
//
// The parent capped only confirmed-trackpad flushes; wheel and Unknown
// streams flushed their whole backlog in one 16ms slot on the assumption
@@ -82,7 +82,6 @@ async fn queue_and_interjection_lifecycle() {
.wait_for_text("STEPTHREE", Duration::from_secs(90))
.expect("steps 5-7: I1 then P2 drained through to the final reply");
// The send-now cancel of turn 1 is silent.
assert!(
!harness.contains_text("Turn cancelled by user"),
"send-now cancel must not render a cancelled marker\nscreen:\n{}",
@@ -166,7 +166,6 @@ async fn quote_block_drag_copy_excludes_bars_pty() {
"quote content cells should carry the selection highlight"
);
// Release to finish the drag and copy.
harness
.inject_keys(sgr_mouse(0, bravo_row, bravo_end, 'm').as_bytes())
.expect("release drag");
@@ -56,7 +56,6 @@ async fn read_tool_header_selection_copies_path_only_pty() {
.inject_keys(format!("{PROMPT}\r").as_bytes())
.expect("submit prompt");
// Wait for the Read header (label + filename) to appear.
harness
.wait_for_text(path_tail, Duration::from_secs(45))
.unwrap_or_else(|_| {
@@ -39,10 +39,14 @@ async fn reasoning_efforts_fallback_menu_matches_builtin() {
.expect("built-in xhigh row");
let screen = harness.screen_contents();
for description in [
"Maximum reasoning", // xhigh
"Heavy reasoning", // high
"Balanced reasoning", // medium
"Faster, lighter reasoning", // low
// xhigh
"Maximum reasoning",
// high
"Heavy reasoning",
// medium
"Balanced reasoning",
// low
"Faster, lighter reasoning",
] {
assert!(
screen.contains(description),
@@ -17,8 +17,8 @@ async fn removed_queued_prompt_never_sent() {
// Hold turn 1 open deterministically: its content streams, but its
// completion is gated until we release it below. This removes the
// turn-end race — the removed row can never be promoted out from under
// the removal, which under load previously let the "removed" prompt run
// as its own turn (consuming TURNTWO) and left the survivor stranded.
// the removal. Without this gate, under load, the "removed" prompt could
// run as its own turn (consuming TURNTWO) and leave the survivor stranded.
content.hold_agent_completions();
let binary = pager_binary().expect("resolve pager binary");
@@ -134,7 +134,8 @@ fn spawn_settled_session(content: &ContentController, project: &Path) -> PtyHarn
harness
.wait_for_text(MOCK_RESPONSE_SENTINEL, Duration::from_secs(30))
.expect("turn rendered");
harness.update(Duration::from_millis(1000)); // let the short turn settle
// Let the short turn settle.
harness.update(Duration::from_millis(1000));
harness
}
@@ -2,7 +2,7 @@
#[allow(unused_imports)]
use super::common::*;
// ── Reproduction: horizontal resize must not lose the scroll position ─────
// Reproduction: horizontal resize must not lose the scroll position.
//
// Bug: when the user has scrolled UP into kigi's scrollback (NOT pinned to the
// bottom / not following) and the terminal is resized HORIZONTALLY, kigi loses
@@ -243,7 +243,7 @@ async fn resize_preserves_scroll_position() {
before.0
);
// ── Resize the WIDTH only (rows unchanged) and let prepare_layout rebuild
// Resize the WIDTH only (rows unchanged) and let prepare_layout rebuild
// the wrapped row map at the new width.
harness
.resize(DEFAULT_ROWS, NARROW_COLS)
@@ -260,7 +260,7 @@ async fn resize_preserves_scroll_position() {
"pager rendered 'panicked' during resize\nscreen:\n{screen_after}"
);
// ── The reproduction assertion: the marker must still be visible AND at
// The reproduction assertion: the marker must still be visible AND at
// ~the same viewport row after a width-only resize.
let (after_row, _) = locate_screen_text(&screen_after, MARKER).unwrap_or_else(|| {
panic!(
@@ -1,4 +1,3 @@
// Per-test-case module for the `pty_e2e` integration test crate.
#[allow(unused_imports)]
use super::common::*;
@@ -20,7 +19,6 @@ async fn reverse_agent_type_mismatch_cursor_to_default() {
let binary = pager_binary().expect("resolve pager binary");
// Start with a model as default via --model flag.
let mut harness = PtyHarness::spawn_with_content(
&binary,
DEFAULT_ROWS,
@@ -47,7 +45,6 @@ async fn reverse_agent_type_mismatch_cursor_to_default() {
.inject_keys(b"/model default-model\r")
.expect("type model switch");
// The question modal should appear.
harness
.wait_for_text("requires starting a new session", Duration::from_secs(15))
.expect("agent type mismatch modal should appear for reverse direction");
@@ -1,4 +1,3 @@
// Per-test-case module for the `pty_e2e` integration test crate.
#[allow(unused_imports)]
use super::common::*;
@@ -8,7 +7,6 @@ use super::common::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn same_agent_type_switch_no_modal() {
// Both models have no agent_type → both use kigi harness.
let content = ContentController::start_with_models(vec![
MockModel::new("model-a"),
MockModel::new("model-b"),
@@ -34,14 +32,12 @@ async fn same_agent_type_switch_no_modal() {
.wait_for_text(MOCK_RESPONSE_SENTINEL, Duration::from_secs(30))
.expect("response rendered");
// Switch to model-b (same agent type).
harness
.inject_keys(b"/model model-b\r")
.expect("type model switch");
// Should switch normally — look for the model name in status bar
// (bottom-right). The toast "✓ Default model: model-b" may be
// transient, so check for "model-b" anywhere on screen.
// The toast "✓ Default model: model-b" may be transient, so check for
// "model-b" anywhere on screen (e.g. the status bar) instead.
harness
.wait_for_text("model-b", Duration::from_secs(15))
.expect("model-b visible on screen after switch");
@@ -1,4 +1,3 @@
// Per-test-case module for the `pty_e2e` integration test crate.
#[allow(unused_imports)]
use super::common::*;
#[allow(unused_imports)]
@@ -6,7 +5,7 @@ use super::scroll::*;
use std::time::Duration;
// ── A8: scroll-debug HUD e2e (`KIGI_SCROLL_DEBUG`) ─────────────────────────
// A8: scroll-debug HUD e2e (`KIGI_SCROLL_DEBUG`)
//
// The HUD is a release-compiled overlay gated purely at runtime, so the
// stock harness binary must (a) show it when the env var is set and (b) show
@@ -119,7 +118,7 @@ async fn scroll_debug_hud_absent_without_env() {
harness.quit().expect("clean quit");
}
// ── A10: `/debug scroll` e2e ────────────────────────────────────────────────
// A10: `/debug scroll` e2e
/// **Command-toggle e2e.** `/debug scroll` typed in the prompt must flip the
/// HUD on without the env var, and a second invocation must flip it off.
@@ -2,11 +2,9 @@
#[allow(unused_imports)]
use super::common::*;
// ── Interactive flow e2e tests ──────────────────────────────────────────
/// 15. **In-session Shift+Tab cycles permission mode.**
/// Routes BackTab through the agent view's `resolve_action`, the path that
/// previously dropped `CycleMode`; test 2b only covers the welcome screen.
/// Routes BackTab through the agent view's `resolve_action`, the path where
/// `CycleMode` can be silently dropped; test 2b only covers the welcome screen.
/// With the auto gate on (client default): Normal → Plan → Auto → Always-Approve → Normal.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
@@ -2,8 +2,6 @@
#[allow(unused_imports)]
use super::common::*;
// ── Small-screen tip must survive a submit into a slow turn ────────────────
//
// Real-usage repro: the tip shows on the welcome→agent promote, the user hits
// Enter under a second later, and the turn takes several seconds. The submit
// used to retire the tip (`clear_on_submit` before the fix cleared every
@@ -22,7 +22,6 @@ async fn assert_tab_focuses_scrollback(content: &ContentController) {
.wait_for_text(MOCK_RESPONSE_SENTINEL, Duration::from_secs(30))
.expect("turn rendered");
// Prompt owns keys before Tab.
assert!(
!harness.contains_text("Space:prompt"),
"precondition: prompt should be focused before Tab\nscreen:\n{}",
@@ -4,54 +4,25 @@ use super::common::*;
#[allow(unused_imports)]
use super::scroll::*;
// ── Regression: trackpad feel — fast flicks must not under-travel ─────────
// Regression: fast trackpad flicks must not under-travel.
//
// User complaint: fast trackpad flicks felt "laggy AND too sensitive" and
// under-traveled. The trackpad flush path had a fixed 6-line per-flush cap:
// at the 16ms cadence that is a hard ~360 lines/s delivery ceiling no matter
// how fast the gesture or how tall the viewport, and whatever backlog was
// still queued when the stream finalized was discarded beyond one more
// 6-line flush — the flick's tail simply evaporated. The fix makes the cap
// proportional (half the viewport per flush, floored at 6) so delivery
// scales with gesture demand, and the finalize flush drains the whole-line
// backlog up to that cap.
// The trackpad flush path used to cap delivery at a fixed 6 lines per 16ms
// flush and discard any backlog still queued when the stream finalized, so
// fast flicks felt "laggy AND too sensitive" and under-traveled regardless
// of gesture speed or viewport height. The fix makes the cap proportional
// (half the viewport per flush, floored at 6) so delivery scales with
// gesture demand, and has the finalize flush drain the backlog up to that
// cap instead of discarding it.
//
// The trackpad cap only binds once a stream is CONFIRMED trackpad, which
// happens mid-stream only on ept=1 brands (their rapid same-direction runs
// promote at the 3rd event; the harness default `TerminalName::Unknown` is
// ept=3, where auto streams stay Unknown until finalize and flush uncapped).
// So this test presents `TERM_PROGRAM=iTerm.app` (ept=1) and floods wheel-up
// reports with `KIGI_SCROLL_SPEED=100` (6x multiplier — an existing user
// setting, used here as a deterministic demand amplifier). Total viewport
// travel is then classification- and jitter-robust:
//
// - Every event contributes at least 1 base line even at the 1.0x accel
// floor, so desired travel is ≥ 40 × 1 × 6 = 240 rows no matter how much
// scheduler jitter stretches the 6ms gaps (jitter only lowers the accel
// multiplier toward 1.0; the speed multiplier is timing-independent). The
// proportional cap (~20 lines/flush at the 50-row PTY) drains that demand
// within the burst plus the 80ms stream-gap window, so travel stays above
// TRAVEL_FLOOR. If extreme stretching (avg gap > 30ms) prevents trackpad
// promotion entirely, the stream is wheel-like and uncapped: travel = 240.
// - Under the parent's fixed 6-line cap the same flood delivers at most 6
// lines per 16ms slot: a ~260ms burst spans ~16 slots (~96 rows), plus the
// post-stop gap window (5 slots + one finalize flush ≈ 36 rows) — ≈ 130
// rows total, with the remaining backlog discarded at finalize. Verified
// against a parent-built binary: travel came out 138 rows (< TRAVEL_FLOOR).
//
// One jitter regime the floor does NOT cover is arrival COMPRESSION (the
// mirror of stretching, per the driver contract in `scroll.rs`): if the
// pager is descheduled for the whole host-paced burst, all reports queue in
// the PTY and arrive as one batch. Accel maxes but delivery is cap-bound to
// one in-batch flush plus the post-stop gap window (~5 cadence slots + one
// finalize flush ≈ 7 flushes × ~20 lines ≈ 140 rows) — a legitimate
// cap-bound outcome numerically indistinguishable from the parent defect
// (138). The regimes separate by CAUSE, visible in the frame count: a
// compressed burst paints ≲7 frames, while the parent's 6-line cap needs
// ~16+ flush frames to travel that far. The detector below softens the
// floor assertion in the compressed regime (distinct message) so a CI stall
// is not misread as the regression; the floor stays a hard assertion in the
// normal regime.
// The proportional cap only binds once a stream is CONFIRMED trackpad,
// which happens mid-stream only on ept=1 brands (their rapid same-direction
// runs promote at the 3rd event; the harness default `TerminalName::Unknown`
// is ept=3 and stays Unknown, flushing uncapped, until finalize). Hence this
// test presents `TERM_PROGRAM=iTerm.app` (ept=1) and floods wheel-up reports
// under `KIGI_SCROLL_SPEED=100` as a deterministic demand amplifier. A
// batched/compressed arrival (pager descheduled through the whole burst) is
// numerically indistinguishable from the old under-travel bug but is
// distinguishable by frame count — see `COMPRESSED_BURST_FRAMES_MAX`.
//
// Only byte-deterministic quantities are asserted (marker indices on the
// final screen, frame counts from the preamble's reset_timing() capture) —
@@ -42,7 +42,6 @@ async fn undo_tip_resets_each_new_session() {
i + 1
);
}
// Cap reached: a further wipe shows nothing for the rest of this run.
wipe_substantial_draft(&mut harness);
harness.update(Duration::from_millis(1000));
assert!(
@@ -38,7 +38,6 @@ async fn undo_tip_session_cap_blocks_fourth_show() {
);
}
// Fourth wipe: the in-memory count is at the cap (3) → gated, no banner.
wipe_substantial_draft(&mut harness);
harness.update(Duration::from_millis(1000));
assert!(
@@ -125,7 +125,6 @@ async fn verb_group_fold_expand_collapse_pty() {
"edit must stay a standalone separator row\nscreen:\n{}",
harness.screen_contents()
);
// Members of a folded run are hidden.
assert!(
!harness.contains_text("a2.txt"),
"folded member must be hidden\nscreen:\n{}",
@@ -43,7 +43,8 @@ fn drag_copy(harness: &mut PtyHarness, needle: &str) -> String {
/// PTY: drag-copy on a verb-group header yields the aggregated label — folded
/// AND expanded — and on the expanded slot the header row and member 0's row
/// are independent targets: dragging the member's path never drags the header
/// label along (the two rows previously shared one selectable range).
/// label along (regression guard: the two rows must not share one
/// selectable range).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "PTY e2e; run the owning pty_e2e_* Cargo test with --ignored (see Cargo.toml)"]
async fn verb_group_header_drag_copy_pty() {
@@ -66,7 +66,6 @@ async fn verify_bashq_claim2_force_interject() {
.inject_keys(CTRL_ENTER)
.expect("send the selected bash row now");
// Send-now: the row executes as real bash NOW, as its own turn.
harness
.wait_for_text("CLAIMTWO_FS_OK", Duration::from_secs(30))
.expect("queued bash executed via send-now");
@@ -35,8 +35,8 @@ async fn waiting_for_model_label_shows_before_first_token() {
.inject_keys(format!("{PROMPT}\r").as_bytes())
.expect("submit prompt");
// The new explicit label, not the old generic "Waiting…". Match without the
// trailing ellipsis so terminal width / glyph handling can't flake it.
// Match without the trailing ellipsis so terminal width / glyph handling
// can't flake it.
harness
.wait_for_text("Waiting for response", Duration::from_secs(10))
.unwrap_or_else(|_| {
@@ -4,30 +4,14 @@ use super::common::*;
#[allow(unused_imports)]
use super::scroll::*;
// ── Infra smoke: timed wheel bursts + viewport markers + frame capture ────
//
// Exercises the scroll-test primitives together against the real pager:
//
// 1. `marker_response` / `marker_screen_row` / `topmost_visible_marker` — a
// transcript of unique one-row `MARKER-nnnn` lines whose topmost visible
// index identifies the viewport position, so "the view scrolled up" is a
// strict index decrease rather than a fragile absolute-row check.
// 2. `send_wheel_burst` / `send_wheel_sequence` — SGR-1006 wheel reports
// written at a controlled inter-event interval (6ms spaced singles here:
// a trackpad stream under the harness terminal's ept=3 classification —
// see the driver doc in `scroll.rs`).
// 3. The harness's live frame capture — `reset_timing()` before the burst,
// `frame_count()` after the drain (the `renders_on_action.rs` pattern).
// Only the byte-deterministic frame count is asserted; no wall-clock.
//
// This is an infrastructure smoke, not a regression repro: it pins the
// contracts scroll-pacing tests rely on (off-screen-top markers scroll INTO
// view, at least one repaint happens, repaints never exceed one per wheel
// event). The frame bound is an AMPLIFICATION bound — real cadence
// coalescing (16ms `REDRAW_CADENCE` → ~12 frames for this burst) is left to
// the behavioral tests. All three assertions hold under either wheel or
// trackpad classification, so they are jitter-proof even when host load
// stretches the nominal 6ms gaps.
// Infrastructure smoke test for the scroll-test primitives (marker
// transcript position tracking, wheel burst/sequence drivers, and the
// harness's live frame capture) against the real pager — not a regression
// repro; see the driver doc in `scroll.rs` for wheel/trackpad
// classification. The frame bound asserted here is an AMPLIFICATION bound
// (repaints never exceed one per wheel event); real cadence coalescing is
// left to the behavioral tests. Only byte-deterministic quantities are
// asserted, so the assertions hold under host-load jitter.
/// Marker count: 120 one-row lines ≫ the 50-row PTY, so early markers sit
/// off-screen-top once the finished stream pins the view to the bottom.
@@ -40,9 +24,9 @@ const BURST_EVENTS: usize = 30;
const BURST_INTERVAL: Duration = Duration::from_millis(6);
/// **Wheel-burst scroll infra smoke.** A closely spaced wheel-up burst over a
/// marker transcript must scroll previously off-screen-top markers into view,
/// without a panic, producing at least one repaint frame and at most one
/// frame per wheel event (no frame amplification).
/// marker transcript must scroll off-screen-top markers into view, without a
/// panic, producing at least one repaint frame and at most one frame per
/// wheel event (no frame amplification).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn wheel_burst_scrolls_viewport_without_frame_amplification() {
@@ -70,8 +54,6 @@ async fn wheel_burst_scrolls_viewport_without_frame_amplification() {
harness.screen_contents()
);
// The viewport scrolled: the topmost visible marker index strictly
// decreased, i.e. a marker that was off-screen-top is now on screen.
let top_after = topmost_visible_marker(&harness).unwrap_or_else(|| {
panic!(
"no marker visible after the wheel burst\nscreen:\n{}",
@@ -87,10 +69,8 @@ async fn wheel_burst_scrolls_viewport_without_frame_amplification() {
harness.screen_contents()
);
// Amplification bound on the live frame capture: the burst repainted at
// least once, and never more than once per wheel event. (Durations are
// not asserted: the no-drain driver means chunks were parsed at drain
// time, and wall-clock is load-sensitive anyway.)
// Durations are not asserted: the no-drain driver means chunks were
// parsed at drain time, and wall-clock is load-sensitive anyway.
let frames = harness.frame_count();
assert!(
frames >= 1,
@@ -4,7 +4,7 @@ use super::common::*;
#[allow(unused_imports)]
use super::scroll::*;
// ── Regression: scroll pacing — one 16ms scroll clock, no ghost frames ────
// Regression: scroll pacing — one 16ms scroll clock, no ghost frames.
//
// User complaint: "scroll is both laggy and too sensitive". Two coupled
// defects in the scroll pipeline:
@@ -93,8 +93,7 @@ async fn wheel_flood_paints_no_ghost_frames() {
harness.screen_contents()
);
// (a) The viewport scrolled: the topmost visible marker index strictly
// decreased, i.e. a marker that was off-screen-top is now on screen.
// (a) viewport moved.
let top_after = topmost_visible_marker(&harness).unwrap_or_else(|| {
panic!(
"no marker visible after the wheel flood\nscreen:\n{}",
@@ -125,7 +124,7 @@ async fn wheel_flood_paints_no_ghost_frames() {
one repaint per event (frame amplification)"
);
// (c) No ghost frames: every frame in the burst window painted movement.
// (c) no ghost frames.
for (i, timing) in harness.frame_timings().iter().enumerate() {
assert!(
timing.chars >= MOVEMENT_CHARS_FLOOR,
@@ -4,7 +4,7 @@ use super::common::*;
#[allow(unused_imports)]
use super::scroll::*;
// ── A7: wheel overscroll at the bottom re-engages follow mode ─────────────
// A7: wheel overscroll at the bottom re-engages follow mode.
//
// User complaint: after wheeling up to read during a streaming turn, wheeling
// back down to the bottom left a "dead wheel tick" before follow mode
@@ -4,7 +4,7 @@ use super::common::*;
#[allow(unused_imports)]
use super::scroll::*;
// ── Regression: streaming must not starve wheel input ─────────────────────
// Regression: streaming must not starve wheel input.
//
// User complaint: "can't scroll while it's streaming". The event loop's
// `select!` is `biased` with the ACP arm above the input arm, and the ACP
@@ -14,13 +14,11 @@ use super::scroll::*;
// `input_rx.is_empty()` and bounds its inner drain, so buffered input is
// serviced within one bounded ACP batch.
//
// This test drives a turn that is still visibly streaming (paced deltas +
// a held completion gate) and wheels up mid-stream, asserting the viewport
// moved BEFORE the turn completed. Determinism does not lean on wall-clock:
// the mock's completion gate holds the turn's terminal SSE event until the
// test releases it, so "the stream had not completed when movement was
// observed" is true by construction; the on-screen witnesses (status label,
// last-chunk sentinel) are content-ordering assertions on top of that.
// Determinism does not lean on wall-clock: the mock's completion gate holds
// the turn's terminal SSE event until the test releases it, so "the stream
// had not completed when movement was observed" is true by construction;
// the on-screen witnesses (status label, last-chunk sentinel) are
// content-ordering assertions on top of that.
//
// Honest scope note: a PTY test cannot force `acp_rx` to be continuously
// non-empty at the exact instant wheel reports land (that interleaving is
@@ -41,7 +41,6 @@ async fn zero_turn_model_switch_no_modal() {
);
}
// No modal should appear.
assert!(
!harness.contains_text("requires starting a new session"),
"modal should NOT appear for zero-turn switch\nscreen:\n{}",
@@ -224,7 +224,6 @@ async fn unknown_brand_malformed_reply_is_discarded() {
"pager never emitted the XTVERSION query for an unknown terminal"
);
// Unterminated DCS reply: a stalled fragment the filter must drop.
harness
.inject_keys(b"\x1bP>|PtyHarnessTerm 9.9")
.expect("inject malformed reply");
@@ -241,7 +241,6 @@ async fn scripted_path_space_hyperlink() {
);
}
/// Count Kitty APC action tokens in raw PTY bytes.
fn count_kitty_actions(raw: &[u8]) -> (usize, usize, usize) {
let text = String::from_utf8_lossy(raw);
let mut transmit_display = 0usize;
+16 -113
View File
@@ -21,9 +21,7 @@ use kigi_tui::views::settings_modal::{
handle_settings_mouse,
};
// ---------------------------------------------------------------------------
// Compile-time exhaustive matrix
// ---------------------------------------------------------------------------
/// Every setting exercised by this file. Must stay in sync with
/// `SettingsRegistry::defaults().all()`.
@@ -101,9 +99,7 @@ fn matrix_is_subset_of_registry() {
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
fn make_state() -> SettingsModalState {
SettingsModalState::new(
@@ -276,9 +272,7 @@ fn assert_set_bool_action(outcome: SettingsKeyOutcome, key: &str, expected: bool
}
}
// ---------------------------------------------------------------------------
// Modal lifecycle — F2 / Esc / Ctrl+,
// ---------------------------------------------------------------------------
#[test]
fn f2_closes_modal_from_any_browse_position() {
@@ -334,9 +328,7 @@ fn esc_in_filter_mode_exits_filter_not_modal() {
assert!(matches!(s.mode, SettingsModalMode::Browse));
}
// ---------------------------------------------------------------------------
// Per-setting keyboard paths
// ---------------------------------------------------------------------------
#[test]
fn space_on_compact_mode_dispatches_typed_setter() {
@@ -505,9 +497,7 @@ fn mouse_click_on_contextual_hints_group_opens_sub_sheet_and_toggles_child() {
);
}
// ---------------------------------------------------------------------------
// Per-setting MOUSE paths (keyboard ↔ mouse parity)
// ---------------------------------------------------------------------------
/// Lay out enough row_rects so that `handle_settings_mouse` can resolve
/// a click to the desired row index. We bypass the renderer here because
@@ -666,10 +656,11 @@ fn mouse_click_at_row_edge_on_focused_row_toggles() {
let mut s = make_state();
synth_rects(&mut s);
let row_y = row_idx_for(&s, "compact_mode") as u16;
// 70 is far right, but still within list_area width=80.
let outcome = handle_settings_mouse(
&mut s,
MouseEventKind::Down(crossterm::event::MouseButton::Left),
70, // far right, within list_area width=80
70,
row_y,
);
assert_set_bool_action(outcome, "compact_mode", true);
@@ -693,10 +684,11 @@ fn mouse_click_on_header_is_a_no_op() {
fn mouse_click_outside_list_is_a_no_op() {
let mut s = make_state();
synth_rects(&mut s);
// 100 is outside list_area (width=80).
let outcome = handle_settings_mouse(
&mut s,
MouseEventKind::Down(crossterm::event::MouseButton::Left),
100, // outside list_area (width=80)
100,
0,
);
assert!(matches!(outcome, SettingsKeyOutcome::Unchanged));
@@ -753,9 +745,7 @@ fn mouse_scroll_up_returns_selection_to_first() {
}
}
// ---------------------------------------------------------------------------
// Filter mode
// ---------------------------------------------------------------------------
/// Filter mode accepts chars into `state.query` and must never leak
/// an `Action`.
@@ -969,11 +959,11 @@ fn filter_backspace_broadens_visible_set() {
// Empty the query out — now everything is visible. We pop one at
// a time and check at each step that the cache regenerates
// (rather than just shrinks).
let _ = handle_settings_key(&mut s, &press(KeyCode::Backspace)); // → "sta"
let _ = handle_settings_key(&mut s, &press(KeyCode::Backspace));
assert_eq!(s.query, "sta");
let _ = handle_settings_key(&mut s, &press(KeyCode::Backspace)); // → "st"
let _ = handle_settings_key(&mut s, &press(KeyCode::Backspace));
assert_eq!(s.query, "st");
let _ = handle_settings_key(&mut s, &press(KeyCode::Backspace)); // → "s"
let _ = handle_settings_key(&mut s, &press(KeyCode::Backspace));
assert_eq!(s.query, "s");
// "s" matches multiple settings (compact_mode via "messages" in
// its description, show_timestamps via "show"/"timestamps",
@@ -1063,9 +1053,7 @@ fn filter_with_multiple_matches_navigates_between_settings() {
assert_eq!(after_pop_keys, vec!["simple_mode"]);
}
// ---------------------------------------------------------------------------
// Filter mode — Enter commits with preserved query
// ---------------------------------------------------------------------------
/// Enter in FilterFocused exits filter focus and preserves the query.
#[test]
@@ -1171,9 +1159,7 @@ fn filter_pageup_pagedown_navigates_in_filter_mode() {
assert_eq!(s.selected, compact_idx);
}
// ---------------------------------------------------------------------------
// Filter mode — g/G filter-aware navigation
// ---------------------------------------------------------------------------
/// `g` lands on the first selectable row (not a header).
#[test]
@@ -1255,9 +1241,7 @@ fn shift_g_jumps_to_last_filtered_row_under_active_filter() {
assert!(matches!(outcome, SettingsKeyOutcome::Unchanged));
}
// ---------------------------------------------------------------------------
// Selection stability
// ---------------------------------------------------------------------------
/// Filter keeps selection when the focused row remains visible.
#[test]
@@ -1276,9 +1260,7 @@ fn filter_keeps_selection_when_currently_selected_row_remains_visible() {
);
}
// ---------------------------------------------------------------------------
// Multi-word AND semantics
// ---------------------------------------------------------------------------
/// One unmatched word in a multi-word query empties the result (AND).
#[test]
@@ -1318,9 +1300,7 @@ fn filter_and_semantics_narrow_strictly() {
);
}
// ---------------------------------------------------------------------------
// Mouse parity under active filter
// ---------------------------------------------------------------------------
/// Lay out `row_rects` for filtered state — only visible rows get rects.
fn synth_rects_filtered(state: &mut SettingsModalState) {
@@ -1396,9 +1376,7 @@ fn mouse_click_at_filtered_out_row_position_is_no_op() {
);
}
// ---------------------------------------------------------------------------
// Filter-rebuild timing — cache stability
// ---------------------------------------------------------------------------
/// `filtered_indices()` is a stable borrow, not regenerated per call.
#[test]
@@ -1428,9 +1406,7 @@ fn filter_cache_pointer_changes_on_query_mutation() {
);
}
// ---------------------------------------------------------------------------
// Render with filter — translation + "No matches"
// ---------------------------------------------------------------------------
/// `scroll_offset` stays within `filtered_indices()` bounds under filter.
#[test]
@@ -1499,9 +1475,7 @@ fn render_no_matches_placeholder_includes_query() {
);
}
// ---------------------------------------------------------------------------
// PickingEnum / EditingValue Esc routing
// ---------------------------------------------------------------------------
/// Esc in `PickingEnum` returns to Browse.
#[test]
@@ -1533,9 +1507,7 @@ fn esc_in_editing_value_mode_returns_to_browse() {
assert!(matches!(s.mode, SettingsModalMode::Browse));
}
// ---------------------------------------------------------------------------
// Registry contracts
// ---------------------------------------------------------------------------
/// Pins which keys belong to each SettingKind.
#[test]
@@ -1845,9 +1817,7 @@ fn setting_value_variants_are_distinct() {
assert_ne!(s, i);
}
// ---------------------------------------------------------------------------
// KeyEventKind filtering
// ---------------------------------------------------------------------------
/// Release events are dropped (kitty-keyboard protocol parity).
#[test]
@@ -1916,9 +1886,7 @@ fn repeat_j_navigation_is_processed() {
}
}
// ---------------------------------------------------------------------------
// d-key reset-to-default
// ---------------------------------------------------------------------------
/// `d` dispatches `Action::OpenResetConfirm` on the focused row.
#[test]
@@ -1998,9 +1966,7 @@ fn d_key_on_header_row_is_unchanged() {
);
}
// ---------------------------------------------------------------------------
// Mouse hit-rect edge cases
// ---------------------------------------------------------------------------
/// Click with empty `row_rects` (partial render) is a no-op.
#[test]
@@ -2043,9 +2009,7 @@ fn scroll_down_at_last_row_is_unchanged() {
);
}
// ---------------------------------------------------------------------------
// Stub tests — `#[ignore] + unimplemented!()` until wired up.
// ---------------------------------------------------------------------------
/// Multi-word AND filter narrows to matching settings + section headers.
#[test]
@@ -2440,13 +2404,11 @@ fn pr4_picker_dispatches_each_theme_settings_action_variant() {
}
}
// ---------------------------------------------------------------------------
// Mouse-path coverage for the new Enum settings.
// The `every_registered_setting_is_exercised`
// test's docstring promises "keyboard test + mouse test" per
// registered key. Earlier only keyboard tests shipped for the 3 new
// enums; these tests close that gap.
// ---------------------------------------------------------------------------
/// Clicking on an Enum row in Browse mode selects it without firing
/// any Action — Enum rows require an explicit Enter to open the
@@ -2532,7 +2494,6 @@ fn pr4_mouse_click_in_theme_picker_is_no_op() {
assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));
}
// ---------------------------------------------------------------------------
// `multiline_mode` (first PAGER-owned setting)
//
// Unlike the SHARED bools (which round-trip through
@@ -2544,7 +2505,6 @@ fn pr4_mouse_click_in_theme_picker_is_no_op() {
//
// These tests mirror the keyboard + mouse coverage promised by
// `ALL_SETTINGS_EXERCISED` — same rigor as `compact_mode` et al.
// ---------------------------------------------------------------------------
/// Keyboard Space on the multiline row dispatches the typed setter
/// with the inverted snapshot value (default false → true). The modal
@@ -2656,9 +2616,7 @@ fn pr5_multiline_mode_renders_under_editor_category() {
);
}
// ---------------------------------------------------------------------------
// permission_mode (security-relevant Enum, no preview)
// ---------------------------------------------------------------------------
/// `permission_mode` lives under the `Agent` section.
#[test]
@@ -2989,9 +2947,7 @@ fn pr6_search_yolo_matches_permission_mode() {
);
}
// ---------------------------------------------------------------------------
// Mouse path tests for permission_mode (keyboard ↔ mouse parity)
// ---------------------------------------------------------------------------
/// First click on unselected `permission_mode` row only selects.
#[test]
@@ -3088,9 +3044,7 @@ fn pr6_mouse_click_on_permission_mode_indicator_opens_picker_in_one_click() {
}
}
// ---------------------------------------------------------------------------
// permission_mode 3-state tests (default/ask/always-approve)
// ---------------------------------------------------------------------------
/// Picking "Default" dispatches `SetPermissionMode(Default)`.
#[test]
@@ -3379,14 +3333,12 @@ fn pr11_permission_mode_kind_is_always_approve_projection() {
// (dispatch_confirm_reset_setting_reset_dispatches_typed_setter_for_*
// + dispatch_confirm_reset_setting_cancel_preserves_modal_state).
// ---------------------------------------------------------------------------
// Render-side tests for the reset-confirm overlay.
//
// These tests assert that the rendered buffer contains the
// confirmation prompt text + breadcrumb + y/n shortcuts. Without
// them, a future change that breaks the overlay's rendering layer
// would silently regress to "user can't see the dialog".
// ---------------------------------------------------------------------------
/// User-feedback follow-up: the reset-confirm overlay applies a
/// uniform "being reset" dim style to **every cell** of the focused
@@ -3606,7 +3558,6 @@ fn docs_footer_renders_for_browse_and_picker() {
}
}
// ---------------------------------------------------------------------------
// User-feedback follow-up: expandable rows + restart pill on
// expand/edit.
//
@@ -3614,7 +3565,6 @@ fn docs_footer_renders_for_browse_and_picker() {
// label line; Left/`h` collapses it. Multiple rows can be expanded
// simultaneously. The "restart" pill renders only while the row is
// expanded (change-time feedback is the toast's job).
// ---------------------------------------------------------------------------
/// Helper: render the modal into a sized buffer and return the full
/// rendered text as a single newline-joined string. Used by the
@@ -3734,8 +3684,8 @@ fn restart_pill_visible_when_expanded() {
/// Edited value (differs from registered default) but collapsed:
/// NO pill. A collapsed non-default row showing it forever misreads
/// as "restart pending" — the exact repro a user hit with a
/// previously-set Off value in a fresh session.
/// as "restart pending" — the exact repro a user hit with an Off
/// value carried over from an earlier session.
#[test]
fn restart_pill_hidden_when_edited_but_collapsed() {
use kigi_shell::agent::config::UiConfig;
@@ -3775,10 +3725,10 @@ fn expanded_description_wraps_to_modal_width() {
// cols. Expand and check that the entire description text is
// present in the buffer.
//
// **Width.** The `→ expand` shortcut was added to the
// Browse footer which can push the footer onto an extra line
// at narrower widths; we render at 80 cols to keep the full
// wrapped description visible.
// **Width.** The `→ expand` shortcut in the Browse footer can
// push the footer onto an extra line at narrower widths; we
// render at 80 cols to keep the full wrapped description
// visible.
navigate_to(&mut s, "permission_mode");
let _ = handle_settings_key(&mut s, &press(KeyCode::Right));
@@ -3958,9 +3908,7 @@ fn reset_confirm_prompt_helper_builds_well_formed_string_for_every_setting() {
}
}
// ---------------------------------------------------------------------------
// String + Int editor + validators
// ---------------------------------------------------------------------------
/// Int stepper: Enter opens, Up/Down/Left/Right step+clamp, Enter commits.
#[test]
@@ -4261,10 +4209,9 @@ fn pr15_int_stepper_rejects_text_input_keys() {
KeyCode::Char('5'),
KeyCode::Char('a'),
KeyCode::Char('-'),
// Extended reject-set.
KeyCode::Char(' '), // Space (the Bool-toggle key in Browse mode)
KeyCode::Char('+'), // Plus (would be a naïve numpad expectation)
KeyCode::Char('.'), // Decimal point
KeyCode::Char(' '),
KeyCode::Char('+'),
KeyCode::Char('.'),
KeyCode::Backspace,
KeyCode::Delete,
KeyCode::Home,
@@ -4376,9 +4323,7 @@ fn pr8_default_model_and_max_thoughts_width_defaults_roundtrip() {
);
}
// ---------------------------------------------------------------------------
// default_selected_permission (Agent Enum, no preview — SHELL-owned, persists)
// ---------------------------------------------------------------------------
/// `default_selected_permission` lives under `Agent` and is SHELL-owned.
#[test]
@@ -4555,9 +4500,7 @@ fn default_selected_permission_picker_esc_does_not_dispatch_action() {
);
}
// ---------------------------------------------------------------------------
// Mouse path tests for default_selected_permission
// ---------------------------------------------------------------------------
/// First click on unselected row only selects.
#[test]
@@ -4644,7 +4587,6 @@ fn default_selected_permission_mouse_click_on_indicator_opens_picker_in_one_clic
}
}
// ---------------------------------------------------------------------------
// `plan_mode` (Agent-category Enum, PAGER-owned + ACP-mediated,
// supports_preview: false)
//
@@ -4659,7 +4601,6 @@ fn default_selected_permission_mouse_click_on_indicator_opens_picker_in_one_clic
// `session/set_mode` request that mutates per-agent state and gates
// tool dispatch. Per-keystroke preview would either fire N round-trips
// per nav OR commit on every keystroke. Both are unacceptable.
// ---------------------------------------------------------------------------
/// `plan_mode` lives under the `Agent` section:
/// pins the category against drift.
@@ -4922,12 +4863,10 @@ fn pr10_plan_mode_choices_use_canonical_strings() {
);
}
// ---------------------------------------------------------------------------
// Mouse path tests for plan_mode (keyboard ↔ mouse parity).
//
// Mirrors the permission_mode / coding_data_sharing mouse tests. Every
// keyboard interaction has a mouse equivalent.
// ---------------------------------------------------------------------------
/// First mouse-click on a DIFFERENT (non-selected) `plan_mode` row
/// only SELECTS the row (no picker entry, no Action). Mirrors the
@@ -5017,7 +4956,6 @@ fn pr10_mouse_click_on_plan_mode_indicator_opens_picker_in_one_click() {
}
}
// ---------------------------------------------------------------------------
// `render_mermaid` (SHELL-owned Enum, Appearance).
//
// Unlike `plan_mode` (PAGER-owned, snapshot-seeded), `render_mermaid` is
@@ -5030,7 +4968,6 @@ fn pr10_mouse_click_on_plan_mode_indicator_opens_picker_in_one_click() {
//
// These tests honor the `ALL_SETTINGS_EXERCISED` contract — keyboard AND
// mouse coverage, same rigor as `plan_mode` / `coding_data_sharing`.
// ---------------------------------------------------------------------------
/// `render_mermaid` lives under `Appearance` and is SHELL-owned (persisted to
/// `[ui].render_mermaid`). Pins the category + owner against drift.
@@ -5202,10 +5139,8 @@ fn render_mermaid_choices_use_canonical_strings() {
);
}
// ---------------------------------------------------------------------------
// Mouse path tests for render_mermaid (keyboard ↔ mouse
// parity). Mirrors the plan_mode mouse tests.
// ---------------------------------------------------------------------------
/// First mouse-click on a DIFFERENT (non-selected) `render_mermaid` row only
/// SELECTS the row (no picker entry, no Action).
@@ -5284,11 +5219,9 @@ fn mouse_click_on_render_mermaid_indicator_opens_picker_in_one_click() {
}
}
// ---------------------------------------------------------------------------
// screen_mode (SHELL Enum, Appearance, restart_required, no preview).
// Catalog [fullscreen, minimal]; product default when unset is fullscreen.
// Session-only switches stay on /minimal and /fullscreen (do not write config).
// ---------------------------------------------------------------------------
/// Enter on the `screen_mode` row opens the picker seeded at the product
/// default `fullscreen` (UiConfig.screen_mode is None → canonical fullscreen).
@@ -5426,11 +5359,9 @@ fn mouse_click_on_screen_mode_indicator_opens_picker_in_one_click() {
}
}
// ---------------------------------------------------------------------------
// hunk_tracker_mode (SHELL Enum, Advanced, restart_required, no preview).
// Catalog [agent_only, all_dirty, off]; `disabled` aliases `off` at parse
// time. Mirrors the render_mermaid enum tests (keyboard ↔ mouse parity).
// ---------------------------------------------------------------------------
/// Enter on the `hunk_tracker_mode` row opens the picker seeded at the
/// default `agent_only`.
@@ -5560,9 +5491,7 @@ fn mouse_click_on_hunk_tracker_mode_indicator_opens_picker_in_one_click() {
}
}
// ---------------------------------------------------------------------------
// CLI batch: show_tips, auto_update (SHELL Bool, restart_required)
// ---------------------------------------------------------------------------
/// Space-toggle on `show_tips` dispatches typed setter.
#[test]
@@ -5678,9 +5607,7 @@ fn pr13_cli_batch_settings_are_discoverable_via_search() {
}
}
// ---------------------------------------------------------------------------
// fork_secondary_model (DynamicEnum, restart_required: false)
// ---------------------------------------------------------------------------
/// `fork_secondary_model` lives under Models.
#[test]
@@ -5784,9 +5711,7 @@ fn pr14_model_family_settings_are_discoverable_via_search() {
}
}
// ---------------------------------------------------------------------------
// vim_mode (scrollback navigation) — PAGER-owned, paired with simple_mode
// ---------------------------------------------------------------------------
/// Keyboard Space on the vim_mode row dispatches the typed setter
/// with the inverted snapshot value (default false → true). Same
@@ -5880,12 +5805,10 @@ fn simple_mode_label_distinguishes_input_from_scrollback() {
assert!(vim.keywords.contains(&"vim"));
}
// ---------------------------------------------------------------------------
// keep_text_selection — SHELL-owned Mouse Enum (`flash` | `hold`)
//
// Mirrors `render_mermaid`: `supports_preview: false`, Enter opens picker,
// commit dispatches `Action::SetKeepTextSelection(TextSelection)`.
// ---------------------------------------------------------------------------
#[test]
fn keep_text_selection_renders_under_mouse_shell_owned() {
@@ -6115,9 +6038,7 @@ fn keep_text_selection_hold_snapshot_seeds_picker_at_hold() {
}
}
// ---------------------------------------------------------------------------
// scroll_speed — SHELL-owned Int under Mouse, no preview
// ---------------------------------------------------------------------------
/// Int stepper open/step/commit for scroll_speed. Defaults to 50;
/// mid-range policy: Up/Down ±1, Left/Right ±5.
@@ -6202,10 +6123,8 @@ fn scroll_speed_renders_under_mouse_shell_owned_bounds_1_to_100() {
}
}
// ---------------------------------------------------------------------------
// scroll_mode — SHELL-owned Mouse Enum (`auto` | `wheel` | `trackpad`),
// no preview (mirrors keep_text_selection).
// ---------------------------------------------------------------------------
#[test]
fn scroll_mode_renders_under_mouse_shell_owned_no_preview() {
@@ -6296,9 +6215,7 @@ fn mouse_click_on_selected_scroll_mode_row_opens_picker() {
}
}
// ---------------------------------------------------------------------------
// scroll_lines — SHELL-owned Int under Mouse (1-10), no preview
// ---------------------------------------------------------------------------
#[test]
fn scroll_lines_renders_under_mouse_shell_owned_bounds_1_to_10() {
@@ -6378,9 +6295,7 @@ fn scroll_lines_mouse_click_opens_editor() {
);
}
// ---------------------------------------------------------------------------
// invert_scroll — SHELL-owned Bool (Mouse, default false)
// ---------------------------------------------------------------------------
#[test]
fn invert_scroll_space_dispatches_typed_setter() {
@@ -6442,9 +6357,7 @@ fn invert_scroll_renders_under_mouse_shell_owned_default_false() {
}
}
// ---------------------------------------------------------------------------
// display_refresh_auto_cadence — SHELL-owned Bool (Appearance, default false)
// ---------------------------------------------------------------------------
#[test]
fn display_refresh_auto_cadence_space_dispatches_typed_setter() {
@@ -6531,9 +6444,7 @@ fn display_refresh_auto_cadence_defaults_roundtrip_via_current_value_for() {
assert_eq!(value, SettingValue::Bool(true));
}
// ---------------------------------------------------------------------------
// show_thinking_blocks — SHELL-owned Bool (Appearance, default true)
// ---------------------------------------------------------------------------
#[test]
fn show_thinking_blocks_space_dispatches_typed_setter() {
@@ -6637,9 +6548,7 @@ fn show_thinking_blocks_renders_under_appearance_category_shell_owned() {
);
}
// ---------------------------------------------------------------------------
// prompt_suggestions — SHELL-owned Bool (Editor, default true)
// ---------------------------------------------------------------------------
#[test]
fn prompt_suggestions_space_dispatches_typed_setter() {
@@ -6743,9 +6652,7 @@ fn prompt_suggestions_renders_under_editor_category_shell_owned() {
);
}
// ---------------------------------------------------------------------------
// respect_manual_folds — PAGER-owned Bool
// ---------------------------------------------------------------------------
#[test]
fn respect_manual_folds_space_dispatches_typed_setter() {
@@ -6820,9 +6727,7 @@ fn respect_manual_folds_renders_under_appearance_category_pager_owned() {
}
}
// ---------------------------------------------------------------------------
// group_tool_verbs — SHELL-owned Bool (Appearance, default true)
// ---------------------------------------------------------------------------
#[test]
fn group_tool_verbs_space_dispatches_typed_setter() {
@@ -6923,9 +6828,7 @@ fn group_tool_verbs_renders_under_appearance_category_shell_owned() {
);
}
// ---------------------------------------------------------------------------
// collapsed_edit_blocks — SHELL-owned Bool (Appearance, default false)
// ---------------------------------------------------------------------------
#[test]
fn collapsed_edit_blocks_space_dispatches_typed_setter() {