| 1 | # v0.9.2 terminal / key / modal acceptance matrix (#3758) |
| 2 | |
| 3 | Status vocabulary, used literally: |
| 4 | |
| 5 | - **PASS** — an automated cell exists, and this document names the test that |
| 6 | covers it. It does **not** claim the test was run in this session; see |
| 7 | "How to run" below. |
| 8 | - **UNRUN** — no automated coverage. Every `UNRUN` row is a literal physical |
| 9 | observation in a real terminal emulator or over a real SSH link, which a |
| 10 | pseudo-terminal reproduces the byte protocol of but not the emulator. |
| 11 | |
| 12 | Nothing in this document asserts a test result. It records which cells have |
| 13 | automated acceptance and which remain human work. |
| 14 | |
| 15 | ## What is automated, and where |
| 16 | |
| 17 | | Test target | File | |
| 18 | | --- | --- | |
| 19 | | Provider-free terminal/key/modal matrix | `crates/tui/tests/terminal_matrix_qa.rs` | |
| 20 | | Running-turn contract against a loopback mock provider | `crates/tui/tests/release_runtime_qa.rs` | |
| 21 | | Shortcut-copy canonical-action audit | `crates/tui/src/tui/keybindings.rs` (unit tests) | |
| 22 | | PTY harness, terminal-mode ledger, view-stack trace reader | `crates/tui/tests/support/qa_harness/` | |
| 23 | |
| 24 | Supporting machinery added for this matrix: |
| 25 | |
| 26 | - `support/qa_harness/modes.rs` — replays the raw PTY control stream into a |
| 27 | DEC-private-mode ledger. Terminal-mode restoration is invisible on the |
| 28 | rendered frame, so the control stream is the only truthful evidence. |
| 29 | - `support/qa_harness/view_log.rs` — reads the product's existing |
| 30 | `codewhale_tui::view_stack` trace records (push/pop, `ModalKind`, depth). |
| 31 | Modal coverage asserts on those records rather than on "the frame changed", |
| 32 | because a replaced modal and a closed modal render identically. |
| 33 | - `Harness::transcript` / `terminal_modes` / `diagnostics` — every bounded wait |
| 34 | fails with the frame **and** the mode ledger. |
| 35 | |
| 36 | ### How to run |
| 37 | |
| 38 | ``` |
| 39 | cargo test -p codewhale-tui --test terminal_matrix_qa --locked -- --test-threads=1 |
| 40 | cargo test -p codewhale-tui --test release_runtime_qa --locked -- --test-threads=1 |
| 41 | cargo test -p codewhale-tui --bin codewhale-tui --locked keybindings |
| 42 | ``` |
| 43 | |
| 44 | These are Unix-gated (`#![cfg(unix)]`); Windows ConPTY input plumbing is a |
| 45 | separate audit and its rows are `UNRUN` below. |
| 46 | |
| 47 | ## Automated cells |
| 48 | |
| 49 | ### Terminal geometry |
| 50 | |
| 51 | | Cell | Status | Covered by | |
| 52 | | --- | --- | --- | |
| 53 | | 80x24 classic | PASS | `size_matrix_keeps_the_composer_visible_and_inside_the_viewport` | |
| 54 | | 120x40 laptop | PASS | same | |
| 55 | | 60x20 narrow split | PASS | same | |
| 56 | | 48x14 tiny pane | PASS | same | |
| 57 | | 200x50 wide | PASS | same | |
| 58 | | Live resize across all of the above, composer stays typable | PASS | same | |
| 59 | | No row overflows the viewport; cursor stays in bounds | PASS | same | |
| 60 | |
| 61 | ### `TERM` / `COLORTERM` capability tiers |
| 62 | |
| 63 | | Cell | Status | Covered by | |
| 64 | | --- | --- | --- | |
| 65 | | `xterm-256color` + `COLORTERM=truecolor` | PASS | `capability_matrix_honors_the_advertised_color_tier` | |
| 66 | | `xterm-256color`, no truecolor claim → no 24-bit SGR emitted | PASS | same | |
| 67 | | `xterm` (unknown tier) → no 24-bit SGR emitted | PASS | same | |
| 68 | | `screen-256color` (tmux-style) → no 24-bit SGR emitted | PASS | same | |
| 69 | | `NO_COLOR=1` present → still boots and paints | PASS | same | |
| 70 | | `CODEWHALE_ASCII_SAFE=1` narrows decorative glyphs; default tier differs | PASS | `ascii_safe_tier_removes_decorative_glyphs_the_default_tier_paints` | |
| 71 | |
| 72 | `NO_COLOR` is deliberately a boot-and-paint row only. The palette does not |
| 73 | honor `NO_COLOR` today, and this matrix does not invent a contract for it. |
| 74 | |
| 75 | ### Paste and IME |
| 76 | |
| 77 | | Cell | Status | Covered by | |
| 78 | | --- | --- | --- | |
| 79 | | Bracketed single line | PASS | `paste_matrix_lands_in_the_composer_without_autosubmitting` | |
| 80 | | Bracketed multiline | PASS | same | |
| 81 | | Bracketed with trailing newline (no auto-submit) | PASS | same | |
| 82 | | Bracketed CJK / wide glyphs | PASS | same | |
| 83 | | Raw (unbracketed) multiline | PASS | same | |
| 84 | | Large payload (~2 KB) | PASS | same | |
| 85 | | IME-style per-character commits are typing, not paste | PASS | `ime_style_commits_are_typing_and_the_following_enter_submits` | |
| 86 | | Lone trailing commit + Enter still submits | PASS | same | |
| 87 | | Bracketed paste over SSH stays in the composer | PASS | `qa_pty.rs::paste_bracketed_from_macos_client_into_linux_ssh_stays_in_composer` (pre-existing) | |
| 88 | | Repeated Enter after a raw paste still submits | PASS | `qa_pty.rs::paste_unbracketed_then_repeated_enter_still_submits` (pre-existing) | |
| 89 | |
| 90 | ### Mouse, resize, focus |
| 91 | |
| 92 | | Cell | Status | Covered by | |
| 93 | | --- | --- | --- | |
| 94 | | Click / drag / wheel never decode as composer text | PASS | `mouse_resize_and_focus_events_never_reach_the_composer_as_text` | |
| 95 | | Focus lost / gained never decode as text | PASS | same | |
| 96 | | `FocusGained` re-establishes focus reporting and bracketed paste | PASS | same | |
| 97 | | Resize during a focus transition keeps the draft | PASS | same | |
| 98 | | Wheel/click ownership on real work-surface rows | PASS | `qa_pty.rs::work_surface_real_rows_own_click_wheel_and_resize` (pre-existing) | |
| 99 | |
| 100 | ### Modals |
| 101 | |
| 102 | | Cell | Status | Covered by | |
| 103 | | --- | --- | --- | |
| 104 | | Help (F1) opens and Esc closes to depth 0 | PASS | `every_provider_free_modal_opens_and_escape_returns_to_the_composer` | |
| 105 | | Command palette (Ctrl+K) | PASS | same | |
| 106 | | Context inspector (`/context`) | PASS | same | |
| 107 | | Live transcript (`/transcript`) | PASS | same | |
| 108 | | Theme picker (`/theme`) | PASS | same | |
| 109 | | Skills manager (`/skills`) | PASS | same | |
| 110 | | Composer is typable again after the full sweep | PASS | same | |
| 111 | | Approval modal review + deny without side effect | PASS | `qa_pty.rs::approval_modal_keeps_wheel_for_review_and_denies_without_side_effect` (pre-existing) | |
| 112 | | Help overlay advertises exactly the chords this matrix drives | PASS | `help_overlay_advertises_the_chords_this_matrix_drives` | |
| 113 | |
| 114 | Modals that need a provider, a saved session, or a live Fleet to open |
| 115 | deterministically (model picker, provider picker, fleet setup/roster, setup |
| 116 | wizard, session picker, worktree manager) are **not** listed as PASS. They are |
| 117 | not physical-terminal work either — they are follow-on automation, tracked as a |
| 118 | gap rather than claimed. |
| 119 | |
| 120 | ### Running-turn contract |
| 121 | |
| 122 | | Cell | Status | Covered by | |
| 123 | | --- | --- | --- | |
| 124 | | Enter queues while a turn is running | PASS | `release_runtime_qa.rs::release_queued_follow_ups_dispatch_exactly_once_and_in_order` | |
| 125 | | Composer advertises `↵ queue` while busy | PASS | same | |
| 126 | | Stash chords are never advertised as send/queue/steer | PASS | same, plus `help_overlay_advertises_the_chords_this_matrix_drives` | |
| 127 | | Queued follow-ups dispatch exactly once, in order (no drop, no double send) | PASS | `release_queued_follow_ups_dispatch_exactly_once_and_in_order` | |
| 128 | | Ctrl+Enter steers the running turn | PASS | `release_runtime_qa.rs::release_ctrl_enter_steers_running_turn` (pre-existing) | |
| 129 | | Shift+Enter / Alt+Enter / Ctrl+J stay newlines during a running turn | PASS | same | |
| 130 | | Empty Enter promotes the next queued follow-up | PASS | `release_runtime_qa.rs::release_empty_enter_promotes_queued_follow_up` (pre-existing) | |
| 131 | | Explicit queue dispatch (`/queue send <n>`) exists as the named action | PASS | `release_empty_enter_promotes_queued_follow_up` asserts the advertised copy | |
| 132 | |
| 133 | ### Shortcut copy — one canonical action per key |
| 134 | |
| 135 | | Cell | Status | Covered by | |
| 136 | | --- | --- | --- | |
| 137 | | No key advertises two actions without a context qualifier | PASS | `keybindings.rs::every_advertised_key_names_exactly_one_canonical_action` | |
| 138 | | Ctrl+G / Ctrl+S documented once, as stash only | PASS | `keybindings.rs::stash_chords_advertise_stashing_and_nothing_else` | |
| 139 | | Enter / Ctrl+Enter / newline chords each own one verb | PASS | `keybindings.rs::running_turn_verbs_belong_to_one_chord_each` | |
| 140 | | Bare `v` never advertised; details is Alt+V | PASS | `keybindings.rs::tool_details_documents_alt_v_only_never_bare_v` (pre-existing) | |
| 141 | | Alt+? never advertised; F1 / Ctrl+/ are the help paths | PASS | `keybindings.rs::help_advertises_f1_and_ctrl_slash_never_alt_question` (pre-existing) | |
| 142 | |
| 143 | The one intentional double-listing is `Ctrl+C` / `Ctrl+C (selection)`: cancel |
| 144 | versus copy, disambiguated in the advertised chord itself. The audit permits |
| 145 | exactly that shape and rejects an unqualified second meaning. |
| 146 | |
| 147 | ### Terminal-mode restoration |
| 148 | |
| 149 | | Cell | Status | Covered by | |
| 150 | | --- | --- | --- | |
| 151 | | Ctrl+D on an empty composer restores every mode | PASS | `terminal_modes_are_restored_on_every_exit_path` | |
| 152 | | SIGINT restores every mode | PASS | same | |
| 153 | | Exit from a non-256-color terminal restores every mode | PASS | same | |
| 154 | | Keyboard-enhancement stack fully unwound (no `^[[>5u` residue, #1583) | PASS | same | |
| 155 | | Cursor restored visible after the alternate screen | PASS | same | |
| 156 | | The leak guard actually covers the modes the TUI enables | PASS | `the_leak_guard_covers_the_modes_the_tui_enables` | |
| 157 | |
| 158 | ## UNRUN — physical observation only |
| 159 | |
| 160 | Every row here needs a human at a real terminal. A PTY delivers the same bytes, |
| 161 | but it is not the emulator: it has no font stack, no GPU renderer, no |
| 162 | Option-key mapping, no window manager, and no network path. |
| 163 | |
| 164 | | Cell | Status | Why it cannot be automated here | |
| 165 | | --- | --- | --- | |
| 166 | | iTerm2 — rendering, colors, wide glyphs | UNRUN | Emulator-specific renderer and font shaping | |
| 167 | | iTerm2 — `Cmd+V` paste, `⌥` chords, Cmd-forwarding config | UNRUN | Depends on the emulator's own key remapping | |
| 168 | | macOS Terminal.app — 256-color fallback appearance | UNRUN | The visual result of the tier, not the emitted bytes | |
| 169 | | macOS Terminal.app — Option-as-Meta behaviour | UNRUN | Emulator preference, not a byte protocol | |
| 170 | | WezTerm — kitty keyboard protocol chords (Ctrl+Shift+A/O/T) | UNRUN | Requires the emulator's enhanced-keyboard implementation | |
| 171 | | WezTerm — mouse selection and OSC 52 clipboard | UNRUN | Requires a real clipboard and window system | |
| 172 | | Real SSH session — paste, latency, resize propagation | UNRUN | Needs a real network path and remote tty | |
| 173 | | tmux inside a real emulator — nested capability negotiation | UNRUN | Nested terminal behaviour, not reproducible in a bare PTY | |
| 174 | | Windows Terminal / ConPTY — input plumbing | UNRUN | The PTY suite is `#![cfg(unix)]`; ConPTY needs its own audit | |
| 175 | | System notification banner appearance | UNRUN | macOS attributes it to the unbundled `osascript` host (#4847) | |
| 176 | |
| 177 | Physical observations, when they are made, belong in a separate record. Do not |
| 178 | fold a one-off visual observation into the PASS tables above — a flaky or |
| 179 | unrepeatable observation carries different weight than a test that runs on |
| 180 | every change, and merging them would make both less trustworthy. |
| 181 | |
| 182 | ## Known gaps |
| 183 | |
| 184 | - Provider-dependent modals (model/provider/fleet/setup/session/worktree) have |
| 185 | no automated open/close cell yet. |
| 186 | - The matrix is Unix-only. |
| 187 | - `NO_COLOR` has boot coverage, not behavioural coverage, because it is not |
| 188 | currently a contract. |
| 189 |