Skip to content

fix(core): stop app on unhandled q and ctrl+c#238

Merged
RtlZeroMemory merged 6 commits intomainfrom
fix/233-default-q-quit
Mar 4, 2026
Merged

fix(core): stop app on unhandled q and ctrl+c#238
RtlZeroMemory merged 6 commits intomainfrom
fix/233-default-q-quit

Conversation

@RtlZeroMemory
Copy link
Owner

@RtlZeroMemory RtlZeroMemory commented Mar 4, 2026

Summary

  • stop widget-mode apps on unhandled top-level quit inputs (q/Q text, raw Ctrl+C text, and unhandled Ctrl+C key events)
  • keep existing behavior where explicit keybindings win and top-level error-screen Q handling remains unchanged
  • add regression tests for default quit behavior and keybinding override behavior
  • update changelog unreleased bug fixes

Validation

  • npm -w @rezi-ui/core run build
  • node --test --test-concurrency=1 packages/core/dist/app/__tests__/resilience.test.js
  • node scripts/run-tests.mjs --scope packages --filter "packages/core/dist/app/__tests__/(resilience|shortcutEnforcement|keybindings.api)\.test\.js$"

Fixes #233

Summary by CodeRabbit

  • Bug Fixes

    • App now gracefully stops on unhandled q/Q or Ctrl+C inputs while preserving custom keybindings.
    • Input routing distinguishes handled-but-no-render events to prevent unintended quits or side effects.
  • Tests

    • New resilience tests for quitting via text, Ctrl+C, and overridden quit bindings.
    • Fuzz/event tests adjusted to avoid accidental global-quit during randomized input.
  • Documentation

    • Changelog unreleased notes updated.

@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5290bd78-4390-4fdf-bd31-a535dad535ac

📥 Commits

Reviewing files that changed from the base of the PR and between bd896da and 3113b0a.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • packages/core/src/app/__tests__/resilience.test.ts
  • packages/core/src/app/createApp.ts

📝 Walkthrough

Walkthrough

Unhandled top-level q/Q and Ctrl+C inputs now stop the app by default when not consumed by explicit handlers; createApp added quit-detection helpers and a shutdown path. Tests and fuzzing were updated to avoid accidental quits during generated inputs.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Added Unreleased entry: unhandled top-level q/Q and Ctrl+C now stop the app by default; explicit keybinding handlers still take precedence.
Quit Detection & Runtime
packages/core/src/app/createApp.ts
Added key constants and quit-detection helpers (KEY_LOWER_Q, KEY_LOWER_R, CTRL_C_CODEPOINT, isUnmodifiedTextQuitEvent, isUnhandledCtrlCKeyEvent, stopFromUnhandledQuitEvent); integrated unhandled-quit shutdown path into routing loop.
Widget Routing / Input Editing
packages/core/src/app/widgetRenderer.ts, packages/core/src/app/widgetRenderer/inputEditing.ts
Added optional consumed?: boolean to routing outcome types and new ROUTE_NO_RENDER_CONSUMED constant; updated code paths to return consumed outcomes when events are handled but no render is needed.
Resilience Tests
packages/core/src/app/__tests__/resilience.test.ts
Added tests verifying default quit via text q and Ctrl+C, behavior when stop throws, and override via custom q keybinding; imports ZR_MOD_CTRL/ZR_MOD_SHIFT. (Patch contains duplicated test blocks.)
Fuzzing / Event Generation Tests
packages/core/src/__tests__/stress/fuzz.random-events.test.ts
Avoids emitting q/Q text and default-quit key sequences in generated events (isDefaultQuitKeyEvent); adjusts key/text event generation to prevent accidental quits. (Duplicate helper declared twice in diff.)

Sequence Diagram

sequenceDiagram
    participant User
    participant EventLoop as Event Loop
    participant Router as Widget Router
    participant QuitDetector as Quit Detector
    participant App as App Instance

    User->>EventLoop: Input (text 'q'/'Q' or key Ctrl+C)
    EventLoop->>Router: Route event to widgets/keybindings
    Router->>QuitDetector: Was event handled / needsRender?
    alt Event consumed by widget/keybinding
        QuitDetector-->>Router: consumed (no quit)
        Router->>App: Execute routed handler
    else Unhandled quit condition detected
        QuitDetector-->>App: stopFromUnhandledQuitEvent()
        App->>App: stop()/dispose() (graceful shutdown)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰
I hopped through input streams with cheer,
A tiny "q" now says: "bench gear",
Ctrl+C whispers, soft and fleet,
Unless a handler holds my feet,
I bow, I stop, and nibble a treat.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing default quit behavior for unhandled q and Ctrl+C inputs at the app level.
Linked Issues check ✅ Passed The PR successfully addresses all requirements from issue #233: implements default quit on unhandled top-level q/Ctrl+C, preserves explicit keybinding overrides, and adds regression tests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing default quit behavior: handler additions, routing flag updates, fuzz test guards, and changelog/test documentation are all within scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/233-default-q-quit

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/app/createApp.ts`:
- Around line 442-458: The isUnmodifiedTextQuitEvent function's multi-line
return should be consolidated into a single expression to satisfy the formatter:
replace the current multi-line return with one-line boolean expression that
checks ev.codepoint against KEY_Q, KEY_LOWER_Q and CTRL_C_CODEPOINT (e.g. return
ev.codepoint === KEY_Q || ev.codepoint === KEY_LOWER_Q || ev.codepoint ===
CTRL_C_CODEPOINT;), leaving the existing early kind check intact.
- Around line 1343-1351: Remove the unnecessary "continue;" at the end of the
for-loop branch inside createApp (the block that checks routed.action,
routed.needsRender, isUnmodifiedTextQuitEvent(ev) ||
isUnhandledCtrlCKeyEvent(ev)); specifically delete the trailing continue after
noteBreadcrumbConsumptionPath("widgetRouting") and stopFromUnhandledQuitEvent()
since control will naturally proceed to the next iteration—no other control-flow
changes are required.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 205cacad-a7d6-46a2-bb7c-f206df6ab708

📥 Commits

Reviewing files that changed from the base of the PR and between be73751 and d821e5e.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • packages/core/src/app/__tests__/resilience.test.ts
  • packages/core/src/app/createApp.ts

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d821e5eb01

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/app/createApp.ts`:
- Around line 963-969: The stopFromUnhandledQuitEvent function currently calls
void app.stop() which drops any promise rejection; change it to capture the
Promise from app.stop() (e.g., const p = app.stop()), wrap that call in
try/catch to handle synchronous throws, and attach .catch(...) to handle async
rejections and .finally(...) if needed for post-stop cleanup—use the same
pattern as quitFromTopLevelViewError to ensure any rejection from app.stop() is
observed and handled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 15a5fc9d-fe3b-4620-b9fc-640387aabb80

📥 Commits

Reviewing files that changed from the base of the PR and between d821e5e and 7a99017.

📒 Files selected for processing (1)
  • packages/core/src/app/createApp.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/core/src/app/__tests__/resilience.test.ts (1)

182-207: Split Ctrl+C key and text assertions into separate tests.

This test currently depends on stop/start timing in the middle of the same case. Using separate app instances (or two tests) will reduce flake risk and make failures more local.

Proposed refactor
-test("unhandled ctrl+c key/text events stop app by default", async () => {
+test("unhandled ctrl+c key event stops app by default", async () => {
   const backend = new StubBackend();
   const app = createApp({ backend, initialState: {} });

   app.view(() => ui.text("hello"));

   await app.start();
   await emitResize(backend, 1);
   await settleNextFrame(backend);

   await pushEvents(backend, [
     { kind: "key", timeMs: 3, key: 67, mods: ZR_MOD_CTRL, action: "down" },
   ]);
   await flushMicrotasks(30);

   assert.equal(backend.stopCalls >= 1, true);
+});
 
-  await app.start();
+test("unhandled ctrl+c text event stops app by default", async () => {
+  const backend = new StubBackend();
+  const app = createApp({ backend, initialState: {} });
+
+  app.view(() => ui.text("hello"));
+
+  await app.start();
   await emitResize(backend, 5);
   await settleNextFrame(backend);

   await pushEvents(backend, [{ kind: "text", timeMs: 6, codepoint: 3 }]);
   await flushMicrotasks(30);

-  assert.equal(backend.stopCalls >= 2, true);
+  assert.equal(backend.stopCalls >= 1, true);
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/app/__tests__/resilience.test.ts` around lines 182 - 207,
The test "unhandled ctrl+c key/text events stop app by default" mixes two
separate behaviors and restarts the same app mid-test, causing timing flakiness;
split into two separate tests (e.g., "unhandled ctrl+c key event stops app" and
"unhandled ctrl+c text event stops app") each creating its own StubBackend and
app via createApp, registering the view, starting the app, emitting
resize/settle, pushing the appropriate single event with pushEvents (the key
event with key=67 and mods=ZR_MOD_CTRL for the key test, and the text event with
codepoint=3 for the text test), flushing microtasks, and asserting
backend.stopCalls >= 1; remove the second app.start() reuse and ensure each test
fully isolates lifecycle and assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/app/createApp.ts`:
- Around line 963-970: The stopFromUnhandledQuitEvent function is currently
swallowing synchronous errors from app.stop(); change the catch block to capture
the error (e.g., catch (err)) and do not return silently: log the error (using
the existing logger or processLogger) and rethrow or assign stopPromise =
Promise.reject(err) so the failure surfaces as a fatal condition instead of
dropping the quit intent; ensure stopPromise is always set (either to the
returned promise or a rejected promise) and remove the early return so upstream
handlers can observe the failure.

---

Nitpick comments:
In `@packages/core/src/app/__tests__/resilience.test.ts`:
- Around line 182-207: The test "unhandled ctrl+c key/text events stop app by
default" mixes two separate behaviors and restarts the same app mid-test,
causing timing flakiness; split into two separate tests (e.g., "unhandled ctrl+c
key event stops app" and "unhandled ctrl+c text event stops app") each creating
its own StubBackend and app via createApp, registering the view, starting the
app, emitting resize/settle, pushing the appropriate single event with
pushEvents (the key event with key=67 and mods=ZR_MOD_CTRL for the key test, and
the text event with codepoint=3 for the text test), flushing microtasks, and
asserting backend.stopCalls >= 1; remove the second app.start() reuse and ensure
each test fully isolates lifecycle and assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7bb3d66d-9e37-49b4-a7e5-afa4ad746a73

📥 Commits

Reviewing files that changed from the base of the PR and between 3c632c2 and bd896da.

📒 Files selected for processing (4)
  • packages/core/src/app/__tests__/resilience.test.ts
  • packages/core/src/app/createApp.ts
  • packages/core/src/app/widgetRenderer.ts
  • packages/core/src/app/widgetRenderer/inputEditing.ts

@RtlZeroMemory RtlZeroMemory merged commit b53bbcb into main Mar 4, 2026
20 of 22 checks passed
@RtlZeroMemory RtlZeroMemory mentioned this pull request Mar 4, 2026
@RtlZeroMemory RtlZeroMemory deleted the fix/233-default-q-quit branch March 6, 2026 06:46
Sign up for free to 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.

Just can't quit

1 participant