Skip to content

fix(extension-testkit): name-bound the dispose steps and stop rpc-observer timers outliving their observer (#361) - #418

Merged
omridevk merged 4 commits into
mainfrom
fix/361-dispose-bounds
Aug 11, 2026
Merged

fix(extension-testkit): name-bound the dispose steps and stop rpc-observer timers outliving their observer (#361)#418
omridevk merged 4 commits into
mainfrom
fix/361-dispose-bounds

Conversation

@omridevk

@omridevk omridevk commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • settleTeardown steps are now named ({name, run, timeoutMs?}) and each wrapped in p-timeout (20s default), so a wedged closeBrowser/close/stop names itself in the rejection instead of surfacing as vitest's generic 30s hookTimeout failure, and the sibling steps still settle inside the hook budget (Promise.allSettled already made them failure-independent since fix(test-teardown): harden playwright suite teardown against CI leaks #412; this bounds and names each one).
  • rpcObserver.dispose() cleared its waiters Set but never cancelled the pending setTimeout each buffered() call schedules. Under @playwright/test's single-worker-per-file execution (post test(embed): move integration tests onto @playwright/test under tests/e2e — the runner owns page lifecycle (#389) #415), that timer could outlive its page/observer and fire during a later, unrelated test, misattributing a timeout to the wrong test — the root cause behind the original Flaky: recorder useRecorderTestApi afterAll dispose exceeds 30s hookTimeout, fails the file while all tests pass #361 symptom surfacing again from the recorder migration. dispose() now fails every pending waiter (which internally clearTimeouts) before clearing the set.
  • openObservedPage (used by both launch()'s primary page and secondClient()) now wires observer.dispose() to the page's own 'close' event, so observers created for testkit pages are always disposed by their natural owner instead of only being disposed manually by ad-hoc call sites like connect-handshake.ts.
  • Added p-timeout (already used elsewhere in the workspace, e.g. @conciv/browser-fixture) as a direct dependency of @conciv/extension-testkit.

Test plan

  • packages/extension-testkit/test/settle-teardown.test.ts — unit test proving a wedged, named step rejects with testkit <name> exceeded <ms>ms while sibling steps still complete.
  • packages/extension-testkit/test/rpc-observer-dispose.it.test.ts — real-browser test proving dispose() fails a pending waiter immediately (well under its own natural timeout) instead of leaving the timer to fire later, and that closing a launch()-opened page disposes its observer and cancels pending waits.
  • All 4 new tests captured failing against unfixed source first (verbatim output in agent report), then passing after the fix.
  • @conciv/extension-testkit typecheck (tsc -p tsconfig.json --noEmit via turbo) — green.
  • @conciv/extension-testkit full test suite (TURBO_CONCURRENCY=1 VITEST_MAX_FORKS=1 turbo run test --concurrency=1) — 9 files / 31 tests green.
  • @conciv/extension-recorder typecheck + lint (its test suite is CI-only, not run locally) — green.
  • pnpm lint && pnpm format:check — green (one pre-existing, unrelated oxfmt finding in docs/assets/page-session-card-spike/fake-form.html from main, not touched by this PR).
  • pnpm exec fallow audit --changed-since origin/main --format json --quiet — verdict pass, 0 introduced findings (9 pre-existing/inherited unused-dependency findings, unrelated to this diff).

Refs #361, epic #409.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved cleanup reliability when closing pages and test environments.
    • Pending RPC waits now settle promptly when their associated page is closed or disposed.
    • Prevented unhandled errors from abandoned waits.
    • Teardown operations continue independently, report failing step names, and time out stalled operations after 20 seconds.
    • Cleanup failures now provide clearer details about the affected operation and timeout.
  • Tests

    • Added coverage for observer disposal, abandoned waits, and teardown timeout behavior.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@omridevk, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 821bb53d-b1d1-471a-af98-dc906e2569c6

📥 Commits

Reviewing files that changed from the base of the PR and between c81ee05 and 694a32e.

📒 Files selected for processing (1)
  • packages/embed/tests/e2e/rpc-observer.it.test.ts
📝 Walkthrough

Walkthrough

The extension testkit now supports named, timed teardown steps. RPC observers retain waiter promises, project settlement results, suppress abandoned-wait rejections, and dispose automatically when their page closes.

Changes

Extension testkit lifecycle handling

Layer / File(s) Summary
Named teardown steps and timeouts
packages/extension-testkit/src/settle-teardown.ts, packages/extension-testkit/src/get-extension-test-api.ts, packages/extension-testkit/package.json, packages/extension-testkit/test/settle-teardown.test.ts
Teardown steps now include names and optional timeouts. settleTeardown applies a 20-second default, aggregates outcomes, and reports timed-out step names.
Page-scoped observer disposal
packages/extension-testkit/src/rpc-observer.ts, packages/extension-testkit/src/launch.ts, packages/extension-testkit/test/rpc-observer-dispose.it.test.ts
RPC observers now retain typed waiters, project settlement results, reject pending waits on disposal, suppress abandoned waiter rejections, and dispose when the page closes.

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

Sequence Diagram(s)

sequenceDiagram
  participant Page
  participant RPCObserver
  participant PendingWait
  Page->>RPCObserver: close event
  RPCObserver->>PendingWait: reject with disposal error
  RPCObserver->>RPCObserver: attach rejection handlers
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The linked issue requires a Storybook 10.5 upgrade and CI mitigation removal, but this PR changes only extension-testkit teardown behavior. Implement the Storybook package upgrades, remove the specified Vitest and CI mitigations, and validate the affected builds and tests.
Out of Scope Changes check ⚠️ Warning The PR changes extension-testkit teardown and RPC observer behavior, which is unrelated to the linked Storybook upgrade issue. Move the extension-testkit changes to a separate PR or link them to an issue covering teardown and RPC observer disposal.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main changes: named dispose steps and RPC observer timer cleanup.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/361-dispose-bounds

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Hardens extension-testkit teardown by bounding named cleanup steps and disposing RPC observers with their pages.

Changes:

  • Adds per-step teardown timeouts and diagnostics.
  • Cancels pending RPC observer waits on disposal/page closure.
  • Adds regression coverage and the p-timeout dependency.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pnpm-lock.yaml Locks p-timeout.
packages/extension-testkit/package.json Adds the teardown dependency.
packages/extension-testkit/src/settle-teardown.ts Bounds and names cleanup steps.
packages/extension-testkit/src/rpc-observer.ts Rejects pending waits during disposal.
packages/extension-testkit/src/launch.ts Disposes observers when pages close.
packages/extension-testkit/src/get-extension-test-api.ts Names teardown operations.
packages/extension-testkit/test/settle-teardown.test.ts Tests bounded, independent teardown.
packages/extension-testkit/test/rpc-observer-dispose.it.test.ts Tests observer disposal behavior.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/extension-testkit/test/rpc-observer-dispose.it.test.ts Outdated
Comment thread packages/extension-testkit/test/rpc-observer-dispose.it.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/extension-testkit/src/settle-teardown.ts`:
- Around line 15-18: Update the teardown step mapping around step.run() to
invoke it inside a promise callback before passing it to pTimeout, converting
synchronous throws into rejected promises so steps.map() continues and later
teardown steps settle. Add coverage for a synchronously throwing run function
and verify subsequent steps still complete.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0be7ba79-60da-4db6-b126-38a22e6a0e43

📥 Commits

Reviewing files that changed from the base of the PR and between 92ae8c9 and 240527b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • packages/extension-testkit/package.json
  • packages/extension-testkit/src/get-extension-test-api.ts
  • packages/extension-testkit/src/launch.ts
  • packages/extension-testkit/src/rpc-observer.ts
  • packages/extension-testkit/src/settle-teardown.ts
  • packages/extension-testkit/test/rpc-observer-dispose.it.test.ts
  • packages/extension-testkit/test/settle-teardown.test.ts

Comment thread packages/extension-testkit/src/settle-teardown.ts Outdated
…erver timers outliving their observer (#361)

settleTeardown now names each step and wraps it in p-timeout (20s default), so a
wedged closeBrowser/close/stop names itself in the error instead of blowing
vitest's generic 30s hookTimeout, and the sibling steps still settle inside the
hook budget.

rpc-observer's dispose() cleared its waiters Set but never cancelled the
setTimeout each buffered() call schedules, so a pending wait outlived its own
observer and page; under @playwright/test's single-worker-per-file run, that
timer could fire during a later, unrelated test and misattribute a timeout.
dispose() now fails every pending waiter (which clears its timer) before
clearing the set, and openObservedPage wires dispose to the page's own 'close'
event so observers never outlive the page that owns them.

Refs #361, epic #409.
…c-observer waits

CI caught a regression from the prior dispose fix: embed's
composer-rich-input.it.test.ts failed with "Error: rpc observer disposed
while awaiting page.queries" leaking as an unhandled rejection from an
abandoned wait (a wait a test never awaited, or moved on from before the
page closed).

Root cause: completed()/firstEvent() were async wrapper functions, so the
promise a caller actually holds is a *different* object from the internal
buffered() promise — attaching a no-op .catch() to only the internal one
(the previous fix) didn't reach the outer promise callers can abandon.
Collapsed the wrapper: buffered()/settle() now take the result projection
directly and return the exact promise handed to callers, so
Waiter.promise IS that promise. dispose() attaches a no-op .catch() to it
before failing it, so an abandoned wait's disposal rejection is always
handled (no leak) while an awaited wait still receives the rejection
(multiple handlers each get it).

Refs #361, epic #409.
- settle-teardown.ts: wrap step.run() in Promise.resolve().then() before
  pTimeout so a step that throws synchronously (not just one that returns
  a rejected promise) still lets sibling steps start and complete, instead
  of throwing out of steps.map() before any step began.
- rpc-observer-dispose.it.test.ts: drop the wall-clock
  Date.now()-delta assertion; a descheduled CI worker can fail it for
  reasons unrelated to the code under test. rejects.toThrow(/disposed/)
  already proves disposal beat the natural timeout semantically.
- rpc-observer-dispose.it.test.ts: migrate the two raw chromium.launch()
  calls onto @conciv/browser-fixture's file-scoped, timeout-bounded
  browser fixture (already a direct dependency), each test opening and
  closing its own page. The third test (closing the launched page...)
  is unchanged: it exercises launch.ts's own browser lifecycle, not a
  bare chromium.launch() call.

Refs #361, epic #409.
… rpc-observer test

CI (run 31442555924, shard-1) failed rpc-observer.it.test.ts:186 with
"rpc observer disposed while awaiting page.queries" — not composer-rich-input
as first suspected. The test intentionally leaves a page.queries wait
pending (proving an extension-namespaced call never falsely matches the
core path) and disposes while it's still pending, which #361's dispose
contract correctly rejects. The test derived a second promise via
`.then()` with no `.catch()`; dispose's own `waiter.promise.catch(() => {})`
only guards the promise it holds, not sibling branches callers derive from
it, so the derived chain surfaced as an unhandled rejection. Attaching
`.catch()` to that derived chain is enough — no rpc-observer.ts change
needed, the dispose contract in rpc-observer-dispose.it.test.ts is correct
and unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omridevk
omridevk merged commit 4886a6d into main Aug 11, 2026
44 of 46 checks passed
@omridevk
omridevk deleted the fix/361-dispose-bounds branch August 11, 2026 01:08
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.

2 participants