Skip to content

Commit 2edc5d5

Browse files
committed
test(dogfood): each file's temporary cwd is created from a base the scratch-dir scan can read
The dispatch-gates self-test refuses a mkdtempSync whose base it cannot read, and the per-file cwd took its base from inject(): a path handed over from the globalSetup. Each file's directory is now made as mkdtempSync(join(tmpdir(), TAG-file-)), where TAG is the run's reserved name (only a name crosses inject(), and a separator in it is refused). The globalSetup sweeps that prefix at run end, then removes the reservation, so the shared boot's handles still live until the run ends and a concurrent run's directories are never touched. Isolation, the leftover guard and the stale-dir clearing are unchanged. Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
1 parent faf8dce commit 2edc5d5

3 files changed

Lines changed: 59 additions & 24 deletions

File tree

‎packages/qa/dogfood/test/per-file-cwd.global-setup.ts‎

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,34 @@
1010
// left by a developer's earlier run on a tree without this isolation, or by
1111
// a crashed run. The guard judges only what THIS run leaves, so an old
1212
// leftover never reds a run that wrote nothing.
13-
// 2. It creates ONE temporary root for the run and hands it to every worker
14-
// through `provide` / `inject`. Each test file makes its own working
15-
// directory under that root.
13+
// 2. It reserves a TAG for the run, `os-dogfood-run-XXXXXX`, as a directory
14+
// `mkdtempSync` creates under the system temp directory, and hands the tag
15+
// (a name, never a path) to every worker through `provide` / `inject`.
16+
// Each test file makes its own working directory directly under the system
17+
// temp directory, named `<tag>-file-XXXXXX`.
1618
//
17-
// At the END of the run it removes that root, and with it every per-file
18-
// directory. The removal is run-level, not per-file: on the `shared-showcase`
19-
// project (`isolate: false`) one memoized boot serves every file on a worker,
20-
// and its SQLite handles stay open in the directory of the file that booted it.
19+
// At the END of the run it removes every directory whose name starts with this
20+
// run's `<tag>-file-`, then the reservation itself. Another run's directories
21+
// carry another tag, so a concurrent run on the same machine is never touched.
22+
// The removal is run-level, not per-file: on the `shared-showcase` project
23+
// (`isolate: false`) one memoized boot serves every file on a worker, and its
24+
// SQLite handles stay open in the directory of the file that booted it.
25+
//
26+
// Why a tag and not a shared parent path (#21924): every `mkdtempSync` base in
27+
// this tree must be one the tree's scratch-directory scan can read, so that an
28+
// in-tree fixture root can never hide behind an expression
29+
// (`scripts/pm/dispatch-gates.mjs`, "no mkdtempSync site in this tree takes a
30+
// base the scan cannot read"). A path handed over through `inject()` is such an
31+
// expression. `join(tmpdir(), ...)` is not: it is outside the tree by
32+
// construction, whatever name follows it.
2133
//
2234
// ⛔ This teardown never JUDGES anything. On vitest 4.1.11 an error thrown from
2335
// a globalSetup teardown is printed as `error during close` and the run still
2436
// exits 0 (measured), so a guard placed here would be a false green. The guard
2537
// is a throwing `afterAll` in the per-file module, which fails a test file.
26-
import { mkdtempSync, rmSync } from 'node:fs';
38+
import { mkdtempSync, readdirSync, rmSync } from 'node:fs';
2739
import { tmpdir } from 'node:os';
28-
import { join } from 'node:path';
40+
import { basename, join } from 'node:path';
2941
import { fileURLToPath } from 'node:url';
3042
import type { TestProject } from 'vitest/node';
3143

@@ -34,19 +46,29 @@ const PACKAGE_ROOT = fileURLToPath(new URL('..', import.meta.url));
3446

3547
declare module 'vitest' {
3648
export interface ProvidedContext {
37-
/** The run's temporary root; each test file makes its working directory under it. */
38-
dogfoodCwdRoot: string;
49+
/** The run's tag; each test file makes its working directory as `join(tmpdir(), '<tag>-file-')`. */
50+
dogfoodRunTag: string;
3951
}
4052
}
4153

42-
let runRoot: string | undefined;
54+
/** The prefix of every per-file directory a run tagged `tag` creates under the system temp directory. */
55+
export function perFileDirPrefix(tag: string): string {
56+
return `${tag}-file-`;
57+
}
58+
59+
let reservation: string | undefined;
4360

4461
export function setup(project: TestProject): void {
4562
rmSync(join(PACKAGE_ROOT, '.objectstack'), { recursive: true, force: true });
46-
runRoot = mkdtempSync(join(tmpdir(), 'os-dogfood-run-'));
47-
project.provide('dogfoodCwdRoot', runRoot);
63+
reservation = mkdtempSync(join(tmpdir(), 'os-dogfood-run-'));
64+
project.provide('dogfoodRunTag', basename(reservation));
4865
}
4966

5067
export function teardown(): void {
51-
if (runRoot) rmSync(runRoot, { recursive: true, force: true });
68+
if (!reservation) return;
69+
const prefix = perFileDirPrefix(basename(reservation));
70+
for (const name of readdirSync(tmpdir())) {
71+
if (name.startsWith(prefix)) rmSync(join(tmpdir(), name), { recursive: true, force: true });
72+
}
73+
rmSync(reservation, { recursive: true, force: true });
5274
}

‎packages/qa/dogfood/test/per-file-cwd.setup.ts‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,18 @@
1919
// ## What it does
2020
//
2121
// At module top level, which runs before the test file's own imports, it makes
22-
// a directory under the run's temporary root and `chdir`s into it. `afterAll`
23-
// restores the previous working directory. The directories are removed at the
24-
// end of the run by the globalSetup, not here: the memoized `shared-showcase`
25-
// boot keeps its SQLite handles open in the first file's directory.
22+
// a directory directly under the system temp directory, named with the run's
23+
// tag (`<tag>-file-XXXXXX`), and `chdir`s into it. `afterAll` restores the
24+
// previous working directory. The directories are removed at the end of the
25+
// run by the globalSetup, which sweeps its own tag, not here: the memoized
26+
// `shared-showcase` boot keeps its SQLite handles open in the first file's
27+
// directory.
28+
//
29+
// The base is spelled `join(tmpdir(), ...)` on purpose (#21924): the tree's
30+
// scratch-directory scan must be able to read every `mkdtempSync` base, and a
31+
// path received through `inject()` is one it cannot read. Only the run's TAG
32+
// comes through `inject()`, as a name component, and it is refused below if
33+
// it could carry a separator.
2634
//
2735
// The invariant for every dogfood author: a file runs in its own temporary
2836
// cwd, so anything cwd-relative it writes is its own and disappears with the
@@ -38,26 +46,31 @@
3846
// what this run leaves.
3947
import { afterAll, inject } from 'vitest';
4048
import { existsSync, mkdtempSync, readdirSync } from 'node:fs';
49+
import { tmpdir } from 'node:os';
4150
import { join } from 'node:path';
4251
import { fileURLToPath } from 'node:url';
52+
import { perFileDirPrefix } from './per-file-cwd.global-setup.js';
4353

4454
/** `packages/qa/dogfood`, resolved from this module's own location. */
4555
const PACKAGE_ROOT = fileURLToPath(new URL('..', import.meta.url));
4656
/** What a file must never leave in the package directory. */
4757
const LEFTOVER = join(PACKAGE_ROOT, '.objectstack', 'data');
4858

49-
const runRoot = inject('dogfoodCwdRoot');
50-
if (!runRoot) {
59+
const runTag = inject('dogfoodRunTag');
60+
if (!runTag) {
5161
throw new Error(
52-
'per-file-cwd.setup.ts: no run root was provided. The globalSetup ' +
62+
'per-file-cwd.setup.ts: no run tag was provided. The globalSetup ' +
5363
'`test/per-file-cwd.global-setup.ts` must be wired in packages/qa/dogfood/vitest.config.ts; ' +
5464
'without it this file would run in the package directory.',
5565
);
5666
}
67+
if (/[\\/]|\.\./.test(runTag)) {
68+
throw new Error(`per-file-cwd.setup.ts: the run tag ${JSON.stringify(runTag)} is not a plain directory name.`);
69+
}
5770

5871
const previousCwd = process.cwd();
5972
const presentAtStart = existsSync(LEFTOVER);
60-
process.chdir(mkdtempSync(join(runRoot, 'file-')));
73+
process.chdir(mkdtempSync(join(tmpdir(), perFileDirPrefix(runTag))));
6174

6275
afterAll(() => {
6376
process.chdir(previousCwd);

‎packages/qa/dogfood/vitest.config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ runProjectCliOverridePreflight({
143143
// `.objectstack/data` exists in the package directory: that throw is the guard.
144144
// - The `globalSetup` below is ROOT-level: one run, one call, covering both
145145
// projects and each `OS_TEST_SHARD` slice (measured). It clears a stale
146-
// `.objectstack` at the start and removes the run's temporary root at the end.
146+
// `.objectstack` at the start and removes the run's per-file directories at the end.
147147
// Its teardown judges nothing, because a throw there exits 0 on vitest 4.1.11.
148148
// Both modules' headers carry the rest, including what a dogfood author owes.
149149
const PER_FILE_CWD = './test/per-file-cwd.setup.ts';

0 commit comments

Comments
 (0)