Skip to content

test(desktop): unmount every TerminalSubstrate tree before the window closes - #9440

Merged
matt2e merged 1 commit into
mainfrom
mtoohey/flake-terminal-substrate-unmount
Nov 14, 2026
Merged

matt2e merged 1 commit into
mainfrom
mtoohey/flake-terminal-substrate-unmount

Conversation

@matt2e

@matt2e matt2e commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Summary

desktop/src/features/terminal/TerminalSubstrate.test.mjs can pass every test and then run the Desktop JS job out of memory several minutes later. This moves the React Testing Library unmount from beforeEach to afterEach, so no tree is still mounted when the jsdom window closes. Only the test file changes (6 insertions, 2 deletions).

Commit (cherry-picked unchanged from a local flake-fix series):

  • test(desktop): unmount every TerminalSubstrate tree before the window closes

CI sighting

One sighting was found in the CI runs from 7 to 12 November 2026 that were searched:

In that job the suite reported 6,950 of 6,951 passing. The log shows An update to TerminalSubstrate inside a test was not wrapped in act(...) at 21:54:33Z, then FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory at 22:02:17Z, and the file is reported as failed at file level (test at src/features/terminal/TerminalSubstrate.test.mjs:1:1) after 504.6 s.

Root cause

The file called cleanup() only in beforeEach, so the last test's tree was still mounted when after(() => dom.window.close()) ran. If ThemeProvider's asynchronous palette update landed after that close, it re-rendered the substrate, the splash effect's palette guard passed, and requestAnimationFrame on the closed window started a fresh jsdom interval that nothing cleared. That 60 Hz loop repainted the banner into the test's recording getContext stub indefinitely, and paintLog grew until the heap limit. No individual test hangs, so a per-test timeout would not catch it.

Fix

afterEach(() => cleanup?.()) unmounts every tree, including the last, before after() closes the window. The beforeEach call became redundant and is removed. No assertions change, and no timeouts, retries or sleeps are added.

Related issue

N/A (flake fix). No existing issue or PR for this failure was found. Three open PRs touch the same test file (#5057, #5485, #7777); none of them changes where cleanup runs.

Testing

Human testing is still required. This PR does not carry buzz-review-completed; the author has not yet run this test themselves on this branch.

Verification of the fix (before this branch was split out)

  • The test file, 5 consecutive runs: 25 of 25 tests passed each time in about 6.8 s, with no act warning.
  • The CI out-of-memory failure was not reproduced with the real test file. Its mechanism was reproduced in a scratch harness with the same stubs: without cleanup before the window close, 3 of 3 runs left the palette unresolved at close, added 11,556 paintLog entries per 100 ms, kept one live timer, and never exited. With cleanup before the close, 3 of 3 runs added no entries, left no timers, and exited on their own.

Local checks on this branch

The push ran the repository's full pre-push hook set from a checkout of this branch's head (7f4117340), with no lanes excluded and without --no-verify. The branch is one commit behind main; that commit changes only mobile and docs files, none of them touched here.

Ran and passed:

  • branch-skew, push-head-scope and file-size-check
  • desktop-check (pnpm check)
  • desktop-typecheck (pnpm typecheck)
  • desktop-test (pnpm test, the full desktop unit suite, which includes TerminalSubstrate.test.mjs)

Did not run:

  • rust-tests, desktop-tauri-checks and mobile-checks. The hook selects lanes by changed path, and this branch changes no Rust, Tauri or mobile file.
  • Desktop builds and the Playwright E2E suites, which are CI-only.
  • just ci.
  • The 5-run repetition and the scratch harness above were not repeated on this branch.

A green run does not prove the flake is gone: the failure needs the palette update to land after the window closes.

🤖 Generated with Claude Code

… closes

`TerminalSubstrate.test.mjs` only called RTL `cleanup()` in `beforeEach`,
so the last test's tree was still mounted when `after()` closed the jsdom
window. If ThemeProvider's asynchronous palette update landed after that
close, it re-rendered the substrate, the splash effect's palette guard
passed, and `requestAnimationFrame` on the closed window started a fresh
jsdom interval that nothing cleared. The 60 Hz loop repainted the banner
into the test's recording `getContext` stub forever, growing `paintLog`
until the heap limit — the file passed all 25 tests in CI and then died
~8 minutes later with "JavaScript heap out of memory".

Move the unmount into `afterEach` so every tree, including the last, is
torn down before `after(() => dom.window.close())` runs; the `beforeEach`
call is now redundant and is removed.

Verified: 5 consecutive runs of the file pass 25/25 in ~6.8 s with no act
warning. A scratch harness with the same stubs reproduced the mechanism
3/3 without cleanup (palette unresolved at close, +11,556 paintLog
entries per 100 ms, one live Timeout, process never exits) and 3/3 with
cleanup before close (+0 entries, 0 timers, process exits on its own).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: mtoohey <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
@matt2e
matt2e requested a review from a team as a code owner November 13, 2026 03:28
@matt2e
matt2e deployed to codex-review November 13, 2026 03:28 — with GitHub Actions Active
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 13, 2026
@github-actions

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated, security-focused review generated by Codex.
Use it as a supplement to human review; false positives are possible.

Scope

  • Exact PR diff: 3c0adc188206bab8551a7af53dcf6bd54c9d0d09...7f4117340f8c9e4dde06ab25422914c3ed382b86
  • Model: gpt-6.1-sol

💡 Click "edited" above to see earlier reviews for this PR.


Review Summary

Overall Risk: NONE

The PR moves terminal test cleanup to afterEach, ensuring mounted React trees are unmounted before the jsdom window closes. No concrete security, correctness, or reliability issues were found in the authorized diff.

Findings

No concrete security, correctness, or reliability findings were identified.

Notes

  • Review used read-only source inspection. Tests were not executed, as instructed.

Generated by Codex Security Review |
Requested by: @matt2e |
Workflow run

@kazemek21 kazemek21 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.

Kyle reviewed this PR and the Nexus review-tracker notes.

Posted by Codex (gpt-6.1-sol) on behalf of kylez

@jedwards27 jedwards27 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.

🤖 :bot: Jude’s code review agent — team recommendation: APPROVE (GitHub COMMENT event per repository agent-review policy).

Reviewed 3c0adc188206bab8551a7af53dcf6bd54c9d0d09..7f4117340f8c9e4dde06ab25422914c3ed382b86 at a clean matching HEAD. Risk: low — a single Desktop test-file hook change; no product/runtime contract changes. TerminalSubstrate.test.mjs:69–75 moves RTL unmount from the next test’s beforeEach into afterEach, which precedes the file-level after(window.close) even for the final test. The splash effect cancels its scheduled RAF on unmount (TerminalSubstrate.tsx:368–396). No blocking code finding; author action for this review: none.

Verification: At the matching clean head, the focused Node file passed 25/25 and exited normally without its prior act warning; cd desktop && pnpm test passed 6,950/6,950 and 185/185, exit 0; pnpm check and pnpm typecheck exited 0; git diff --check 3c0adc...HEAD and node --check passed. pnpm check emitted three warnings in unchanged files. Live exact-head CI reports successful Desktop JS, smoke, integration, Windows build and macOS build checks. The earlier full-suite attempt with a 120s reviewer cap was superseded by the complete run.

Adversarial evidence and residual risk: Reverting the hook move in a temporary mutation still let the 25 tests pass and exit in one 12.3s run. This demonstrates the old timing-dependent failure is not deterministically caught by the existing file; it does not demonstrate failure of the proposed hook. The PR reports a separate scratch-harness negative/positive control (3/3 each), not an in-tree regression assertion. An explicit deterministic lifecycle assertion would strengthen protection against a future revert, but is optional rather than a current blocker. A single green suite/CI run cannot prove a timing flake permanently absent. No native UI observation is necessary to establish this test-only boundary; the repository’s separate human-testing readiness step remains with the author/human operator before declaring the PR ready.

Verification owner: CI/reviewer for exact-head checks; human author/operator for the repository human-testing checklist. A new head requires delta review.

@matt2e
matt2e merged commit 94c154e into main Nov 14, 2026
73 checks passed
@matt2e
matt2e deleted the mtoohey/flake-terminal-substrate-unmount branch November 14, 2026 00:45
wpfleger96 added a commit that referenced this pull request Nov 14, 2026
* origin/main: (24 commits)
  docs(readme): point desktop readers at block/buzz-app and describe where each client lives (#9493)
  test(cli,db): freeze the assertion test clock and give pool release tests a test-lane acquire budget (#9442)
  test(desktop): unmount every TerminalSubstrate tree before the window closes (#9440)
  fix(mobile): drop the iOS share extension from the build until it's signed (#9489)
  chore(release): release Buzz Desktop version 0.5.30 (#9491)
  perf(mobile): parse each event's thread reference once (#9460)
  perf(mobile): load only the directory when search opens (#9466)
  perf(mobile): stop rebuilding Home's channel list when a route covers it (#9459)
  perf(mobile): ask typing subscriptions only for the typing window (#9454)
  perf(mobile): skip avatar URL parsing for non-HTTP URLs (#9448)
  perf(mobile): download the identity archive list only when it changed (#9445)
  perf(mobile): don't treat a Control Center pull as a resume (#9443)
  fix(mobile): keep a superseded roster read out of the offline snapshot (#9452)
  perf(mobile): read channel bot roles from the member list (#9451)
  perf(mobile): read Activity once per reconnect (#9455)
  fix(mobile): offer role controls for agents (#9475)
  perf(mobile): stop rebuilding the native nav bar on every scrolled frame (#9458)
  perf(mobile): keep verified-name batches open as people are added (#9450)
  perf(mobile): read only changed custom emoji sets on reconnect (#9446)
  perf(mobile): keep latest-message times on a directory load (#9457)
  ...

Signed-off-by: Will Pfleger <[email protected]>

# Conflicts:
#	crates/buzz-cli/src/desktop_provider.rs
bostonaholic added a commit that referenced this pull request Nov 14, 2026
…eaction-recipients

* origin/main: (111 commits)
  perf(acp): avoid repeated observer batch serialization (#9483)
  docs(readme): point desktop readers at block/buzz-app and describe where each client lives (#9493)
  test(cli,db): freeze the assertion test clock and give pool release tests a test-lane acquire budget (#9442)
  test(desktop): unmount every TerminalSubstrate tree before the window closes (#9440)
  fix(mobile): drop the iOS share extension from the build until it's signed (#9489)
  chore(release): release Buzz Desktop version 0.5.30 (#9491)
  perf(mobile): parse each event's thread reference once (#9460)
  perf(mobile): load only the directory when search opens (#9466)
  perf(mobile): stop rebuilding Home's channel list when a route covers it (#9459)
  perf(mobile): ask typing subscriptions only for the typing window (#9454)
  perf(mobile): skip avatar URL parsing for non-HTTP URLs (#9448)
  perf(mobile): download the identity archive list only when it changed (#9445)
  perf(mobile): don't treat a Control Center pull as a resume (#9443)
  fix(mobile): keep a superseded roster read out of the offline snapshot (#9452)
  perf(mobile): read channel bot roles from the member list (#9451)
  perf(mobile): read Activity once per reconnect (#9455)
  fix(mobile): offer role controls for agents (#9475)
  perf(mobile): stop rebuilding the native nav bar on every scrolled frame (#9458)
  perf(mobile): keep verified-name batches open as people are added (#9450)
  perf(mobile): read only changed custom emoji sets on reconnect (#9446)
  ...

Signed-off-by: Matthew Boston <[email protected]>

This branch was successfully deployed

1 active deployment
codex-review — 7f411734 Deployed Nov 13, 2026 by matt2e via Run Codex Security Review #14811
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex-security-review-current The posted Codex security review matches its recorded range.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants