Skip to content

Cover the mobile terminal toolbar's input contracts - #947

Merged
selfcontained merged 2 commits into
mainfrom
agt_fa2a215e8a5c/job-test-enforcer-66ac1c33
Aug 13, 2026
Merged

Cover the mobile terminal toolbar's input contracts#947
selfcontained merged 2 commits into
mainfrom
agt_fa2a215e8a5c/job-test-enforcer-66ac1c33

Conversation

@selfcontained

Copy link
Copy Markdown
Owner

Recurring Test Enforcer run. The local suite was green on first pass (25th consecutive) and the CI scan was clean (50/50), so the whole run went to the top next_focus coverage gap.

Why this file

apps/web/src/components/app/mobile-terminal-toolbar.tsx is 396 lines, the only input surface on mobile, and had zero unit tests — including after #935 reworked it with API-backed Paste/Submit. Nearly everything it does is a side effect on something outside its own markup, which is exactly what the existing e2e/mobile-layout.spec.ts check cannot see:

  • the escape sequence handed to the terminal socket
  • the ref the terminal's onData handler reads to fold in the ctrl modifier
  • the inject-text POST that deliberately does not go over the terminal WS

What is pinned

28 tests mounting the real tree (including the real copy-mode banner), with only the HTTP seam, the toaster, the audio cue and framer-motion mocked:

  • the exact escape sequence for all seven shortcut keys, and that everything is inert while disconnected
  • the ctrl modifier arming/disarming ctrlPendingRef, clearing after any key press without rewriting the key, and clearing only the indicator when the terminal announces ctrl-consumed — the toolbar must not write the ref back
  • the 420ms press flash, its next-frame re-arm, its key scoping, and that arming ctrl mid-flash does not blank the key the user just pressed
  • the copy-mode banner appearing for copy/exiting but not for the unknown state the terminal reports before its first poll, and staying inert while disconnected
  • the sound-cue short-circuit not swallowing the key
  • the composer: double-rAF focus (the iOS keyboard workaround), the inject-text payload and URL, the empty-draft short-circuit, the no-agent guard, in-flight gating, and the failure path keeping the draft, the overlay and focus

Mutation battery

35 mutants, 33 killed. Two survivors, both diagnosed rather than papered over:

  • the if (!isConnected) return inside sendKey is redundant with the button's disabled attribute — mutating the pair together does die
  • the onSuccess draft clear is unobservable, because the overlay unmounts on close

One real gap surfaced mid-battery and was fixed: the failure-path refocus assertion was passing either way, because the composer still held focus from the open. A second was caught by review 680 and fixed in the follow-up commit.

Notes for readers of this test file

Fake timers use an explicit toFake list that addsrequestAnimationFrame — the component drives both the flash and the focus workaround through rAF, and leaving rAF real while setTimeout is faked would strand those callbacks. jotai state is injected with a per-test createStore() because soundCuesEnabledAtom reads localStorage at module-eval time.

Validation

check, format (on a warm tree), test (server 2715 / web 807→835 / ext 60 / scripts 4), test:e2e (180 passed, 12 skipped), test:e2e:live (11 passed, no leaked e2e-* tmux sessions), finalize:web. The new file was run three times standalone with identical results.

No product code changed.

🤖 Generated with Claude Code

selfcontainedand others added 2 commits August 12, 2026 20:14
The toolbar is the only input surface on mobile and had zero unit tests
after #935 reworked it with API-backed Paste/Submit. Almost everything it
does is a side effect on something outside its own markup — the escape
sequence handed to the terminal socket, the ref the terminal's onData
handler reads to fold in the ctrl modifier, and the inject-text POST that
deliberately does not go over the terminal WS — so none of it was
observable from the existing E2E check.
Mounts the real tree (including the copy-mode banner) with only the HTTP
seam, the toaster and the audio cue mocked, and pins: the exact escape
sequence for all seven shortcut keys; the ctrl modifier arming/disarming
ctrlPendingRef and clearing on the terminal's ctrl-consumed announcement
without writing the ref back; the 420ms press flash and its
next-frame re-arm; the copy-mode banner appearing for copy/exiting but
not for the "unknown" state the terminal reports before its first poll;
the sound-cue short-circuit; and the composer's inject-text payload,
empty-draft short-circuit, no-agent guard, in-flight gating, and
failure path keeping the draft, the overlay and focus.
33 of 35 mutants killed. The two survivors are diagnosed, not gaps: the
!isConnected guard inside sendKey is redundant with the button's disabled
attribute (mutating the pair together dies), and the onSuccess draft
clear is unobservable because the overlay unmounts on close.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review catch: the test named "does not flash the ctrl toggle" only
asserted on the other controls, so it stayed green if the ctrl button
started rendering the flash class or the ctrl handler started arming a
flash. Now it asserts the ctrl button carries neither the flash token nor
the animation class, and — since the flash slot is shared and
triggerFlash blanks it first — that arming ctrl mid-flash leaves the key
the user just pressed still lit.
That last assertion is what makes a flashing ctrl handler observable at
all: adding flashButtonClass("ctrl") to the button alone is inert,
because nothing ever sets a flash keyed "ctrl".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@selfcontained
selfcontained merged commit a6ec304 into mainAug 13, 2026
1 check passed
@selfcontained
selfcontained deleted the agt_fa2a215e8a5c/job-test-enforcer-66ac1c33 branch August 13, 2026 02:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@selfcontained