Filed by the os-dev seat while implementing objectui#8500 (session session_01FhBNJcLRZLe8M87VcUgpKr). Grading, labels and domain:* are the triage seat's. Measured on origin/main c30026715.
The defect
vitest.setup.network-escape-guard.ts is objectui#6640's standing guard, and its own header states what it is for: "the afterEach that fails ANY escape in ANY file". In the unit project it fails an escape in exactly one file per worker — the first one that worker runs. Every file after that escapes silently.
Measured
Two identical test files, both reaching a real socket at happy-dom's default origin, run in one worker of the unit project:
pnpm exec vitest run --project unit --maxWorkers=1 --fileParallelism=false \
scripts/__tests__/zzesc-1-escape.test.ts scripts/__tests__/zzesc-2-escape.test.ts
ZZORDER 1 <- execution order, from the files' own console.log
ZZORDER 2
file: scripts/__tests__/zzesc-1-escape.test.ts <- the only Network escape raised
Test Files 1 failed | 1 passed (2)
Each file, run alone, reds. Run together, only the first one does. Both bodies are byte-identical apart from the label.
The mechanism
The unit project runs isolate: false. Vitest re-executes setupFiles for every test file even with the module graph shared, but a module those setup files import is evaluated once per worker — so a hook registered in such a module's body attaches to the first test file of the worker and to no other.
vitest.setup.network-escape-guard.ts is imported by vitest.setup.base.ts (line 12); it is not itself a setupFiles entry. So its afterEach registers once. Its globalThis.fetch wrapper is unaffected — that is a module-scope assignment on a shared global and survives fine, which is why the recording half keeps working while the asserting half does not.
Probe that isolates the mechanism, same project shape:
ZZPROBE inner body ran, count= 1 <- module imported BY a setup file
ZZPROBE setup body ran, count= 1 <- the setup file itself
ZZPROBE afterEach n= 1 <- setup file's hook, file 1
ZZPROBE inner afterEach n= 1 <- imported module's hook, file 1
ZZPROBE setup body ran, count= 2 <- setup file re-executes for file 2
ZZPROBE afterEach n= 2 <- its hook fires again
<- the imported module's hook does NOT
Blast radius, and why this is not objectui#8500
- Only the
unit project. dom / dom-heavy / dist keep isolate: true, so their setup graph re-executes per file and the hook registers every time.
- objectui#8500 is the other half of the same file's integrity — something replacing the
fetch wrapper — and its fix (a per-file afterAll armed from a real setup file, vitest.setup.shared-global-leak-guard.ts) deliberately sidesteps this mechanism rather than repairing it. Repairing this one is a separate change with a separate blast radius: escapes that are invisible today in the unit project would all become red at once, and how many there are has not been measured.
- The
unit project is environment: 'node', so globalThis.location is undefined and a relative fetch cannot resolve to the escape origin by itself. An absolute one can, as the probe above shows.
Two directions
- Move the registration into a file Vitest re-executes per test file — either make the guard a
setupFiles entry in its own right, or have vitest.setup.base.ts call an installer the guard exports. The latter collides with scripts/__tests__/network-escape-ledger.test.ts's pin that the guard exports nothing, so that pin would need a deliberate edit.
- Measure first: count how many
unit files escape once the hook really covers them, before deciding whether to land the repair in one PR or a burn-down.
Whichever is chosen, the count from direction 2 is worth having before the change lands, because today's green is not evidence of no escapes.
Generated by Claude Code
Filed by the
os-devseat while implementing objectui#8500 (sessionsession_01FhBNJcLRZLe8M87VcUgpKr). Grading, labels anddomain:*are the triage seat's. Measured onorigin/mainc30026715.The defect
vitest.setup.network-escape-guard.tsis objectui#6640's standing guard, and its own header states what it is for: "theafterEachthat fails ANY escape in ANY file". In theunitproject it fails an escape in exactly one file per worker — the first one that worker runs. Every file after that escapes silently.Measured
Two identical test files, both reaching a real socket at happy-dom's default origin, run in one worker of the
unitproject:Each file, run alone, reds. Run together, only the first one does. Both bodies are byte-identical apart from the label.
The mechanism
The
unitproject runsisolate: false. Vitest re-executessetupFilesfor every test file even with the module graph shared, but a module those setup files import is evaluated once per worker — so a hook registered in such a module's body attaches to the first test file of the worker and to no other.vitest.setup.network-escape-guard.tsis imported byvitest.setup.base.ts(line 12); it is not itself asetupFilesentry. So itsafterEachregisters once. ItsglobalThis.fetchwrapper is unaffected — that is a module-scope assignment on a shared global and survives fine, which is why the recording half keeps working while the asserting half does not.Probe that isolates the mechanism, same project shape:
Blast radius, and why this is not objectui#8500
unitproject.dom/dom-heavy/distkeepisolate: true, so their setup graph re-executes per file and the hook registers every time.fetchwrapper — and its fix (a per-fileafterAllarmed from a real setup file,vitest.setup.shared-global-leak-guard.ts) deliberately sidesteps this mechanism rather than repairing it. Repairing this one is a separate change with a separate blast radius: escapes that are invisible today in theunitproject would all become red at once, and how many there are has not been measured.unitproject isenvironment: 'node', soglobalThis.locationis undefined and a relative fetch cannot resolve to the escape origin by itself. An absolute one can, as the probe above shows.Two directions
setupFilesentry in its own right, or havevitest.setup.base.tscall an installer the guard exports. The latter collides withscripts/__tests__/network-escape-ledger.test.ts's pin that the guard exports nothing, so that pin would need a deliberate edit.unitfiles escape once the hook really covers them, before deciding whether to land the repair in one PR or a burn-down.Whichever is chosen, the count from direction 2 is worth having before the change lands, because today's green is not evidence of no escapes.
Generated by Claude Code