@webpieces/tooling-testkit 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ import { RulePackManifest, FieldDef, ConfigObject } from '@webpieces/rules-sdk';
2
+ /** Frozen pre-migration policy data for real config-loader fixtures; never used by product code. */
3
+ export declare class PolicyFixture {
4
+ manifests(): readonly RulePackManifest[];
5
+ /** Explicitly declares real CommonJS fixture modules; the real Node module transport loads them. */
6
+ declareIn(root: string, manifests?: readonly RulePackManifest[]): void;
7
+ /** Use deliberately in policy-loading tests, instead of modifying the generic temp-directory helper. */
8
+ makeRepo(prefix: string): string;
9
+ }
10
+ export declare const policyFixture: PolicyFixture;
11
+ export declare const fixtureSchemas: Record<string, Record<string, FieldDef>>;
12
+ export declare const fixtureTuning: Record<string, ConfigObject>;
13
+ export declare const fixtureMigrations: {
14
+ scope: string;
15
+ key: string;
16
+ movedTo: string;
17
+ instruction: string;
18
+ label: string;
19
+ prunable: boolean;
20
+ }[];
21
+ export declare const fixtureHookGuardNames: string[];
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fixtureHookGuardNames = exports.fixtureMigrations = exports.fixtureTuning = exports.fixtureSchemas = exports.policyFixture = exports.PolicyFixture = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("node:fs"));
6
+ const path = tslib_1.__importStar(require("node:path"));
7
+ const spec_temp_dirs_1 = require("./spec-temp-dirs");
8
+ const legacy_owned_policies_json_1 = tslib_1.__importDefault(require("./legacy-owned-policies.json"));
9
+ class OwnershipBaseline {
10
+ schemas;
11
+ tuning;
12
+ seeds;
13
+ migrations;
14
+ owners;
15
+ }
16
+ const baseline = legacy_owned_policies_json_1.default;
17
+ /** Frozen pre-migration policy data for real config-loader fixtures; never used by product code. */
18
+ class PolicyFixture {
19
+ manifests() {
20
+ return Object.entries(baseline.owners).filter(entry => entry[0] !== '@webpieces/eslint-rules').map(entry => {
21
+ const owner = entry[0];
22
+ const ids = entry[1];
23
+ return {
24
+ packageName: owner, packageVersion: '1.0.0-fixture', apiVersion: 2,
25
+ ownedRules: ids.map(id => ({
26
+ id, schema: baseline.schemas[id], schemaApiVersion: 1,
27
+ optionalTuning: baseline.tuning[id] ?? {},
28
+ recommendedSeed: baseline.seeds[id],
29
+ section: owner === '@webpieces/agent-workflow-rules' ? 'hookGuards' : 'rules',
30
+ })),
31
+ contributions: [],
32
+ migrations: baseline.migrations.filter(entry => (owner === '@webpieces/agent-workflow-rules' && (entry.scope === 'rule' || entry.key.startsWith('pr-lifecycle-guard.'))) ||
33
+ (owner === '@webpieces/nx-webpieces-rules' && entry.scope === 'field' && entry.key.startsWith('runtime-architecture.'))),
34
+ safeguards: [],
35
+ };
36
+ });
37
+ }
38
+ /** Explicitly declares real CommonJS fixture modules; the real Node module transport loads them. */
39
+ declareIn(root, manifests = this.manifests()) {
40
+ const packageFile = path.join(root, 'package.json');
41
+ // webpieces-disable no-any-unknown -- fixture package JSON retains unrelated test fields
42
+ const pkg = fs.existsSync(packageFile) ? JSON.parse(fs.readFileSync(packageFile, 'utf8')) : { private: true };
43
+ const declarations = manifests.map((manifest, index) => {
44
+ const module = `./fixture-policy-${index}.cjs`;
45
+ fs.writeFileSync(path.join(root, module), `exports.rulePackManifest = ${JSON.stringify(manifest)};\n`);
46
+ return { module };
47
+ });
48
+ pkg['webpieces'] = { rulePacks: declarations };
49
+ fs.writeFileSync(packageFile, JSON.stringify(pkg, null, 2) + '\n');
50
+ }
51
+ /** Use deliberately in policy-loading tests, instead of modifying the generic temp-directory helper. */
52
+ makeRepo(prefix) {
53
+ const root = spec_temp_dirs_1.specTempDirs.make(prefix);
54
+ this.declareIn(root);
55
+ return root;
56
+ }
57
+ }
58
+ exports.PolicyFixture = PolicyFixture;
59
+ exports.policyFixture = new PolicyFixture();
60
+ exports.fixtureSchemas = baseline.schemas;
61
+ exports.fixtureTuning = baseline.tuning;
62
+ exports.fixtureMigrations = baseline.migrations;
63
+ exports.fixtureHookGuardNames = exports.policyFixture.manifests().flatMap(pack => pack.ownedRules.filter(rule => rule.section === 'hookGuards').map(rule => rule.id));
64
+ //# sourceMappingURL=policy-fixture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-fixture.js","sourceRoot":"","sources":["../../../../../packages/tooling/tooling-testkit/src/policy-fixture.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { RulePackManifest, FieldDef, ConfigObject } from '@webpieces/rules-sdk';\nimport { specTempDirs } from './spec-temp-dirs';\nimport snapshot from './legacy-owned-policies.json';\n\nclass OwnershipBaseline {\n schemas!: Record<string, Record<string, FieldDef>>;\n tuning!: Record<string, ConfigObject>;\n seeds!: Record<string, ConfigObject>;\n migrations!: typeof snapshot.migrations;\n owners!: Record<string, string[]>;\n}\nconst baseline = snapshot as OwnershipBaseline;\n\n/** Frozen pre-migration policy data for real config-loader fixtures; never used by product code. */\nexport class PolicyFixture {\n manifests(): readonly RulePackManifest[] {\n return Object.entries(baseline.owners).filter(entry => entry[0] !== '@webpieces/eslint-rules').map(entry => {\n const owner = entry[0];\n const ids = entry[1];\n return ({\n packageName: owner, packageVersion: '1.0.0-fixture', apiVersion: 2,\n ownedRules: ids.map(id => ({\n id, schema: baseline.schemas[id as keyof typeof baseline.schemas], schemaApiVersion: 1,\n optionalTuning: baseline.tuning[id as keyof typeof baseline.tuning] ?? {},\n recommendedSeed: baseline.seeds[id as keyof typeof baseline.seeds],\n section: owner === '@webpieces/agent-workflow-rules' ? 'hookGuards' : 'rules',\n })),\n contributions: [],\n migrations: baseline.migrations.filter(entry =>\n (owner === '@webpieces/agent-workflow-rules' && (entry.scope === 'rule' || entry.key.startsWith('pr-lifecycle-guard.'))) ||\n (owner === '@webpieces/nx-webpieces-rules' && entry.scope === 'field' && entry.key.startsWith('runtime-architecture.'))),\n safeguards: [],\n } as RulePackManifest);\n });\n }\n\n /** Explicitly declares real CommonJS fixture modules; the real Node module transport loads them. */\n declareIn(root: string, manifests: readonly RulePackManifest[] = this.manifests()): void {\n const packageFile = path.join(root, 'package.json');\n // webpieces-disable no-any-unknown -- fixture package JSON retains unrelated test fields\n const pkg: Record<string, unknown> = fs.existsSync(packageFile) ? JSON.parse(fs.readFileSync(packageFile, 'utf8')) : { private: true };\n const declarations = manifests.map((manifest, index) => {\n const module = `./fixture-policy-${index}.cjs`;\n fs.writeFileSync(path.join(root, module), `exports.rulePackManifest = ${JSON.stringify(manifest)};\\n`);\n return { module };\n });\n pkg['webpieces'] = { rulePacks: declarations };\n fs.writeFileSync(packageFile, JSON.stringify(pkg, null, 2) + '\\n');\n }\n\n /** Use deliberately in policy-loading tests, instead of modifying the generic temp-directory helper. */\n makeRepo(prefix: string): string {\n const root = specTempDirs.make(prefix);\n this.declareIn(root);\n return root;\n }\n}\n\nexport const policyFixture = new PolicyFixture();\nexport const fixtureSchemas = baseline.schemas;\nexport const fixtureTuning = baseline.tuning;\nexport const fixtureMigrations = baseline.migrations;\nexport const fixtureHookGuardNames = policyFixture.manifests().flatMap(pack => pack.ownedRules.filter(rule => rule.section === 'hookGuards').map(rule => rule.id));\n"]}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Creates `$TMPDIR` scratch directories for specs and testkits, and reaps every one of them when the
3
+ * process exits.
4
+ *
5
+ * Use the shared `specTempDirs` instance — the reaper is registered per-instance, so a caller that
6
+ * constructs its own gets a second `exit` listener and no benefit.
7
+ */
8
+ export declare class SpecTempDirs {
9
+ private readonly created;
10
+ private reaperRegistered;
11
+ /**
12
+ * `fs.mkdtempSync` under `os.tmpdir()`, with the path remembered for cleanup.
13
+ *
14
+ * `prefix` keeps the existing `wp-<area>-` convention so a leaked tree still names its creator; the
15
+ * trailing dash matters because mkdtemp appends six random characters directly onto it.
16
+ */
17
+ make(prefix: string): string;
18
+ /**
19
+ * `make`, with symlinks resolved.
20
+ *
21
+ * macOS hands back `/var/folders/...` from `os.tmpdir()` while `/var` is a symlink to `/private/var`,
22
+ * so a fixture that compares a path it was given against one the code under test computed sees two
23
+ * different strings for the same directory. Specs that do that comparison call this instead.
24
+ */
25
+ makeReal(prefix: string): string;
26
+ /**
27
+ * Removes every directory this instance created, then forgets them.
28
+ *
29
+ * Deliberately never throws: it runs from an `exit` handler where a throw would replace a passing
30
+ * test run's exit code with a crash, and a scratch directory that cannot be removed is a leak, not a
31
+ * failure. Callers may also invoke it directly — a long suite that wants its fixtures gone before the
32
+ * end of the run.
33
+ */
34
+ reapAll(): void;
35
+ private registerReaper;
36
+ }
37
+ export declare const specTempDirs: SpecTempDirs;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.specTempDirs = exports.SpecTempDirs = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const os = tslib_1.__importStar(require("os"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ // The Vitest setup loads this leaf before per-spec fs mocks; keep filesystem services lazy.
9
+ const to_error_1 = require("@webpieces/tooling-common/to-error");
10
+ // ---------------------------------------------------------------------------
11
+ // ONE OWNER FOR EVERY `$TMPDIR` SCRATCH TREE THE TOOLING CREATES.
12
+ //
13
+ // THE DEFECT. Every `packages/tooling/**` spec builds its fixture with
14
+ // `specTempDirs.make('wp-<something>-')` and NONE of them removed it afterwards.
15
+ // Measured on one developer machine: 270,200 abandoned `wp-*` directories out of 286,270 total entries
16
+ // in `$TMPDIR` — 64,828 of them from `vitest.setup.mts` alone, which mints one isolated `$HOME` per test
17
+ // FILE per run. The cost is not disk (every `wp-*` prefix together is under 1 GiB; the `$HOME` dirs are
18
+ // empty by design) — it is INODES and the fact that `$TMPDIR` stops being inspectable: `ls` takes ~30s
19
+ // and `du` takes minutes, which is how this went unnoticed for as long as it did.
20
+ //
21
+ // WHY `$TMPDIR` AND NOT `.webpieces/`, WHICH IS WHERE EVERYTHING ELSE GOES. Because for a TEST fixture
22
+ // the whole point is to be outside the repo, and two separate mechanisms depend on that:
23
+ // - `vitest.setup.mts` mints a throwaway `$HOME` so a spec cannot read the developer's real
24
+ // `~/.webpieces/config.json`. A fake HOME inside the repo defeats its own isolation.
25
+ // - The fixtures `git init` and `git worktree add` INSIDE themselves. A nested git repo under the real
26
+ // repo root is what breaks the nx graph — the same hazard that makes `.claude/worktrees/` a
27
+ // mandatory gitignore entry.
28
+ // So the location was never the bug. The missing cleanup was.
29
+ //
30
+ // WHO ACTUALLY CALLS `reapAll`, AND WHY IT IS NOT A LINE IN EACH SPEC. An `afterAll` written into each
31
+ // of the 212 call sites is a line somebody has to remember, forever, in every new spec — precisely the
32
+ // discipline already demonstrated not to hold here. Instead `vitest.setup.mts` calls `reapAll()` in ONE
33
+ // global `afterAll`, so a call site swaps `fs.mkdtempSync(...)` for `specTempDirs.make(...)` and there
34
+ // is no other half to forget.
35
+ //
36
+ // THE `exit` REAPER BELOW IS THE SECONDARY BELT, NOT THE PRIMARY ONE — and the first cut of this file
37
+ // had that backwards. It relied on `process.on('exit')` alone, and a 145-file tooling run then left
38
+ // exactly 145 `wp-vitest-home-` directories behind: a 100% miss. The cause is `pool: 'forks'` (see
39
+ // vitest.config.mts) — vitest KILLS its workers rather than letting them exit, and a killed process runs
40
+ // no exit handler. The handler stays because it costs one listener and does fire for the non-vitest
41
+ // callers (testkits driven from plain node scripts), but nothing here should depend on it.
42
+ //
43
+ // SO CLEANUP IS BEST-EFFORT BY CONSTRUCTION, at two levels: a worker killed mid-file skips the
44
+ // `afterAll` too. That residue is why `CleanTmp` sweeps aged `wp-*` out of `$TMPDIR` via
45
+ // `TmpScratchSweeper` — belt, braces, and a sweep for what both of them miss.
46
+ // ---------------------------------------------------------------------------
47
+ /**
48
+ * Creates `$TMPDIR` scratch directories for specs and testkits, and reaps every one of them when the
49
+ * process exits.
50
+ *
51
+ * Use the shared `specTempDirs` instance — the reaper is registered per-instance, so a caller that
52
+ * constructs its own gets a second `exit` listener and no benefit.
53
+ */
54
+ class SpecTempDirs {
55
+ created = [];
56
+ reaperRegistered = false;
57
+ /**
58
+ * `fs.mkdtempSync` under `os.tmpdir()`, with the path remembered for cleanup.
59
+ *
60
+ * `prefix` keeps the existing `wp-<area>-` convention so a leaked tree still names its creator; the
61
+ * trailing dash matters because mkdtemp appends six random characters directly onto it.
62
+ */
63
+ make(prefix) {
64
+ this.registerReaper();
65
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
66
+ this.created.push(dir);
67
+ return dir;
68
+ }
69
+ /**
70
+ * `make`, with symlinks resolved.
71
+ *
72
+ * macOS hands back `/var/folders/...` from `os.tmpdir()` while `/var` is a symlink to `/private/var`,
73
+ * so a fixture that compares a path it was given against one the code under test computed sees two
74
+ * different strings for the same directory. Specs that do that comparison call this instead.
75
+ */
76
+ makeReal(prefix) {
77
+ return fs.realpathSync(this.make(prefix));
78
+ }
79
+ /**
80
+ * Removes every directory this instance created, then forgets them.
81
+ *
82
+ * Deliberately never throws: it runs from an `exit` handler where a throw would replace a passing
83
+ * test run's exit code with a crash, and a scratch directory that cannot be removed is a leak, not a
84
+ * failure. Callers may also invoke it directly — a long suite that wants its fixtures gone before the
85
+ * end of the run.
86
+ */
87
+ reapAll() {
88
+ const dirs = this.created.splice(0, this.created.length);
89
+ for (const dir of dirs) {
90
+ // webpieces-disable no-unmanaged-exceptions -- chokepoint: this runs from vitest's afterAll and
91
+ // from an exit handler, where a throw would turn a passing run into a crash over a scratch dir
92
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
93
+ try {
94
+ fs.rmSync(dir, { recursive: true, force: true });
95
+ }
96
+ catch (err) {
97
+ const error = (0, to_error_1.toError)(err);
98
+ process.stderr.write(`SpecTempDirs: could not remove ${dir}: ${error.message}\n`);
99
+ }
100
+ }
101
+ }
102
+ // One listener per instance, added lazily so a process that never makes a fixture never registers.
103
+ registerReaper() {
104
+ if (this.reaperRegistered)
105
+ return;
106
+ this.reaperRegistered = true;
107
+ process.on('exit', () => this.reapAll());
108
+ }
109
+ }
110
+ exports.SpecTempDirs = SpecTempDirs;
111
+ // The shared instance — the reaper and the created-list are per-instance, so every caller must use this.
112
+ exports.specTempDirs = new SpecTempDirs();
113
+ //# sourceMappingURL=spec-temp-dirs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec-temp-dirs.js","sourceRoot":"","sources":["../../../../../packages/tooling/tooling-testkit/src/spec-temp-dirs.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,+CAAyB;AACzB,mDAA6B;AAC7B,4FAA4F;AAC5F,iEAA6D;AAE7D,8EAA8E;AAC9E,kEAAkE;AAClE,EAAE;AACF,uEAAuE;AACvE,iFAAiF;AACjF,uGAAuG;AACvG,yGAAyG;AACzG,wGAAwG;AACxG,uGAAuG;AACvG,kFAAkF;AAClF,EAAE;AACF,uGAAuG;AACvG,yFAAyF;AACzF,8FAA8F;AAC9F,yFAAyF;AACzF,yGAAyG;AACzG,gGAAgG;AAChG,iCAAiC;AACjC,8DAA8D;AAC9D,EAAE;AACF,uGAAuG;AACvG,uGAAuG;AACvG,wGAAwG;AACxG,uGAAuG;AACvG,8BAA8B;AAC9B,EAAE;AACF,sGAAsG;AACtG,oGAAoG;AACpG,mGAAmG;AACnG,yGAAyG;AACzG,oGAAoG;AACpG,2FAA2F;AAC3F,EAAE;AACF,+FAA+F;AAC/F,yFAAyF;AACzF,8EAA8E;AAC9E,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAa,YAAY;IACJ,OAAO,GAAa,EAAE,CAAC;IAChC,gBAAgB,GAAG,KAAK,CAAC;IAEjC;;;;;OAKG;IACH,IAAI,CAAC,MAAc;QACf,IAAI,CAAC,cAAc,EAAE,CAAC;QACtB,MAAM,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;QAC3D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACvB,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;;OAMG;IACH,QAAQ,CAAC,MAAc;QACnB,OAAO,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED;;;;;;;OAOG;IACH,OAAO;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACzD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACrB,gGAAgG;YAChG,+FAA+F;YAC/F,8DAA8D;YAC9D,IAAI,CAAC;gBACD,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACrD,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;gBAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kCAAkC,GAAG,KAAK,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;YACtF,CAAC;QACL,CAAC;IACL,CAAC;IAED,mGAAmG;IAC3F,cAAc;QAClB,IAAI,IAAI,CAAC,gBAAgB;YAAE,OAAO;QAClC,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAS,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IACnD,CAAC;CACJ;AAzDD,oCAyDC;AAED,yGAAyG;AAC5F,QAAA,YAAY,GAAG,IAAI,YAAY,EAAE,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\n// The Vitest setup loads this leaf before per-spec fs mocks; keep filesystem services lazy.\nimport { toError } from '@webpieces/tooling-common/to-error';\n\n// ---------------------------------------------------------------------------\n// ONE OWNER FOR EVERY `$TMPDIR` SCRATCH TREE THE TOOLING CREATES.\n//\n// THE DEFECT. Every `packages/tooling/**` spec builds its fixture with\n// `specTempDirs.make('wp-<something>-')` and NONE of them removed it afterwards.\n// Measured on one developer machine: 270,200 abandoned `wp-*` directories out of 286,270 total entries\n// in `$TMPDIR` — 64,828 of them from `vitest.setup.mts` alone, which mints one isolated `$HOME` per test\n// FILE per run. The cost is not disk (every `wp-*` prefix together is under 1 GiB; the `$HOME` dirs are\n// empty by design) — it is INODES and the fact that `$TMPDIR` stops being inspectable: `ls` takes ~30s\n// and `du` takes minutes, which is how this went unnoticed for as long as it did.\n//\n// WHY `$TMPDIR` AND NOT `.webpieces/`, WHICH IS WHERE EVERYTHING ELSE GOES. Because for a TEST fixture\n// the whole point is to be outside the repo, and two separate mechanisms depend on that:\n// - `vitest.setup.mts` mints a throwaway `$HOME` so a spec cannot read the developer's real\n// `~/.webpieces/config.json`. A fake HOME inside the repo defeats its own isolation.\n// - The fixtures `git init` and `git worktree add` INSIDE themselves. A nested git repo under the real\n// repo root is what breaks the nx graph — the same hazard that makes `.claude/worktrees/` a\n// mandatory gitignore entry.\n// So the location was never the bug. The missing cleanup was.\n//\n// WHO ACTUALLY CALLS `reapAll`, AND WHY IT IS NOT A LINE IN EACH SPEC. An `afterAll` written into each\n// of the 212 call sites is a line somebody has to remember, forever, in every new spec — precisely the\n// discipline already demonstrated not to hold here. Instead `vitest.setup.mts` calls `reapAll()` in ONE\n// global `afterAll`, so a call site swaps `fs.mkdtempSync(...)` for `specTempDirs.make(...)` and there\n// is no other half to forget.\n//\n// THE `exit` REAPER BELOW IS THE SECONDARY BELT, NOT THE PRIMARY ONE — and the first cut of this file\n// had that backwards. It relied on `process.on('exit')` alone, and a 145-file tooling run then left\n// exactly 145 `wp-vitest-home-` directories behind: a 100% miss. The cause is `pool: 'forks'` (see\n// vitest.config.mts) — vitest KILLS its workers rather than letting them exit, and a killed process runs\n// no exit handler. The handler stays because it costs one listener and does fire for the non-vitest\n// callers (testkits driven from plain node scripts), but nothing here should depend on it.\n//\n// SO CLEANUP IS BEST-EFFORT BY CONSTRUCTION, at two levels: a worker killed mid-file skips the\n// `afterAll` too. That residue is why `CleanTmp` sweeps aged `wp-*` out of `$TMPDIR` via\n// `TmpScratchSweeper` — belt, braces, and a sweep for what both of them miss.\n// ---------------------------------------------------------------------------\n\n/**\n * Creates `$TMPDIR` scratch directories for specs and testkits, and reaps every one of them when the\n * process exits.\n *\n * Use the shared `specTempDirs` instance — the reaper is registered per-instance, so a caller that\n * constructs its own gets a second `exit` listener and no benefit.\n */\nexport class SpecTempDirs {\n private readonly created: string[] = [];\n private reaperRegistered = false;\n\n /**\n * `fs.mkdtempSync` under `os.tmpdir()`, with the path remembered for cleanup.\n *\n * `prefix` keeps the existing `wp-<area>-` convention so a leaked tree still names its creator; the\n * trailing dash matters because mkdtemp appends six random characters directly onto it.\n */\n make(prefix: string): string {\n this.registerReaper();\n const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));\n this.created.push(dir);\n return dir;\n }\n\n /**\n * `make`, with symlinks resolved.\n *\n * macOS hands back `/var/folders/...` from `os.tmpdir()` while `/var` is a symlink to `/private/var`,\n * so a fixture that compares a path it was given against one the code under test computed sees two\n * different strings for the same directory. Specs that do that comparison call this instead.\n */\n makeReal(prefix: string): string {\n return fs.realpathSync(this.make(prefix));\n }\n\n /**\n * Removes every directory this instance created, then forgets them.\n *\n * Deliberately never throws: it runs from an `exit` handler where a throw would replace a passing\n * test run's exit code with a crash, and a scratch directory that cannot be removed is a leak, not a\n * failure. Callers may also invoke it directly — a long suite that wants its fixtures gone before the\n * end of the run.\n */\n reapAll(): void {\n const dirs = this.created.splice(0, this.created.length);\n for (const dir of dirs) {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: this runs from vitest's afterAll and\n // from an exit handler, where a throw would turn a passing run into a crash over a scratch dir\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n } catch (err: unknown) {\n const error = toError(err);\n process.stderr.write(`SpecTempDirs: could not remove ${dir}: ${error.message}\\n`);\n }\n }\n }\n\n // One listener per instance, added lazily so a process that never makes a fixture never registers.\n private registerReaper(): void {\n if (this.reaperRegistered) return;\n this.reaperRegistered = true;\n process.on('exit', (): void => this.reapAll());\n }\n}\n\n// The shared instance — the reaper and the created-list are per-instance, so every caller must use this.\nexport const specTempDirs = new SpecTempDirs();\n"]}