Skip to content

Increase Ink composer and input-state coverage (Fixes #2018) - #3375

Merged
acoliver merged 2 commits into
dev/0.12.0from
issue2018
Aug 30, 2026
Merged

Increase Ink composer and input-state coverage (Fixes #2018)#3375
acoliver merged 2 commits into
dev/0.12.0from
issue2018

Conversation

@acoliver

@acoliver acoliver commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

TLDR

Test coverage for the Ink surfaces that decide whether the user can type: composer visibility, input active-state gating, startup readiness, escape priority, and large-paste handling.

The only production change is a behaviour-preserving extraction: the isInputActive expression inside the unexported useInputFinish becomes an exported pure computeIsInputActive. Same expression, same short-circuit order, same truthiness semantics, real hook return types rather than widened ones. Everything else in the diff is tests, one tmux script, and one fixture.

Reviewers should look hardest at two things. First, the hasActiveDialog drift guard in DefaultAppLayout.test.tsx, which uses a Proxy to record every property the real predicate reads and asserts that set equals the test table. Second, whether the new tests actually have teeth; I broke the corresponding production code for each one and recorded the failures below.

Dive Deeper

What had no coverage

  • isInputActive was computed inline inside an unexported hook, so nothing exercised the predicate.
  • Composer.tsx and Notifications.tsx had no test file at all.
  • InlineContent.test.tsx always passed isInputActive: false, so the branch that actually renders the composer was never taken.
  • DefaultAppLayout.test.tsx covered 2 of the 25 flags read by hasActiveDialog.
  • handleLargePaste and expandLargePastePlaceholders had no tests; only the below-threshold branch was touched, indirectly.

What the tests now prove

Composer visible when command initialization completes. The predicate flips false to true when slashCommands goes from undefined to loaded, with an empty array counting as loaded. InlineContent renders the real Composer and real InputPrompt and shows the default, vim and shell placeholders.

Composer hidden only for intentional blocking states. The streaming-state case is table-driven over Object.values(StreamingState) rather than a hand-listed set, so a newly added state is automatically covered and defaults to hidden. initError and isProcessing are each proven independently sufficient with every other input held at its ready value.

Dialogs suppress the composer. Every flag, not a hand-picked subset. The guard test wraps the base UI state in a Proxy, calls the real hasActiveDialog, and asserts the recorded read-set equals the test table, which catches drift in both directions. This also surfaced that createBaseUIState was missing isPoliciesDialogOpen.

Startup errors render actionable UI. Notifications renders the composed Initialization Error: ... line plus the remediation line; prefers a fuller matching history error, proven with a non-error distractor and an unrelated error distractor ahead of the match so that "render the first error item" would fail; suppresses all of it while Responding; and renders warnings alongside an init error.

Escape priority ordering, limited to the combinations the existing suites did not already cover: shell-path suggestions close while shell mode stays active, and dismissing slash suggestions leaves the buffer intact without arming the double-escape prompt.

Large-paste placeholders: line threshold, character threshold, the three-line boundary, cursor position derived from placeholder length, distinct labels for successive pastes, and round-trip expansion through the real InputPrompt submit path.

One tmux startup smoke asserting the first meaningful frame contains the composer, using a dedicated offline fake-provider fixture so it needs no credentials.

Teeth checks

Each new test was verified by breaking the production code it covers and observing the failure, then restoring the code:

Production code broken Test that failed
expandLargePastePlaceholders call removed from inputPromptHooks.ts paste submit tests, receiving [4 lines pasted #1] instead of the body
dialog ternary in MainControls forced to the Composer branch all 25 dialog cases, DIALOG_MANAGER_RENDERED absent
completion.resetCompletionState() removed from handleEscapeKey escape test, clear still present in the frame
uiState.isNarrow added to hasActiveDialog the drift guard, reporting the extra key

Decisions worth flagging

AC4.2 withdrawn. The plan originally asked for a case proving the shell-mode branch of handleEscapeKey beats the completion branch. That state cannot occur: useCommandCompletion passes reverseSearchActive || shellModeActive as the disable flag to useSlashCompletion, and inputPromptRender.tsx will not render slash suggestions in shell mode. A first draft covered it by mocking useCommandCompletion into that impossible state; it was removed rather than kept. The reasoning is recorded in the plan document.

New file instead of appending. The escape tests are in a new InputPrompt.escape.test.tsx. Appending them to InputPrompt.completion.test.tsx pushed that file past the 800-line lint budget. The new file is not on the legacy typecheck-exclusion list in packages/cli/tsconfig.json, so unlike its five siblings it is fully typechecked.

Assertions are on rendered output, not spies. The DialogManager and Composer doubles in DefaultAppLayout.test.tsx now render text sentinels instead of being asserted via toHaveBeenCalledTimes, since mock-call verification is not evidence under dev-docs/RULES.md.

A flaky draft was fixed. An earlier version of the shell-path test completed against the repository root. It was slow and its assertion depended on the checkout's directory listing, and it failed under full-suite load. It now completes against a dedicated temporary directory.

Known unrelated flake

One full-suite run showed a failure in packages/agents/src/core/turn.watchdog.test.ts, "whole-stream liveness ... => NO timeout". That test is byte-identical to main, this PR touches no file in packages/agents, and the test uses real timers with a 30ms idle threshold against eight 10ms sleeps, so heavy parallel load can erase the margin. It passes 5/5 in isolation on this branch and 3/3 under synthetic CPU load on a clean main worktree. Flagging it rather than hiding it; CI is the arbiter.

Follow-up, not changed here

No live call site currently assigns a non-null initError. The InitErrorBox path may be dead. Noted in the plan document; changing production behaviour was out of scope for a coverage issue.

Reviewer Test Plan

git fetch origin issue2018 && git checkout issue2018

# The new and modified suites
bun test packages/cli/src/ui/containers/AppContainer/hooks/useAppInput.predicate.test.ts
bun test packages/cli/src/ui/components/Notifications.test.tsx
bun test packages/cli/src/ui/components/inputPromptText.test.ts
bun test packages/cli/src/ui/components/InputPrompt.escape.test.tsx
bun test packages/cli/src/ui/components/InputPrompt.paste.test.tsx
bun test packages/cli/src/ui/layouts/InlineContent.test.tsx
bun test packages/cli/src/ui/layouts/DefaultAppLayout.test.tsx

To confirm the tests are not decorative, break the production code and watch them fail:

# 1. Drift guard: add `uiState.isNarrow,` to the dialogFlags array in
#    packages/cli/src/ui/layouts/DefaultAppLayoutHelpers.tsx
bun test packages/cli/src/ui/layouts/DefaultAppLayout.test.tsx   # guard fails

# 2. Escape: comment out `completion.resetCompletionState();` in the
#    `completion.showSuggestions` branch of handleEscapeKey in
#    packages/cli/src/ui/components/inputPromptKeyHandlers.ts
bun test packages/cli/src/ui/components/InputPrompt.escape.test.tsx   # fails

# 3. Paste: stop calling expandLargePastePlaceholders in
#    packages/cli/src/ui/components/inputPromptHooks.ts
bun test packages/cli/src/ui/components/InputPrompt.paste.test.tsx   # fails

git checkout -- packages/cli/src/ui

The tmux smoke is gated and does not run by default:

LLXPRT_E2E_TMUX=1 bun test scripts/tests/interactive-ui.test.ts

Interactively, the behaviour under test is the composer itself: start the CLI and confirm the Type your message or @path/to/file prompt appears, then check that opening a dialog (/theme, /settings) replaces it, that Escape closes an open suggestion list without clearing what you typed, and that pasting more than four lines collapses to a [N lines pasted #1] placeholder which expands back to the full text on submit.

Testing Matrix

🍏 🪟 🐧
npm run
npx
Docker
Podman - -
Seatbelt - -

Verified on macOS: npm run format, npm run lint, npm run typecheck and npm run build all exit 0. The full npm run test run had the single unrelated packages/agents watchdog flake described above and no failures in any file this PR touches. The tmux lane is gated behind LLXPRT_E2E_TMUX=1 and was validated for JSON shape and registration rather than executed locally.

Linked issues / bugs

Fixes #2018

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability of composer visibility and mode-specific placeholders across default, Vim, and shell input modes.
    • Large pasted content is represented compactly while typing and expands correctly on submission.
    • Escape handling now consistently dismisses suggestions without unexpectedly changing input state.
    • Notifications and startup states provide more dependable behavior across error and loading scenarios.
  • Tests

    • Expanded automated coverage for input handling, dialogs, notifications, paste behavior, and interactive startup rendering.

@github-actions github-actions Bot added the maintainer:e2e:ok Trusted contributor; maintainer-approved E2E run label Aug 27, 2026
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dbac7fe4-c90a-45ac-97f0-67b2768d632f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 11b0cedd-8405-46e9-b2f9-a485cf1f39b1

📥 Commits

Reviewing files that changed from the base of the PR and between 3549572 and 0fe0b09.

⛔ Files ignored due to path filters (2)
  • project-plans/issue2018-ink-composer-input-state-coverage.md is excluded by !project-plans/**
  • scripts/tmux-script.issue2018-composer-visibility-smoke.json is excluded by !scripts/tmux-script.*.json
📒 Files selected for processing (10)
  • packages/cli/src/ui/components/InputPrompt.escape.test.tsx
  • packages/cli/src/ui/components/InputPrompt.paste.test.tsx
  • packages/cli/src/ui/components/Notifications.test.tsx
  • packages/cli/src/ui/components/inputPromptText.test.ts
  • packages/cli/src/ui/containers/AppContainer/hooks/useAppInput.predicate.test.ts
  • packages/cli/src/ui/containers/AppContainer/hooks/useAppInput.ts
  • packages/cli/src/ui/layouts/DefaultAppLayout.test.tsx
  • packages/cli/src/ui/layouts/InlineContent.test.tsx
  • scripts/fixtures/issue2018-composer-visibility.responses.jsonl
  • scripts/tests/interactive-ui.test.ts

Included review availability: Your plan provides up to 4 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

The PR adds tests for InputPrompt, input activity, composer visibility, dialog routing, notifications, and startup rendering. It also extracts computeIsInputActive and validates the composer through component, hook, and tmux tests.

Changes

Composer and input-state coverage

Layer / File(s) Summary
Input escape and paste behavior
packages/cli/src/ui/components/InputPrompt.escape.test.tsx, packages/cli/src/ui/components/InputPrompt.paste.test.tsx, packages/cli/src/ui/components/inputPromptText.test.ts
Tests cover Escape priority, large-paste placeholders, placeholder expansion, cursor handling, pending pastes, and newline normalization.
Input active-state predicate
packages/cli/src/ui/containers/AppContainer/hooks/useAppInput.ts, packages/cli/src/ui/containers/AppContainer/hooks/useAppInput.predicate.test.ts
Adds IsInputActiveInputs and computeIsInputActive, then tests streaming, initialization, processing, and slash-command conditions.
Composer and dialog layout coverage
packages/cli/src/ui/layouts/InlineContent.test.tsx, packages/cli/src/ui/layouts/DefaultAppLayout.test.tsx
Tests mode-specific composer placeholders, inactive input, all active-dialog flags, and the no-dialog composer path.
Startup notifications and smoke validation
packages/cli/src/ui/components/Notifications.test.tsx, scripts/fixtures/issue2018-composer-visibility.responses.jsonl, scripts/tests/interactive-ui.test.ts
Tests notification states and validates a tmux startup frame containing the composer without an initialization error.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Merge Risk: 🔵 Low · up to 0fe0b

The PR is largely test-only and does not change production behavior, but one added paste test contains a TypeScript error that the repository typecheck does not cover. The change is mergeable with explicit owner awareness or follow-up to ensure the test is type-checked.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 9 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The reviewable changes address composer visibility, input-state gating, startup errors, dialog suppression, escape handling, paste handling, component and hook combinations, and the tmux smoke test ha… Inspect scripts/tmux-script.issue2018-composer-visibility-smoke.json, which was excluded by the !scripts/tmux-script.*.json path filter, to verify that the gated tmux smoke test meets issue #2018 requirements. The remaining linked-issue obj…
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The changes remain within scope for issue #2018. They add focused tests, a behavior-preserving predicate extraction required for testability, a related fixture, and a tmux startup smoke test.
Title check ✅ Passed The title clearly summarizes the main change: increased Ink composer and input-state test coverage. It is concise, specific, and references issue #2018.
Description check ✅ Passed The description includes all required template sections and provides detailed scope, testing instructions, test results, known limitations, and the linked issue. The testing matrix is partially unveri…
Full details: Linked Issues check

Explanation

The reviewable changes address composer visibility, input-state gating, startup errors, dialog suppression, escape handling, paste handling, component and hook combinations, and the tmux smoke test harness. Verification of the tmux smoke implementation is incomplete because the referenced script is excluded from review.

Resolution

Inspect scripts/tmux-script.issue2018-composer-visibility-smoke.json, which was excluded by the !scripts/tmux-script.*.json path filter, to verify that the gated tmux smoke test meets issue #2018 requirements. The remaining linked-issue objectives are supported by the reviewable changes.

Full details: Docstring Coverage

Explanation

Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 9 files. (1 skipped: 1 unsupported.)

Full details: Description check

Explanation

The description includes all required template sections and provides detailed scope, testing instructions, test results, known limitations, and the linked issue. The testing matrix is partially unverified, but the description is otherwise complete.

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue2018

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Before this PR, the CLI’s Ink composer and input-state behavior were only indirectly exercised, and the logic deciding whether input was active lived inline inside the app-input hook, making it harder to verify in isolation. After this PR, that input-active determination is extracted into a reusable pure helper without changing runtime behavior, and the affected UI paths—composer visibility, input prompt interactions, notifications, and layout rendering—are covered by new unit and smoke tests.

Release Notes

New Features

  • Adds a tmux-based end-to-end smoke test verifying composer visibility on startup, including an automation script and response fixture.

Bug Fixes

Tests

  • Adds unit test coverage for Ink composer, input prompt, notifications, layout, and input-active predicate behavior.

Documentation

  • Adds a project plan outlining test coverage improvements for Ink composer and input-state components.

Refactor

  • Extracts input-active determination into a reusable pure helper without changing runtime behavior.

Chore

Changes

Layer File(s) Summary
core packages/cli/src/ui/containers/AppContainer/hooks/useAppInput.ts Extracts input-active determination into a reusable pure helper without changing runtime behavior.
tests packages/cli/src/ui/components/inputPromptText.test.ts, packages/cli/src/ui/components/InputPrompt.escape.test.tsx, packages/cli/src/ui/components/Notifications.test.tsx, packages/cli/src/ui/components/InputPrompt.paste.test.tsx, packages/cli/src/ui/layouts/InlineContent.test.tsx, packages/cli/src/ui/layouts/DefaultAppLayout.test.tsx, packages/cli/src/ui/containers/AppContainer/hooks/useAppInput.predicate.test.ts Adds unit test coverage for Ink composer, input prompt, notifications, layout, and input-active predicate behavior.
smoke scripts/tests/interactive-ui.test.ts, scripts/tmux-script.issue2018-composer-visibility-smoke.json, scripts/fixtures/issue2018-composer-visibility.responses.jsonl Adds tmux-based end-to-end smoke test verifying composer visibility on startup, including automation script and response fixture.
docs project-plans/issue2018-ink-composer-input-state-coverage.md Project plan outlining test coverage improvements for Ink composer and input-state components.

Magnitude

🎯 2 (M)
1479 additions, 98 deletions, 12 changed files across 1 package, 0 acceptance criteria

Related

Pre-merge Checks

Check Status Note
Title Clear and descriptive; states the coverage area and fixes #2018.
Description Contains all required sections: TLDR, Dive Deeper, Reviewer Test Plan, Testing Matrix, and Linked issues / bugs.
Linked Issues Addresses the core acceptance criteria from #2018: composer visibility, input active-state gating, startup readiness, escape handling, and paste behavior. Minor gap: Ctrl-C behavior is mentioned in the issue's suggested coverage but not explicitly covered by new tests in this PR.
Out of Scope Ctrl-C behavior is listed in issue #2018's suggested coverage but no new Ctrl-C tests are included; this may be deferred or assumed covered by existing suites. The production code change is limited to extracting computeIsInputActive, with no behavior changes.

Walkthrough generated by LLxprt PR Review. Planner issue: #2256

Comment thread packages/cli/src/ui/components/InputPrompt.paste.test.tsx
Comment thread packages/cli/src/ui/layouts/DefaultAppLayout.test.tsx
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

OpenCodeReview — automatic reviews suspended

Automatic OCR reviews are suspended for this PR after 2 of 2 automatic reviews.

To get more reviews you can:

  • Check the box below to re-enable automatic reviews (resets the counter), or

  • Comment /review, /ocr, or /open-code-review to request a single review on demand.

  • Re-enable automatic reviews


OpenCodeReview — PR #3375

  • Reviewed head SHA: 7642575a641c5ba3ddc33d34c94215c6fe16a17e
  • Merge base: c48987421f1dcdc527fce4b47e3d56d8260f67d8
  • Range: full from c48987421f1dcdc527fce4b47e3d56d8260f67d8
  • Range fallback: base-sha-changed
  • Scope: selected 12 file(s), +1453/-98; cumulative 12 file(s), +1453/-98
  • Tokens: 665090 total (493938 input, 171152 output, 273024 cache)
  • OCR version: open-code-review v1.8.4 (e78474478) linux/amd64 built at: 2026-08-01T03:27:37Z https://github.com/alibaba/open-code-review
  • Phase: review
  • Exit code: 0
  • Run: https://github.com/vybestack/llxprt-code/actions/runs/33084716511
  • 8 finding(s) (2 posted inline).
  • Artifacts: ocr-review-output contains raw JSON, stdout, stderr, preview, phase, and exit-code diagnostics.

Findings routed to summary

  • packages/cli/src/ui/components/InputPrompt.escape.test.tsx: [maintainability/low] > The vi.spyOn(terminalCapabilityManager, 'isKittyProtocolEnabled') spy set up in beforeEach is never referenced in any test assertion or used to control test behavior. This is dead code that adds noise to the test setup and could mislead future maintainers into thinking Kitty keyboard protocol behavior is being tested in this file.
  • packages/cli/src/ui/components/InputPrompt.escape.test.tsx: [test/low] > The first test sends the escape key via rendered.stdin.write('\x1B') without wrapping in act(), while the second test does wrap it in act(). This inconsistency could lead to flaky tests or React warnings about state updates outside of act. For reliability, all state-triggering interactions should be wrapped in act().
  • packages/cli/src/ui/layouts/InlineContent.test.tsx: [maintainability/low] > Test helper createComposerUIState returns an incomplete UIState shape and bypasses type checking with as never (line 104). This silently accepts stale mocks when production types change. Provide the missing required fields from the current UIState interface and remove the as never cast so the compiler validates the mock shape.
  • packages/cli/src/ui/layouts/InlineContent.test.tsx: [maintainability/low] > Test helper createUIActions returns an incomplete UIActions shape and bypasses type checking with as never (line 145). If production actions change, these tests may drift silently. Either provide the current required UIActions fields or convert to a partial context provider pattern; at minimum remove as never.
  • packages/cli/src/ui/layouts/InlineContent.test.tsx: [maintainability/low] > Test helper createComposerUIState returns an incomplete UIState shape and bypasses type checking with as never. If production UIState evolves, these tests may silently drift. Provide the current required UIState fields and remove the as never cast so the compiler validates the mock.
  • packages/cli/src/ui/layouts/InlineContent.test.tsx: [maintainability/low] > Test helper createUIActions returns an incomplete UIActions shape and bypasses type checking with as never. If production UIActions evolves, these tests may silently drift. Provide the current required UIActions fields and remove the as never cast so the compiler validates the mock.
  • WARNING: Changed-file coverage 3/10 preview files covered is below the 90% threshold. WARNING: 3 file read/review failure(s) detected.

@acoliver

Copy link
Copy Markdown
Collaborator Author

OCR review triage

Both inline findings examined against the source. Both rejected, with evidence.

1. InputPrompt.paste.test.tsx:317 — kitty-protocol mock leaking into later suites

Rejected: factually incorrect. The mock cannot leak, because the outer beforeEach re-establishes it for every test in the file.

packages/cli/src/ui/components/InputPrompt.paste.test.tsx:162

beforeEach(() => {
  vi.resetAllMocks();
  ...

and in the same outer beforeEach, at line 276:

mockedUseKittyKeyboardProtocol.mockReturnValue({
  enabled: false,
  checking: false,
});

The outer beforeEach runs before the inner one for every test, so the enabled: true value set inside describe('large paste submission') is discarded by vi.resetAllMocks() and replaced with the enabled: false default before any subsequent test body runs. The reverse search suite named in the finding is therefore unaffected.

Adding a reset in the inner afterEach would be dead code. Left as is.

2. DefaultAppLayout.test.tsx:286showPrivacyNotice treated as an active dialog

Rejected as out of scope, and the premise does not hold either.

The suggestion is to change hasActiveDialog to exclude banner-only flags. That is a production behaviour change on a test-coverage issue, so it is out of scope regardless of merit.

On merit, showPrivacyNotice is not banner-only today. DialogManager.tsx:675 early-returns the full-screen dialog:

if (uiState.showPrivacyNotice) {
  return (
    <PrivacyNotice onExit={state.handlePrivacyNoticeExit} config={config} />

PrivacyNotice takes over the frame and owns onExit, so suppressing the composer while it is up is the correct and coherent behaviour, not a mismatch. The test records what the code does and what the code should do, and those agree.

The drift guard makes the opposite direction safe too: if someone later decides a privacy notice should be non-modal and removes the flag from hasActiveDialog, the Proxy-based guard fails loudly and points at the exact key, so this test will not silently block that change; it will ask for the table to be updated alongside it.

The Ink surfaces that decide whether the user can type had no direct tests.
`isInputActive` was computed inline inside an unexported hook; `Composer.tsx`
and `Notifications.tsx` had no test file at all; `InlineContent.test.tsx`
always passed `isInputActive: false`, so the branch that actually renders the
composer was never taken; `DefaultAppLayout.test.tsx` covered 2 of the 25
dialog flags; and the large-paste placeholder path had no coverage.

Production change, and the only one: extract the inline predicate in
useAppInput.ts into an exported pure `computeIsInputActive`. Same expression,
same short-circuit order, same truthiness for `!initError` / `!isProcessing` /
`!!slashCommands`, and the input types are the real hook return types rather
than widened ones. `useInputFinish` delegates to it.

Coverage added, by behaviour:

- Composer visible when command initialization completes. The predicate flips
  false to true when `slashCommands` goes from undefined to loaded (an empty
  array counts as loaded), and `InlineContent` renders the real
  Composer -> InputPrompt with the default, vim and shell placeholders.
- Composer hidden only for intentional blocking states. The streaming-state
  case is table-driven over `Object.values(StreamingState)`, so a newly added
  state is automatically covered and defaults to hidden. `initError` and
  `isProcessing` are each proven independently sufficient with every other
  input held ready.
- Dialogs suppress the composer. Driven off the full flag list rather than a
  hand-picked subset, and guarded by a Proxy that records every property the
  real `hasActiveDialog` reads, asserting the recorded set equals the test
  table. That catches drift in both directions; adding `uiState.isNarrow` to
  the predicate fails the guard. `createBaseUIState` was also missing
  `isPoliciesDialogOpen`, which this surfaced.
- Startup errors render actionable UI. `Notifications` renders the composed
  "Initialization Error: ..." line plus the remediation line, prefers a fuller
  matching history error (proven with a non-error distractor and an unrelated
  error distractor ahead of it), suppresses everything while Responding, and
  renders warnings alongside an init error.
- Escape priority ordering, for the combinations the existing suites did not
  cover: shell-path suggestions close while shell mode stays active, and
  dismissing slash suggestions leaves the buffer intact without arming the
  double-escape prompt.
- Large-paste placeholders: line and character thresholds, the three-line
  boundary, cursor position relative to the placeholder, distinct labels, and
  round-trip expansion through the real `InputPrompt` submit path so that
  removing the `expandLargePastePlaceholders` call site fails a test.
- One tmux startup smoke asserting the first meaningful frame contains the
  composer, using a dedicated offline fake-provider fixture.

Test-quality notes:

Assertions are on rendered output. The `DefaultAppLayout` doubles now render
text sentinels instead of being spied on, because asserting mock call counts
is not evidence under dev-docs/RULES.md. The new tests were verified to have
teeth by breaking the corresponding production code and observing the
expected failures: disabling `expandLargePastePlaceholders` breaks the paste
submit tests, forcing the composer branch breaks the dialog tests, removing
`completion.resetCompletionState()` breaks the escape test, and adding a flag
to `hasActiveDialog` breaks the drift guard. Production was restored in each
case.

The escape tests live in a new `InputPrompt.escape.test.tsx` rather than being
appended to `InputPrompt.completion.test.tsx`, which would have pushed that
file past the 800-line lint budget. The new file is not on the legacy
typecheck-exclusion list, so unlike its siblings it is fully typechecked.

An earlier draft of the shell-path test completed against the repository root.
That was slow and its assertion depended on the checkout's directory listing,
so it failed under full-suite load. It now completes against a dedicated
temporary directory.

AC4.2 as originally scoped is withdrawn and recorded in the plan. It asked to
prove the shell-mode branch of `handleEscapeKey` beats the completion branch,
but `useCommandCompletion` passes `reverseSearchActive || shellModeActive` as
the disable flag to `useSlashCompletion`, so that combination cannot occur.
Covering it would have required mocking the UI into an impossible state.

Also noted for follow-up, not changed here: no live call site currently
assigns a non-null `initError`.

Fixes #2018
@acoliver acoliver added this to the 0.12.0 milestone Aug 27, 2026
@acoliver
acoliver changed the base branch from main to dev/0.12.0 August 27, 2026 14:52
Comment thread packages/cli/src/ui/components/InputPrompt.escape.test.tsx
Comment thread packages/cli/src/ui/layouts/DefaultAppLayout.test.tsx
@acoliver

Copy link
Copy Markdown
Collaborator Author

OCR review triage (post-retarget run)

One accepted and fixed, one rejected with evidence.

1. DefaultAppLayout.test.tsx — drift guard only records get traps

Accepted and fixed. The observation is correct. hasActiveDialog today reads every flag with a plain property access, so the get trap covered it, but the guard made a promise it could not keep for other access patterns: in, hasOwnProperty, Object.keys, for...in. A future edit using any of those would have slipped past a green guard, which is exactly the failure mode the guard exists to prevent.

The Proxy now also traps has, and rejects enumeration outright rather than pretending to verify it:

  • has records the key, so 'x' in uiState is accounted for like a read.
  • ownKeys and getOwnPropertyDescriptor throw with a message telling the next person to update the guard and ACTIVE_DIALOG_FLAGS together. Enumeration hands the predicate every flag at once, which makes a recorded read-set meaningless; failing loudly is honest, silently passing is not.
  • Symbol keys throw for the same reason rather than being dropped.

Verified against all three patterns the finding named, each applied to hasActiveDialog and then reverted:

Pattern injected into hasActiveDialog Guard result
'isNarrow' in uiState fails, reporting + "isNarrow" in the read-set diff
Object.keys(uiState) fails: enumerated the UI state via ownKeys
Object.prototype.hasOwnProperty.call(uiState, 'isNarrow') fails: enumerated the UI state via getOwnPropertyDescriptor

Before this change, pattern A passed silently. DefaultAppLayoutHelpers.tsx was restored to zero diff after each check; the suite is 27 pass / 0 fail.

2. InputPrompt.escape.test.tsx:345useState inside mockImplementation violates the Rules of Hooks

Rejected. The mock replaces useCommandCompletion, which is itself a custom hook. React's rule is that hooks may be called from function components or from custom hooks, and the mock occupies exactly the position the real hook occupied: called unconditionally, once per render, from InputPrompt's render body. A stateful stand-in for a stateful hook is not a rules violation; it is the same contract the real implementation satisfies.

The state is not incidental, either. An earlier revision of this test used a plain getter over a closure variable rather than useState. It did not work: resetCompletionState flipped the variable but nothing re-rendered, so the suggestion stayed on screen and the test failed. Driving a real re-render is the point, because the assertion is on the rendered frame rather than on the stub's internals.

That assertion is also what gives the test teeth. Commenting out completion.resetCompletionState() in handleEscapeKey fails it with clear still present in the frame.

Moving the state into a wrapper component would add a layer whose only job is to re-render the component the mocked hook already re-renders, without changing what is proven. Left as is.

Review remediation for PR #3375.

The Proxy-based guard in DefaultAppLayout.test.tsx recorded only `get` traps.
`hasActiveDialog` reads every flag with a plain property access today, so the
guard was accurate for the current implementation, but it promised more than it
could deliver: a future edit using `in`, `hasOwnProperty`, `Object.keys` or
`for...in` would have reached flags absent from ACTIVE_DIALOG_FLAGS while the
guard stayed green. That is precisely the drift the guard exists to catch.

The Proxy now also traps `has`, so `'x' in uiState` is recorded like a read.
Enumeration is rejected rather than approximated: `ownKeys` and
`getOwnPropertyDescriptor` throw, because enumeration hands the predicate every
flag at once and makes a recorded read-set meaningless. Symbol keys throw for
the same reason instead of being silently dropped. Each error names the fix:
update the guard and the flag table together.

Verified by injecting each pattern into hasActiveDialog and reverting:

  'isNarrow' in uiState                                    -> fails, read-set
                                                              diff shows
                                                              + "isNarrow"
  Object.keys(uiState)                                     -> fails, enumerated
                                                              via ownKeys
  Object.prototype.hasOwnProperty.call(uiState,'isNarrow') -> fails, enumerated
                                                              via
                                                              getOwnPropertyDescriptor

The first of those passed silently before this change.

Also rejected, with reasoning recorded on the PR: the claim that `useState`
inside the `useCommandCompletion` mock in InputPrompt.escape.test.tsx breaks the
Rules of Hooks. The mock stands in for a custom hook and is called
unconditionally once per render from InputPrompt, which is exactly where a
custom hook may call useState. The state is also required: an earlier revision
used a getter over a closure variable, which flipped without re-rendering, so
the suggestion stayed on screen and the assertion on the rendered frame failed.

Verification: format, lint, typecheck, build and the full suite all pass with
zero failures.

Refs #2018
@acoliver
acoliver merged commit 25a5fad into dev/0.12.0 Aug 30, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintainer:e2e:ok Trusted contributor; maintainer-approved E2E run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Increase Ink composer and input-state coverage

1 participant