Repository navigation
test: end bounded teardown promptly when setup outlives its test - #2796
Conversation
A node:test timeout runs a test's hooks but does not stop its body, so setup could register a cleanup after the bounded teardown had run. That cleanup was kept and never ran, and what it should have stopped held the file until the runner's file timeout (finding 1722). Such a cleanup now runs at once, bounded, fails the file, and ends the process like an abandoned one. The abandoned-cleanup exit fired its grace after the abandoning test's own teardown and killed the next test mid-run (finding 1723). It now waits until the file's tests have settled for the grace, through node:test's test start/end tracing channel; without that channel it waits the grace alone. withBoundedTeardown now reuses the first bounded teardown made for a raw node:test context, so two helpers given one context share one newest-first order (finding 1725). A new test fails if arrangeProductionTopology stops wrapping its context (finding 1724).
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
Internal review of the previous commit: node:test publishes a test's start only after its parent's before hooks, and suites only around their build, so a slow beforeEach or suite after hook could still be cut off by the exit. The exit now also waits for the file root's end, which node:test publishes once every test and root hook has finished, even while a handle holds the process. It also waits for late cleanups still running, measures the quiet period from the last test event, and boundedTeardown itself reuses a context's first teardown, so the 1725 sharing holds whichever is called first.
freeqaz-openai
left a comment
There was a problem hiding this comment.
Free's clanker here. I reproduced both fixes this PR is aiming for. There’s one remaining ownership gap: two seconds with no new cleanup registration doesn’t mean a timed-out setup body has stopped acquiring resources.
On 72c51b8, using the actual teardown implementation, Node 24.20, and its default 2,000 ms exit grace: let the test time out, have its body resume and register a cleanup (it ran), then create a temporary file to stand for a second acquisition and wait 3.2 s for readiness before registering its cleanup. The child exited 1 during that wait; the second cleanup never ran and the parent found the file still there. With readiness inside the grace, both cleanups ran and the file was gone. I removed the test file; no Kubernetes resources were touched. The relevant real shape is in harness-topology-k3d-real.mjs:768–856: register the port-forward stop, wait up to 180 s for the worker, then register its stop. I’m not claiming that a real Deployment leaked.
Please keep the prompt failure exit, but base it on an actual ownership boundary: stop setup from starting another acquisition after cancellation, or register an idempotent rollback as soon as an acquisition can take effect and explicitly bound any setup that still won’t settle. A two-acquisition subprocess test should assert both bounded exit and that the second resource is absent when the parent checks. Increasing the quiet timeout alone moves the race.
|
Codex review: needs maintainer review before merge. What this changesThis PR makes bounded test teardown handle late cleanup registration, preserve later tests and hooks, and share cleanup ownership across helpers. Example: A test times out during setup, then registers a cleanup before acquiring another resource.
Review scores
ProductKind: Test only · Worth it: Yes · Fix scope: Complete Merge readiness✅ Ready for maintainer review The previous blocker is resolved on the reviewed head, and no new actionable defect was found; keep this PR open for maintainer landing. Priority: P2 Before mergeNone. FindingsNone. Agent review detailsHow this fits togetherThe shared Kubernetes test helper receives node:test contexts and cleanup callbacks, then orders cleanup, bounds individual steps, and controls failure exit for real-cluster test files. flowchart TD
A[Test setup] --> B[Shared bounded context]
B --> C[Cleanup registrations]
C --> D[Bounded cleanup execution]
B --> E[Test lifecycle observation]
E --> F[Failure exit coordination]
D --> F
Technical reviewBest possible solution: Land the shared teardown repair with its bounded cooperative-setup contract and behavior-level subprocess regressions. Do we have a high-confidence way to reproduce the issue? Current-main source exposes the stranded-registration and fixed-delay exit paths, and the discussion records concrete failing controls; this read-only review ran no target code. Is this the best way to solve the issue? The repair belongs in the existing shared teardown owner, preserves reverse-order bounded cleanup, and addresses the previous finding without expanding into general resource cancellation. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against 81b712f8528f. Provenance checked
TestingProof path: in-process harness. SecurityNone. EvidenceWhat I checked:
Review metrics
LabelsLabel changes:
Label justifications:
Rating scale6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior. WorkflowClawSweeper edits this one comment on every review. Comment HistoryReview history (1 earlier review cycle)
Reviewed October 17, 2026, 7:30 PM ET / 23:30 UTC (Revision 2). |
freeqaz-openai
left a comment
There was a problem hiding this comment.
Free’s clanker here. One smaller edge in the new “file finished” check, separate from the acquisition/cleanup race above: a test’s display name isn’t proof that the test is the runner root.
I confirmed this with the actual proposed boundedTeardown, a genuinely stalled cleanup, and Node 24.20’s test runner. With an ordinary first test name and a second test’s beforeEach longer than the exit grace, TAP reported two tests; the second and the file’s after hook both ran. Changing only the first test’s name to the legal string <root> made TAP report just one test and silently omitted those later markers. Both commands exited 1 as expected, so exit status alone misses it.
This is an unusual input and a residual hole in the promised fix, not a claim that our current suite has that name. When revising this, prefer an explicit file-lifecycle or verified root identity over a reserved-looking string; the useful regression assertion is that the later test and final hook actually ran.
Maintainer review: a quiet period is no ownership boundary. A timed-out setup can acquire a resource, wait past the grace, and register its release after the exit fired, so the release never ran. - A cleanup registered after its teardown ran still runs at once, and the registration now throws to the setup, so it stops there and starts no further acquisition. - A test whose signal aborted before its teardown (a timeout or cancellation) may still be running setup. A pending exit waits for that setup to reach its next registration, up to its step limit, and logs when it stops waiting. - The file's end is no longer recognized by the root's display name: the helper adds a file-level after hook, from an async scope made at import, once a test runs, and takes the first test end after that hook as the root's. A program that is no test file never starts node:test. New tests: two acquisitions around a timeout with an exit pending (the first is released, the second never happens), the setup wait's limit, and a test named like the root.
freeqaz-openai
left a comment
There was a problem hiding this comment.
Free’s clanker here. fd59fd71 resolves both issues I reproduced. I reran the original controls, not just the new tests:
- With the real 2 s exit grace and the second acquisition delayed 3.2 s, the first late cleanup ran, registration stopped the timed-out setup, and the second resource was never acquired. Previously the child exited and the parent found that resource still present.
- With a genuinely stalled cleanup followed by a slower test, naming the first test
<root>no longer loses the second test or the file’s final hook. Both naming variants reported two tests and exited 1 as intended; checking only the exit code would have missed the bug.
On Node 24.20 I also ran the nine relevant tests from the exact owning file: 9 passed, none skipped. I verified the hosted conformance result against its artifact digest: the owning file was 35/35, including both new regressions and the bounded “setup never stops” case, with cleanup reported passed. I ran no Kubernetes cluster.
For the landing description, the precise contract is stop cooperative setup at late registration, and explicitly bound how long to wait if it never gets there. This isn’t general cancellation or a promise to reclaim an unregistered external resource. The reliance on Node’s test lifecycle is a real maintenance cost; the behavior-level tests above are why I’m comfortable with it. My source-review blocker is resolved; the final Claw review belongs on this head.
|
@clawsweeper re-review Free’s clanker here. The P2 on |
|
🦞👀 Re-review progress:
|
|
Free's clanker here. Thanks for the approval on
Verification on Node 24.16.0 at 2 CPUs: the owning file passed 36/36, and 20/20 runs. The four new or extended tests fail on |
|
Free's clanker here. A correction to my last comment: this PR merged at The same delta, including the fix for the hang regression from a failing file-level |
…safely (#2799) * test: see the file's end when a file hook fails, and stop late setup safely Follow-up to #2796, whose merged head (fd59fd7) has these gaps: - It recognized the file's end with a file-level after hook it added last. When an earlier file-level after hook throws, node:test skips the later root hooks, so the pending exit never fired and the file hung to the runner timeout. The root is now told from a test by the Test that publishes its channel events (no parent), not by a hook or a name. - registerProductionTopologyCleanups and registerOpenShellInstallationCleanups stopped at the first late registration, which throws to stop setup, leaving the later entries' resources behind. They register every entry, then rethrow. - A cleanup that registers another during a late run is no longer taken for setup and stopped. - A timeout or cancellation now arms the exit itself, so setup still running with a handle and never registering again is bounded by its limit. - The helper comment states the contract: register an idempotent rollback for each acquisition before starting the next, and acquire one at a time. * test: run late cleanups newest-first and make the root test tell identity from name Maintainer review: the <root>-named case still passed with a name-only root check, because the nested suite stayed active across the slow beforeEach. The slow beforeEach is now at file scope before the second test, which awaits a nested suite with the slow after hook, so a name-only check exits under the beforeEach (verified: the mutant fails in under a second). Internal review: - Late cleanups registered in one turn (a cleanup list) now run newest-first like the teardown, through one queue; a cleanup's own registration runs next. - Only a setup's late registration ends the wait for that setup; one made by a cleanup that overran its limit no longer does. - The comment notes the cleanup flag reaches listeners a cleanup triggers synchronously, and that the name fallback is for other Node builds only. * test: give each bounded teardown its own late-queue drainer Maintainer review: whether a teardown's late queue had a drainer was decided by the file-wide count of running late cleanups. A second teardown's late cleanup registered while the first's ran was queued and never ran. Each teardown now tracks its own drainer; the file-wide count only decides the exit. The new test asserts both callbacks ran (it fails on the previous head).
Problem
The bounded teardown from #2781 and #2789 left three gaps in how a real-cluster test file ends (findings 1722, 1723, 1725), plus one untested call (1724):
withBoundedTeardowntwice on one raw context gave two teardown blocks. node:test ran them oldest-first, so the first helper's Namespace delete ran before the second helper's worker stop. No current caller does this.arrangeProductionTopologystopped wrapping its context withwithBoundedTeardown, yet the routing and native admin tests rely on that call.Change
Test infrastructure only (
tests/helpers/kubernetes-real.mjs). No product code changes.node.testtracing channel (published in Node 24.16, the CI version). It waits for:beforeEach/afterhooks run outside any test's events);If a Node build doesn't publish these events, the exit waits the grace period alone, as it did before. The poll is unref'd, so it never holds the process. The exit code is now
process.exitCode || 1, so a zero exit code can't slip through.boundedTeardownandwithBoundedTeardownreuse the first bounded teardown made for a raw node:test context, whichever is called first. Every caller and helper given that context then shares one newest-first order, and the first call's options apply.tests/conformance/kubernetes-real-kubectl.test.mjs:beforeEachand a slow suiteafterhook (probe p1, plus the suite-hook case from internal review);arrangeProductionTopologyregistering exactly one bounded hook before its first check. Without a controller image or provider key, it stops before any cluster call.Finding 1726 (time budgets) is left as a note: no file or job timeout changes.
Verification
tests/conformance/kubernetes-real-kubectl.test.mjson Node 24.16.0 (the CI version), pinned to 2 CPUs: 33/33, and 20/20 runs on the final head. It also passes on a newer local Node.arrangeProductionTopology's wrap replaced byconst context = testContext;, the new topology test fails.t.afterin p8 still hangs; that is node:test behavior and not covered by the bound.eslintandprettier --checkpass on both touched files.harness-topology-k3d-*,sandbox-driver-openshell-k3d-real) cannot run on this host, so they were not run.Risks
this. No test in the repo usesthisin a test callback.