Skip to content

The network-escape guard's afterEach covers only the FIRST test file per worker in the unit project (isolate: false) #8537

Description

@claude

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

  1. 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.
  2. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repofindingpm:dispatchedpriority:p2teststooling

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions