Skip to content

test(tui): extend coverage gate to Ink components + App.tsx (closes #1501)#1538

Merged
cliffhall merged 10 commits into
v2/mainfrom
tui/1501-ink-component-coverage
Jun 29, 2026
Merged

test(tui): extend coverage gate to Ink components + App.tsx (closes #1501)#1538
cliffhall merged 10 commits into
v2/mainfrom
tui/1501-ink-component-coverage

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #1501.

Summary

Lifts the interim React-surface coverage exclusion in the TUI client (the follow-up to #1484 / #1532). The whole clients/tui/src tree now clears the uniform 90/90/90/90 per-file gate with no React-surface exclusions — App.tsx, every Ink component, and the useSelectableList hook are all exercised under coverage.

Test-only change: no source files were modified.

How it's tested

Added ink-testing-library and renderer-based tests that mount components in-process and drive keypresses through stdin. Two shared passthrough doubles in __tests__/helpers/ make the components mountable under the non-TTY test renderer:

  • inkScrollViewMock.tsx — the real ScrollView renders a placeholder minimap and never mounts its children in a non-TTY env; the double renders children directly and stubs the imperative ref API (scrollBy/scrollTo/getViewportHeight).
  • inkFormMock.tsx — the real ink-form Form is hard to drive field-by-field; the double fires onSubmit on Enter.

Per-component approach:

  • Tabs + hook (Info/Auth/Resources/Prompts/Tools/Notifications/History/Requests, useSelectableList, SelectableItem, Tabs): assert on rendered frame text and drive list navigation / scroll keys.
  • Modals (Tool/Resource/Prompt test modals, DetailsModal): these render position="absolute", which produces an empty frame under the renderer, so their tests assert on behavior — injected InspectorClient fakes being called, onClose, and state transitions — while the passthrough ScrollView still mounts the inner JSX for coverage.
  • App.tsx (~1885 lines): mounts against a controllable mock of the entire @inspector/core surface (InspectorClient, the 7 managed-state classes, the 8 react hooks, transport, auth) plus the callback server. 67 tests drive the keyboard state machine, per-tab content, modal-open flows, OAuth (guided/quick/clear, success + error), and connect/disconnect. Each render is unmounted in afterEach (concurrent ink mounts share raw-mode stdin and interfere with useInput).

Verification

  • clients/tui npm run validate — clean (format / lint / build / test).
  • clients/tui npm run test:coveragegreen, 260 tests, every file ≥90 on all four dimensions, no ERROR lines. App.tsx: 94.6 stmts / 90.6 branch / 94.5 funcs / 96.6 lines.

Config

clients/tui/vitest.config.ts: removed all React-surface entries from the coverage exclude (only the pure re-export tui-servers.ts and test/type globs remain) and widened the include glob to *.test.tsx. The interim-scope comment from #1484 is updated to reflect the completed lift.

🤖 Generated with Claude Code

cliffhall and others added 7 commits June 27, 2026 17:34
Add ink-testing-library and renderer-based tests for useSelectableList,
SelectableItem, and Tabs. Widen the vitest include glob to .test.tsx and
lift these files from the interim React-surface coverage exclusion. The
blanket src/components/**/*.tsx exclude is expanded into an explicit
per-file list so remaining components can be lifted incrementally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1501)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1501)

Add ink-testing-library tests for the 12 Ink components (InfoTab, AuthTab,
ResourcesTab, PromptsTab, ToolsTab, NotificationsTab, HistoryTab,
RequestsTab, and the Tool/Resource/Prompt test modals + DetailsModal),
mounting them through the ink-scroll-view / ink-form passthrough doubles
and driving keypresses via stdin. Modal components render position:absolute
(empty frame under the test renderer) so their tests assert on behavior —
injected InspectorClient fakes and onClose — while the passthrough
ScrollView still mounts the inner JSX for coverage.

Lift these 12 components from the interim React-surface exclusion in
vitest.config.ts; only App.tsx remains excluded. All files clear the
uniform 90/90/90/90 per-file gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mock the full @inspector/core surface (InspectorClient, the 7 managed-state
classes, the 8 react hooks, transport + auth) behind a controllable harness,
plus the ink-scroll-view/ink-form passthroughs, so App renders under
ink-testing-library and its keyboard state machine can be driven via stdin.
Concurrent ink mounts share raw-mode stdin, so each render is unmounted in
afterEach. Foundation covers server-list nav, tab accelerators, connect/
disconnect, OAuth-tab gating, and connected-status rendering (~50% baseline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xclusion (#1501)

Extend App.test.tsx to 67 tests driving the keyboard state machine, tab
content, modals, OAuth flows, and connect/disconnect across the controllable
@inspector/core mock. App.tsx now clears the 90/90/90/90 per-file gate
(94.6/90.6/94.5/96.6).

Remove the last React-surface exclusion (src/App.tsx) from
clients/tui/vitest.config.ts. The whole clients/tui/src tree is now under
the uniform per-file coverage gate with no React-surface exclusions,
completing the interim-scope lift from #1484.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1501)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cliffhall cliffhall added the v2 Issues and PRs for v2 label Jun 27, 2026
@cliffhall cliffhall linked an issue Jun 28, 2026 that may be closed by this pull request
3 tasks
…ns (#1501)

Add targeted App.tsx tests for the OAuth handlers' non-Error throw arms
(quick onCallback, guided start/advance, run-to-completion) and a re-entrant
quick-auth guard, lifting App.tsx branch coverage 90.6% -> 91.48% for a
safer margin above the 90 gate.

Harden the App renderer tests against the heavier v8 coverage instrumentation,
where React+ink's multi-macrotask render scheduling races fixed ticks:
- press() now settles two ticks per key so focus/tab changes flush before the
  next key is routed (ink re-subscribes useInput on re-render);
- expectFrame()/waitUntil() poll for frame content and async flow state
  (callbackServer opts) instead of asserting after a single fixed tick;
- ResourcesTab's content-clear test polls for fetched content likewise.

Plain 'npm test' (the CI path) was already stable; this makes the local
coverage gate stable too (13/13 clean runs vs ~2/8 flaky before).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Follow-up: bumped App.tsx branch coverage from 90.6% → 91.48% for a safer margin above the 90 gate — added tests for the OAuth handlers' non-Error throw arms (quick onCallback, guided start/advance, run-to-completion) and a re-entrant quick-auth guard.

Also hardened the App renderer tests against the heavier v8 coverage instrumentation (where React+ink's multi-macrotask render scheduling races fixed ticks): press() now settles two ticks per key, and frame/async-state assertions poll via expectFrame/waitUntil instead of asserting after a single tick. Plain npm test (the CI path) was already stable; this makes the local coverage gate stable too — 13/13 clean runs vs ~2/8 flaky before.

CI flaked on AuthTab's 'renders quick auth results' under the plain test run:
its oauthFlowState is populated by a post-mount useEffect reading the
inspectorClient, and a single-macrotask tick can race that effect -> setState
-> re-render chain on a slow/loaded CI runner.

Make each component test file's tick() flush several macrotask cycles so such
chains settle deterministically before assertions (no test relies on precise
tick timing — no fake timers / debounce). App.test.tsx already polls via
expectFrame/waitUntil and passed CI, so it's unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 2m 39s —— View job


Review: test(tui) coverage gate extension (#1538)

  • Read PR diff vs origin/v2/main
  • Review test approach & helper mocks
  • Review vitest config changes
  • Verify suite + coverage locally
  • Post review feedback

Verified this is a purely additive, test-only change — git diff origin/v2/main...HEAD over clients/tui/src/** is empty, so no production code was touched. I ran both gates locally:

  • npm test265 passed (23 files)
  • npm run test:coveragegreen, every file ≥90 on all four dimensions. App.tsx lands at 94.78 / 91.48 / 94.54 / 96.64 (stmts/branch/funcs/lines), matching the PR description.

This is high-quality work. The tests are genuinely behavior-driven, not coverage-padding: the OAuth state machine, per-server tab gating, modal open/close flows, and connect/disconnect are all exercised through real keypresses and asserted on rendered frames or injected-fake call sites. The two passthrough doubles are the right call and are well-documented at their definition. The decision to assert on behavior for the position="absolute" modals (which render empty frames under the non-TTY renderer) while still mounting the inner JSX for coverage is a reasonable, clearly-explained tradeoff.

Observations (all non-blocking)

  1. MaxListenersExceededWarning still printed during the run. Both npm test and test:coverage emit:

    MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 resize listeners added to [Socket].

    App.test.tsx:205 raises process.stdout.setMaxListeners(50), but the warning is on a [Socket] and persists — the modal test files (ToolTestModal, ResourceTestModal, PromptTestModal, DetailsModal) each mount components that also register a process.stdout "resize" listener per mount, and those files don't raise the limit. It's cosmetic (the listeners are cleaned up on unmount; this is just transient overlap), but it adds noise to CI logs that a future reader may mistake for a real leak. Consider hoisting setMaxListeners into a shared vitest setupFiles entry so every renderer test file inherits it rather than only App.test.tsx. Fix this →

  2. Shared mutable global globalThis.__INK_FORM_SUBMIT_VALUE__. The ink-form double reads form-submit values from a global. It's cleaned up in ToolTestModal.test.tsx's afterEach, but ResourceTestModal/PromptTestModal/App tests also rely on the same global — worth a quick confirm that each file resets it in afterEach (or that the default-{} fallback makes a stale value harmless). Since vitest isolates module state per test file this is safe today, but it's a latent cross-test coupling if these are ever merged into one file.

  3. Timing model. The suite leans on fixed setTimeout ticks plus waitUntil/expectFrame polling. This is inherent to driving Ink through stdin and you've already hardened it (the follow-up comment's 13/13 clean-run result is the right kind of evidence). No action needed — flagging only so reviewers know the polling helpers are deliberate, not accidental.

  4. vitest.config.ts exclude list is now correctly reduced to just the pure re-export (tui-servers.ts) plus test/type globs, and the interim-scope comment from CLI/TUI: add source coverage gates (in-process runCli + TUI flows) #1484 is updated to reflect the completed lift. This matches the AGENTS.md direction that the TUI React surface was an interim exclusion pending TUI: extend coverage gate to Ink components (lift interim exclusion) #1501. 👍

Verdict

Solid, well-documented test-only PR that delivers exactly what #1501 asked for: the full clients/tui/src tree under the uniform 90/90/90/90 gate with no React-surface exclusions. Nothing here blocks merge — the items above are minor cleanups, the warning in (1) being the most worth a follow-up.
· branch tui/1501-ink-component-coverage

…me-once ink-form global (#1501)

Review follow-ups on #1538 (both non-blocking cleanups):

- Hoist process.std{out,in}.setMaxListeners into a shared vitest setupFile
  (__tests__/setup.ts) wired via setupFiles, so every renderer test file
  inherits it and the MaxListenersExceededWarning is silenced across the
  whole suite — not just App.test.tsx. Remove the per-file call.
- Make the ink-form double consume (clear) __INK_FORM_SUBMIT_VALUE__ on
  submit, eliminating the latent cross-test coupling on the shared global
  regardless of how the modal test files are organized.

No production code changed; suite + coverage gate stay green (App.tsx
94.8/91.5/94.5/96.6), and the warning no longer prints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Thanks for the thorough review! Addressed the two actionable items in ce033056:

  1. MaxListenersExceededWarning — hoisted process.std{out,in}.setMaxListeners(100) into a shared vitest setupFile (clients/tui/__tests__/setup.ts) wired via setupFiles, so every renderer test file inherits it. Removed the per-file call in App.test.tsx. The warning no longer prints under npm test or test:coverage.
  2. Shared __INK_FORM_SUBMIT_VALUE__ global — the ink-form double now consumes the value (clears it) on submit, so a value set by one test can't leak into a later submit regardless of how the modal test files are organized. The latent cross-test coupling is gone at the source.

(3) and (4) needed no action. Suite + coverage gate stay green (App.tsx 94.8/91.5/94.5/96.6).

@claude review

@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 1m 48s —— View job


Re-review: review follow-ups in ce033056

  • Read the diff for the two addressed items
  • Verify setupFile wiring + max-listeners fix
  • Verify ink-form consume-once global fix
  • Run suite + confirm warning gone

Both follow-ups are correctly addressed. Verified locally — 265 passed (23 files) and grep -i MaxListeners over the run output is clean (no warning prints).

1. MaxListenersExceededWarning → shared setupFile ✅

__tests__/setup.ts raises both process.stdout.setMaxListeners(100) and process.stdin.setMaxListeners(100), wired via setupFiles: ["./__tests__/setup.ts"] in vitest.config.ts. Since a setupFile runs once per test file (each worker), every renderer test file now inherits the raised cap — not just App.test.tsx, whose per-file call is correctly removed. Including stdin as well as stdout is a nice touch (ink's raw-mode useInput registers stdin listeners too). The comment accurately explains why it's transient (listeners removed on unmount, no real leak) so a future reader won't mistake it for masking a bug.

2. __INK_FORM_SUBMIT_VALUE__ consume-once ✅

The ink-form double now reads-then-deletes the global on submit:

const value = g.__INK_FORM_SUBMIT_VALUE__ ?? {};
delete g.__INK_FORM_SUBMIT_VALUE__;

This kills the latent cross-test coupling at the source — a value set by one test can no longer leak into a later submit regardless of file organization, and the ?? {} fallback keeps the default-empty behavior. The fix is in the right place (the shared helper), the JSDoc is updated to document the consume semantics, and it removes the reliance on per-file afterEach cleanup that observation (2) flagged. The one behavioral implication — a test submitting twice must re-set the global before each Enter — is the correct, intended contract and matches the documented "set before pressing Enter" usage.

Verdict

Both non-blocking items from the prior review are resolved cleanly, the change stays purely test-only, and the suite + coverage gate remain green (App.tsx 94.8 / 91.5 / 94.5 / 96.6 per the PR description). LGTM. 👍
· branch tui/1501-ink-component-coverage

@cliffhall cliffhall merged commit f43fafc into v2/main Jun 29, 2026
1 check passed
@cliffhall cliffhall deleted the tui/1501-ink-component-coverage branch June 29, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TUI: extend coverage gate to Ink components (lift interim exclusion)

1 participant