Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/8537-network-escape-guard-worker-coverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
---

The network-escape guard's `afterEach` is now registered per test file in the
`unit` project (`isolate: false`) instead of once per worker, so an escape in
any file is red, not only one in the worker's first file (objectui#8537).
Test harness only; no package is released by this change.
44 changes: 42 additions & 2 deletions scripts/__tests__/network-escape-ledger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
* wrapper and the `afterEach` that fails ANY escape in ANY file. Re-adding a
* list is red here; deleting the guard while deleting the list is red here too.
*
* "ANY file" has a registration-time condition of its own (objectui#8537): in
* the `unit` project the hook must be registered per test file through the
* guard's exported installer, called from `vitest.setup.base.ts`. That wiring is
* pinned below; the export list this file used to pin as EMPTY now holds exactly
* that installer.
*
* ## How it reads the guard, and why not with a regex
*
* The absence assertions run over the guard's source with COMMENTS BLANKED, via
Expand Down Expand Up @@ -76,11 +82,19 @@ describe('the network-escape burn-down list stays retired (objectui#7307)', () =
expect(guardCode, 'comment masking is not blanking comments').not.toContain(PROSE_ONLY);
});

it('exports nothing — the ledger was its only export', () => {
it('exports exactly the installer, and no ledger', () => {
// Runtime, not source: this is the SAME module instance the run is guarded
// by (`vitest.setup.base.ts` imports it for every project), so a re-exported
// list shows up here whatever the source looks like.
expect(Object.keys(guard as Record<string, unknown>).sort()).toEqual([]);
//
// This used to pin an EMPTY export list — the ledger was the only export
// the guard ever had. objectui#8537 added one on purpose: the `afterEach`
// has to be registered per test file, and under the `unit` project's
// `isolate: false` only a function CALLED from the setup file can do that
// (an imported module's body runs once per worker). Exactly that name, so a
// list coming back as a second export is still red here.
expect(Object.keys(guard as Record<string, unknown>).sort()).toEqual(['installNetworkEscapeGuard']);
expect(typeof guard.installNetworkEscapeGuard).toBe('function');
});

it('declares no allowlist of tolerated escapes in its code', () => {
Expand Down Expand Up @@ -132,4 +146,30 @@ describe('the STANDING guard survived the retirement (objectui#6640)', () => {
'Fix: serve the probe from a double rather than the network',
);
});

it('registers that afterEach from the installer, and the setup file calls it (objectui#8537)', () => {
// "ANY file" is only true if the hook is registered per test file. In the
// `unit` project (`isolate: false`) this module's body runs once per worker,
// so a module-scope `afterEach(` here would cover the first file of each
// worker and no other — which is what it did until objectui#8537. So: the
// one `afterEach(` in the guard's CODE sits inside the exported installer,
// and `vitest.setup.base.ts` — a setup file Vitest re-executes per test
// file — calls it. The behavioural half (two escaping files in one worker,
// both red) is `network-escape-worker-coverage-8537.test.ts`.
const hooks = guardCode.match(/\bafterEach\(/g) ?? [];
expect(hooks, 'exactly one afterEach registration in the guard').toHaveLength(1);
const installerAt = guardCode.indexOf('export function installNetworkEscapeGuard(');
expect(installerAt, 'the installer is exported').toBeGreaterThan(-1);
expect(
guardCode.indexOf('afterEach('),
'the afterEach must be registered inside installNetworkEscapeGuard(), not at module scope',
).toBeGreaterThan(installerAt);

const baseCode = maskComments(fs.readFileSync(path.join(repoRoot, 'vitest.setup.base.ts'), 'utf8'));
expect(baseCode, 'vitest.setup.base.ts must call the installer').toContain('installNetworkEscapeGuard();');
expect(
baseCode.includes("import './vitest.setup.network-escape-guard'"),
'a bare side-effect import of the guard registers no hook any more — call the installer',
).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* objectui#8537 — deliberate network-escape fixture (a of two).
*
* This file is one half of the LIVE CONTROL for
* `network-escape-worker-coverage-8537.test.ts`: that pin spawns a real vitest
* on the `unit` project with both fixtures in ONE worker, and every file must go
* red under the guard, naming itself. In the ordinary suite the escape below is
* skipped — the fixture is inert unless the pin's child marker is set, so it
* cannot red a normal run.
*
* The two fixtures are byte-identical apart from their tag, so a difference in
* their outcome can only come from their POSITION in the worker.
*
* Evidence that the escape RAN goes to a ledger file the pin names, not to the
* console: vitest's default reporter prints a passing test's console output
* nowhere, so a console line would be visible exactly when the test failed —
* a liveness control that can only fire on the outcome it is meant to be
* independent of (measured while writing the pin: under the defect the pin
* went red on "fixture b never ran" when b had run and passed silently).
*/
import { appendFileSync } from 'node:fs';
import { it } from 'vitest';

const IS_CHILD = process.env.OBJECTUI_ESCAPE_PIN_CHILD === '1';
const LEDGER = process.env.OBJECTUI_ESCAPE_PIN_LEDGER;
const MARK = '__objectui_escape_pin_8537__';
const scope = globalThis as unknown as Record<string, unknown>;

it.skipIf(!IS_CHILD)('fixture a reaches a real socket, on purpose', async () => {
// Leave a mark on the shared global object and report the other fixture's,
// so the pin can see that both files ran in ONE worker and which came second.
const seenOther = scope[MARK] === 'b' ? 'yes' : 'no';
scope[MARK] = 'a';
if (LEDGER) appendFileSync(LEDGER, `file=a saw_other=${seenOther}\n`);
// The `unit` project is a node environment: no `location`, so only an
// ABSOLUTE URL at the escape origin can be an escape. Outcome irrelevant.
await fetch('http://localhost:3000/__objectui_escape_pin_8537__/a').catch(() => undefined);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* objectui#8537 — deliberate network-escape fixture (b of two).
*
* This file is one half of the LIVE CONTROL for
* `network-escape-worker-coverage-8537.test.ts`: that pin spawns a real vitest
* on the `unit` project with both fixtures in ONE worker, and every file must go
* red under the guard, naming itself. In the ordinary suite the escape below is
* skipped — the fixture is inert unless the pin's child marker is set, so it
* cannot red a normal run.
*
* The two fixtures are byte-identical apart from their tag, so a difference in
* their outcome can only come from their POSITION in the worker.
*
* Evidence that the escape RAN goes to a ledger file the pin names, not to the
* console: vitest's default reporter prints a passing test's console output
* nowhere, so a console line would be visible exactly when the test failed —
* a liveness control that can only fire on the outcome it is meant to be
* independent of (measured while writing the pin: under the defect the pin
* went red on "fixture b never ran" when b had run and passed silently).
*/
import { appendFileSync } from 'node:fs';
import { it } from 'vitest';

const IS_CHILD = process.env.OBJECTUI_ESCAPE_PIN_CHILD === '1';
const LEDGER = process.env.OBJECTUI_ESCAPE_PIN_LEDGER;
const MARK = '__objectui_escape_pin_8537__';
const scope = globalThis as unknown as Record<string, unknown>;

it.skipIf(!IS_CHILD)('fixture b reaches a real socket, on purpose', async () => {
// Leave a mark on the shared global object and report the other fixture's,
// so the pin can see that both files ran in ONE worker and which came second.
const seenOther = scope[MARK] === 'a' ? 'yes' : 'no';
scope[MARK] = 'b';
if (LEDGER) appendFileSync(LEDGER, `file=b saw_other=${seenOther}\n`);
// The `unit` project is a node environment: no `location`, so only an
// ABSOLUTE URL at the escape origin can be an escape. Outcome irrelevant.
await fetch('http://localhost:3000/__objectui_escape_pin_8537__/b').catch(() => undefined);
});
178 changes: 178 additions & 0 deletions scripts/__tests__/network-escape-worker-coverage-8537.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { describe, expect, it } from 'vitest';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { stripAnsi } from './helpers/child-verdict';

/**
* objectui#8537 — the network-escape guard covers EVERY test file in a worker
* of the `unit` project, not only the first.
*
* ## What this guards
*
* The `unit` project runs `isolate: false`. Vitest re-executes each
* `setupFiles` entry per test file, but a module a setup file IMPORTS is
* evaluated once per worker — so an `afterEach` registered in that module's
* body attaches to the first test file of the worker and to no other.
* `vitest.setup.network-escape-guard.ts` is such a module, and that is exactly
* where its `afterEach` used to be registered. Measured on `1cca4415e` with
* three byte-identical escaping files: one worker, `1 failed | 2 passed`; three
* workers, `3 failed`; each alone, red. Coverage was one file per worker, and
* a green suite read as "no escapes".
*
* The repair registers the hook from `vitest.setup.base.ts` through the guard's
* exported `installNetworkEscapeGuard()`, so it is per file.
*
* ## Why a spawn, and why two files
*
* An in-process assertion cannot see this defect: whichever file holds the
* assertion, its own hook state says nothing about the NEXT file's. The fact
* has to be measured across a file boundary, inside one worker, on the real
* `unit` project — so a real vitest is spawned with the two fixtures beside
* this file, forced into one worker, and BOTH must go red naming themselves.
*
* ## The caricature this also rejects
*
* A repair that re-registers the hook per file but loses the detection — every
* file "covered", nothing caught — passes any "the hook ran in file 2"
* assertion. Here it reads `0 failed`, which is as red as the defect's
* `1 failed | 1 passed`. Both were run against this pin before it landed.
*
* ## The live controls
*
* A spawn-based pin fails silently in two ways: the fixtures might not have
* escaped at all (skipped, or the marker never reached the child), or they
* might have landed in two workers, where the defect is invisible. So each
* fixture writes a line to a LEDGER FILE when its escape RUNS, and reports
* whether it found the other fixture's mark on the shared global object — which
* is only possible in one worker, for the file that ran second. Exactly one
* `saw_other=yes` is the reading that says "one worker, and one of these was
* not first in it".
*
* A file, not the child's console, and that is measured: vitest's default
* reporter shows a passing test's console output nowhere, so a console-based
* liveness line is present exactly when the test FAILED. Under the defect the
* first draft of this pin went red on "fixture b never ran its escape" while b
* had run and passed silently — a red for the wrong reason, which is the shape
* the pin exists to refuse. The ledger is independent of the outcome.
*
* ## Why the child's output is read ANSI-stripped
*
* The child colours its own output, so every assertion below is made against
* `stripAnsi(...)` and never against the raw bytes. `Test Files 2 failed (2)`
* arrives as `SGR Test Files SGR SGR 2 failed SGR SGR (2) SGR`: `\s+` cannot
* match an SGR sequence and the count is split across two coloured spans, so a
* pattern written against the rendered text cannot match. That is objectui#7897
* — the reason `helpers/child-verdict.ts` exists — and it landed here anyway,
* because it is invisible locally:
*
* - vitest calls `disableDefaultColors()` when `std-env`'s `isAgent` is true,
* and an agent container sets `CLAUDECODE` / `AI_AGENT`. The child inherits
* those (only `VITEST*` keys are dropped below), so an agent's local run is
* UNCOLOURED and green while CI is coloured and red. Measured: same tree,
* `env -u CLAUDECODE -u AI_AGENT` flips the child from 0 to 264 escape
* bytes and this file's `Test Files` assertion from pass to fail.
*
* Stripping at the READER, rather than putting `NO_COLOR` on the child's env,
* is deliberate: the child's reporting environment stays byte-for-byte what CI
* gives it — including the GitHub-Actions annotation reporter it adds itself —
* so what is asserted on is what CI actually produces.
*
* ## Recursion
*
* The child runs only the two fixtures, never this file, so it cannot spawn a
* grandchild; `OBJECTUI_ESCAPE_PIN_CHILD` is what un-skips the fixtures' escape
* there and is never set in the ordinary suite.
*/

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const here = path.dirname(fileURLToPath(import.meta.url));

const FIXTURES = ['a', 'b'].map((tag) =>
path.relative(repoRoot, path.join(here, `network-escape-worker-coverage-8537.escape-${tag}.test.ts`)),
);

const IS_CHILD = process.env.OBJECTUI_ESCAPE_PIN_CHILD === '1';

/** The vitest CLI entry, resolved rather than assumed at a `node_modules` path. */
const vitestCli = (() => {
const require = createRequire(path.join(repoRoot, 'noop.js'));
const pkgPath = require.resolve('vitest/package.json');
const bin = (JSON.parse(fs.readFileSync(pkgPath, 'utf8')).bin as { vitest: string }).vitest;
return path.resolve(path.dirname(pkgPath), bin);
})();

describe('objectui#8537 — the network-escape guard covers every file in a worker', () => {
it('the two fixtures are byte-identical apart from their tag', () => {
// The floor under the spawn below: if the fixtures differed in anything
// but position, a difference in their outcome would not be about coverage.
const [a, b] = FIXTURES.map((f) => fs.readFileSync(path.join(repoRoot, f), 'utf8'));
const normalise = (s: string) => s.replace(/\bfixture [ab]\b/g, 'fixture X').replace(/\b[ab]\b/g, 'X');
expect(a.length).toBeGreaterThan(500);
expect(a).not.toBe(b);
expect(normalise(a)).toBe(normalise(b));
// And they are inert outside the child: the escape is behind the marker,
// and the liveness evidence goes to the ledger, not the console.
for (const src of [a, b]) {
expect(src).toContain('it.skipIf(!IS_CHILD)');
expect(src).toContain('appendFileSync(LEDGER');
}
});

it.skipIf(IS_CHILD)(
'a deliberate escape reds in a file that is NOT first in its worker',
() => {
const env: NodeJS.ProcessEnv = { ...process.env };
// A fresh CLI, not a nested worker of this run.
for (const key of Object.keys(env)) if (key.startsWith('VITEST')) delete env[key];
env.OBJECTUI_ESCAPE_PIN_CHILD = '1';
const ledgerDir = fs.mkdtempSync(path.join(os.tmpdir(), 'objectui-escape-pin-8537-'));
const ledgerPath = path.join(ledgerDir, 'ledger.txt');
env.OBJECTUI_ESCAPE_PIN_LEDGER = ledgerPath;

const { output, status, ledger } = (() => {
try {
const child = spawnSync(
process.execPath,
[vitestCli, 'run', '--project', 'unit', '--maxWorkers=1', '--fileParallelism=false', ...FIXTURES],
{ cwd: repoRoot, encoding: 'utf8', env, timeout: 300_000 },
);
return {
// ANSI-STRIPPED AT THE READER (objectui#7897, the failure the
// helper was extracted for). Everything below reads plain text.
output: stripAnsi(`${child.stdout ?? ''}${child.stderr ?? ''}`),
status: child.status,
ledger: fs.existsSync(ledgerPath) ? fs.readFileSync(ledgerPath, 'utf8') : '',
};
} finally {
fs.rmSync(ledgerDir, { recursive: true, force: true });
}
})();

// Live control 1: both escapes RAN (not skipped, not filtered out) —
// read off the ledger, which does not depend on how the child ended.
expect(ledger, 'fixture a never ran its escape').toContain('file=a ');
expect(ledger, 'fixture b never ran its escape').toContain('file=b ');
// Live control 2: ONE worker, and one of the two was not first in it.
// Under `isolate: false` the second file sees the first file's mark on
// the shared global; in two workers neither would.
const sawOther = ledger.match(/saw_other=yes/g) ?? [];
expect(sawOther, 'the fixtures did not share a worker, so the defect could not show').toHaveLength(1);

// The claim: every file in the worker is covered — BOTH red, each
// attributed to itself. The defect reads `1 failed | 1 passed`; the
// caricature (hook per file, detection lost) reads `2 passed`.
expect(output).toMatch(/Test Files\s+2 failed \(2\)/);
const escapes = output.match(/Network escape: this test reached a REAL socket/g) ?? [];
expect(escapes.length, 'fewer Network escape verdicts than files').toBeGreaterThanOrEqual(2);
for (const fixture of FIXTURES) {
expect(output, `${fixture} was not named by the guard`).toContain(` file: ${fixture}`);
}
expect(status, 'an escape must fail the child run').toBe(1);
},
360_000,
);
});
22 changes: 21 additions & 1 deletion vitest.setup.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,27 @@

import { vi } from 'vitest';
import { installI18nGlobalReset } from './vitest.setup.i18n-global';
import './vitest.setup.network-escape-guard';
import { installNetworkEscapeGuard } from './vitest.setup.network-escape-guard';

// objectui#8537 — the network-escape guard's `afterEach` is registered HERE,
// per execution of this setup file, and not by importing the guard.
//
// The `unit` project runs `isolate: false`. Vitest re-executes this setup file
// for every test file, but a module it imports is evaluated once per worker —
// so a hook registered in that module's body attached to the first test file
// of each worker and to no other (measured: three byte-identical escaping
// files in one worker, `1 failed | 2 passed`). The guard's `fetch` wrapper is a
// module-scope assignment on a shared global and is unaffected; only the
// asserting half needs to be per file, so only that half is a function. Same
// split as `installI18nGlobalReset()` below, whose header carries the same
// reason.
//
// First, deliberately: hooks run in REVERSE registration order, and this one
// registered first (as the side-effect import it replaced) so that it runs
// LAST — after the i18n reset below and after the DOM setups' RTL `cleanup()`,
// whose act-flush can itself issue the read this hook exists to catch. The
// guard's own `Fix:` text describes that ordering to test authors.
installNetworkEscapeGuard();

// objectui#4514 — put react-i18next's GLOBAL default-instance pointer back
// after every test, so a provider-less render resolves the same way whether it
Expand Down
Loading
Loading