Skip to content

fix(test): let the jest scripts exit instead of stalling on open handles - #676

Open
RonenMars wants to merge 1 commit into
mainfrom
fix/jest-query-client-exit
Open

fix(test): let the jest scripts exit instead of stalling on open handles#676
RonenMars wants to merge 1 commit into
mainfrom
fix/jest-query-client-exit

Conversation

@RonenMars

Copy link
Copy Markdown
Owner

npm run test:unit passes all 105 suites and then never exits.
It looks like a hang, and it is easy to kill and record as "the suite hangs" when the run has in fact already succeeded.

Cause

A test that constructs its own QueryClient never clears it, so React Query schedules setTimeout(gcTime) for each cached query once it goes unused, and node will not exit while that timer is pending.

Measured on 2026-08-12:

run tests take process exits at
test:unit (105 suites) ~20s never — killed at 120s, still alive
useConversations.test.tsx -t "partial failure" 1.4s 305s — the 5-minute default gcTime, to the second
useConversations.test.tsx -t "retention gcTime" 0.9s still alive at 400s — useConversation sets a 7-day gcTime (hooks/useConversations.ts:423)
same file, -t "zzz-no-such-test" 0 tests run 6s, clean

The last row rules out module import as the cause: it is a test body that leaves the timer behind.
--detectOpenHandles reports nothing, which is what makes this hard to place from the symptom alone.

Fix

test:ci already passed --forceExit for exactly this reason.
test:unit, test:integration, test:e2e and test:i18n did not — so the four scripts a developer actually reaches for locally were the four that hung, while the one CI runs was fine.

Giving them the same flag is the whole change.
The same 105 suites, 996 tests, now exit in 20s.

Why not clear the client instead

That was the first plan, and measurement killed it.
The stall does not correlate with test-utils/createWrapper, it correlates with suites that build their own client:

suite createWrapper calls own QueryClient exits on its own?
conversation-search-anchor 12 0 yes, 19s
useSessionActions.resume 0 1 no
useConversations several 1 (7-day gcTime) no

Clearing the wrapper's client in a global afterEach would therefore have fixed nothing observable.
Fixing the leak properly means changing the 16 suites that construct their own client, which is a much wider diff than the problem justifies today.
docs/troubleshooting.md now records that finding so the next person does not re-run the same experiment, and names the 16 suites as the target if anyone wants the leak gone rather than worked around.

Trade-off

--forceExit masks a genuinely stuck handle rather than surfacing it.
That trade was already accepted for test:ci; this makes the four sibling scripts consistent with it rather than introducing it.
Everything printed above the stall is a real result, so no test signal is lost.

Verification

  • Before, on this branch's base: npx jest --ci --runInBand --testPathPattern='__tests__/unit' — 105 suites pass, Jest did not exit one second after the test run has completed., killed at 120s.
  • After: npm run test:unit — 105 suites, 996 tests, all pass, exit=0 at 20s.
  • No JS/TS files are touched, so the lint-before-commit rule does not apply.

npm run test:unit passes all 105 suites and then never exits, because a test that builds its own QueryClient leaves React Query's setTimeout(gcTime) pending and node will not exit while that timer is scheduled.
Measured: the unit suite finishes in about 20s and was still alive when killed at 120s.
useConversations.test.tsx -t "partial failure" finishes its tests in 1.4s and exits on its own at exactly 305s, the 5-minute default gcTime, while the same file's retention tests were still alive at 400s against a 7-day gcTime.

test:ci already passed --forceExit for this reason, but test:unit, test:integration, test:e2e and test:i18n did not, so the four scripts a developer reaches for locally were the four that hung.
Give them the same flag; the same 105 suites now exit in 20s.

Record the identified handle in docs/troubleshooting.md, including the evidence that the correlation is with suites constructing their own client rather than with test-utils/createWrapper.
conversation-search-anchor calls createWrapper twelve times and exits in 19s, while useSessionActions.resume builds one client and never exits.
That rules out clearing the wrapper's client as a fix and points at the 16 suites that build their own, for whoever wants the leak gone rather than worked around.
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.

1 participant