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:
@@ -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{}",
|
||||
|
||||
Reference in New Issue
Block a user