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:
+2
-2
@@ -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)));
|
||||
|
||||
+1
-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");
|
||||
|
||||
|
||||
-3
@@ -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))
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+3
-3
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
-2
@@ -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{}",
|
||||
|
||||
-1
@@ -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");
|
||||
|
||||
+4
-2
@@ -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 "❯ "
|
||||
|
||||
+2
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -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);
|
||||
|
||||
+3
-3
@@ -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!(
|
||||
|
||||
+2
-5
@@ -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
|
||||
|
||||
+2
-3
@@ -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
|
||||
|
||||
-3
@@ -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")
|
||||
|
||||
-1
@@ -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");
|
||||
|
||||
-2
@@ -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")
|
||||
|
||||
-1
@@ -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))
|
||||
|
||||
+7
-13
@@ -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()
|
||||
|
||||
-1
@@ -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");
|
||||
|
||||
+2
-1
@@ -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");
|
||||
|
||||
-1
@@ -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(|_| {
|
||||
|
||||
+8
-4
@@ -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
|
||||
|
||||
-1
@@ -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(|_| {
|
||||
|
||||
+13
-33
@@ -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,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+6
-8
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user