Skip to content

Commit 9e33ee7

Browse files
test(dogfood): each file's temporary cwd is created from a base the scratch-dir scan can read (#21935)
Fixes #21924 Clause-②: no ## What changes `main`'s hourly `Lint & Type Check` run is red on one `dispatch-gates` self-test case (run `37394652870` at `be97cf3c93`, `Lint & Repo Gates`, step `PM dispatch-gates self-test`): > ✗ no mkdtempSync site in this tree takes a base the scan cannot read — UNRESOLVED: packages/qa/dogfood/test/per-file-cwd.setup.ts:60 (a base this scan cannot read: inject('dogfoodCwdRoot')) PR #21919 added that site for #21914. The guard is right: a `mkdtempSync` base handed over through `inject()` is an expression the tree's scratch-directory scan cannot read. So the fix is at the site, and nothing in the guard or the scan changes. - **`packages/qa/dogfood/test/per-file-cwd.setup.ts`**: each file's directory is now `mkdtempSync(join(tmpdir(), perFileDirPrefix(runTag)))`. The base is `tmpdir()`, outside the tree by construction, and the scan reads it as such. - Only the run's TAG crosses `inject()` (key renamed `dogfoodCwdRoot` to `dogfoodRunTag`), and only as a name component. - A tag carrying a path separator or `..` is refused. - **`packages/qa/dogfood/test/per-file-cwd.global-setup.ts`**: - At start it clears a stale `.objectstack` (unchanged). It reserves the run's tag as `mkdtempSync(join(tmpdir(), 'os-dogfood-run-'))` (unchanged expression) and provides its basename. - At END it removes every system-temp entry named with this run's prefix (`perFileDirPrefix`, exported here and imported by the setup module, so the prefix has one spelling), then the reservation. - The removal stays run-level, so the memoized `shared-showcase` boot keeps its handles until the run ends. A concurrent run carries another tag and is never touched. - The teardown still judges nothing. - **`packages/qa/dogfood/vitest.config.ts`**: one comment line ("removes the run's per-file directories at the end"). Unchanged from #21914: every file still runs in its own temporary cwd; the cwd is restored in `afterAll`; and the throwing `afterAll` guard on `packages/qa/dogfood/.objectstack/data` is untouched. Out of scope here: the rule, the case and `scripts/pm/dispatch-gates.mjs` are untouched. Nothing is skipped or recorded as an exception. ## Evidence - **Reproduced first**, on `origin/main` `faf8dce4` with no change: `pnpm check:pm-dispatch-gates` gave `✗ dispatch-gates self-test: 1 of 1976 case(s) failed.`. The one red case is the line quoted above, at `per-file-cwd.setup.ts:60`. The battery took 821.9s. - **At head `2edc5d59`**: `pnpm check:pm-dispatch-gates` gave `✓ no mkdtempSync site in this tree takes a base the scan cannot read` and `✓ dispatch-gates self-test: 1976 cases pass.` (780.2s). - **The case still fails on a planted unreadable base.** A second worktree was checked out at `2edc5d59`, and `scripts/ablation-replace.mjs` (wrap mode) replaced the site with `process.chdir(mkdtempSync(join(inject('dogfoodRunTag'), 'file-')));`. The anchor went from 1 to 0, and the blob from `dc1d3de3` to `51346b9f`. - Result: `✗ dispatch-gates self-test: 1 of 1976 case(s) failed.`, and the red case was exactly `... UNRESOLVED: packages/qa/dogfood/test/per-file-cwd.setup.ts:73 (a base this scan cannot read: inject('dogfoodRunTag'))`. - Restore was proven: blob after restore equals HEAD (`dc1d3de3`), `git diff HEAD` is empty, and `git status --porcelain` is empty. The second worktree was then removed. - **#21914's behaviour, re-proven at `2edc5d59`**: - Whole dogfood suite, `pnpm --filter @objectstack/dogfood test`: `Test Files 208 passed | 1 skipped (209)`, `Tests 1606 passed | 9 skipped (1615)`. Afterwards `packages/qa/dogfood/.objectstack` does not exist. Zero `/tmp/os-dogfood-run-*` entries before the run and zero after it. - H4 ablation through `scripts/ablation-replace.mjs`: the central `process.chdir(...)` was dropped (blob `dc1d3de3` to `ad6684f2`) and two files were run, `showcase-external-autoconnect` and `showcase-search`. Result: `Test Files 2 failed (2)`. Both failed in the guard: `.../packages/qa/dogfood/.objectstack/data exists after this test file ran. Entries: showcase_external.db` (plus `-shm` / `-wal` on the shared-showcase file). - The restore was proven (blob equals HEAD, `git diff HEAD` empty). The restored leg gave `2 passed`, exit 0, and left nothing in the package dir. Zero temp entries were left after each leg, the red one included. - **Gates**: `dispatch-gates --commands` over the 3 changed paths derived 47 families. All 47 ran at `2edc5d59`, and `dispatch-gates --ran` reported `47 derived famil(ies) accounted for — 47 run, 0 NOT-MEASURED`. - `check:dual-build-cjs-loads` first exited 3 (prerequisite: `packages/apps/studio/dist` missing), then exited 0 after building it. - `pnpm lint` exits 0, and `pnpm --filter @objectstack/dogfood typecheck` is green. - `check:pm-dispatch-gates` is path-scoped, so this PR's CI may not schedule it, which is how the red reached `main`. It was run here in full at the head, as above. ## Acceptance notes - Open PR #21930 also edits `packages/qa/dogfood/vitest.config.ts`, in the `isolated` project's alias block (about line 295). This PR changes one comment line near line 146. The hunks do not overlap. - How this red reached `main`: #21919's own CI did not schedule `check:pm-dispatch-gates`, because that battery is path-scoped and the battery's live-tree half reads every tracked source. This is noted, not filed: the scoping is the merge-group ruling that `lint.yml` documents, and the hourly full run caught the red as designed. --- _Generated by [Claude Code](https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent cc2300b commit 9e33ee7

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)