Skip to content

Commit cf218ee

Browse files
test: seven suites keep their temp databases and scaffolds off the shard's shared disk (memory-backed TMPDIR) (#27379)
domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK <!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgKURKuRGF1ewEVVZKx7U1i4)_ Fixes #27306 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:create-objectstack、cli、runtime、driver-turso、driver-sql、service-package、service-automation 七个包的测试配置,把测试临时目录(TMPDIR)指到内存盘 /dev/shm;新增仓根共享助手 `vitest.memory-tmp.mts`,driver-sqlite-wasm 也改读它。不动任何产品代码、用例、断言或超时。 - 为什么改:这些测试把 SQLite 库或脚手架目录写在系统临时目录里,CI 分片上磁盘被邻居测试占满时,fsync 和删目录要排队,用例或收尾钩子就超时;main 上两次 `Test Core (5/8)` 红灯落在 create-objectstack。 - 风险与回滚:只影响测试进程的临时目录;没有可写 /dev/shm 的机器(macOS、Windows)照旧用系统临时目录。回滚即 revert 本 PR。 - 先看:`vitest.memory-tmp.mts`(理由与开关)、`packages/create-objectstack/vitest.config.ts`(与 main 红灯同形的那一包)、`packages/cli/vitest.config.ts`(两个 project 各自带 env)。 - 两分钟验证:`pnpm --filter create-objectstack test`,应 19 个文件全绿;在 4 个 `dd conv=fsync` 写入者旁跑同一命令,改前 2 个文件因 afterAll 超时失败,改后全绿。 - 需要你做的:无,席位落地。 ## What changes - **`vitest.memory-tmp.mts`** (new, repo root, beside `vitest.preset.mts`): `memoryBackedTmpEnv()` returns `{ TMPDIR: '/dev/shm' }` when `/dev/shm` is writable and `{}` otherwise. It holds the rationale once. It is deliberately not a key of the shared preset, which every re-exporter inherits. - **Seven configs spread it into `test.env`**: `create-objectstack`, `cli`, `runtime`, `driver-turso`, `driver-sql`, `service-package` and `service-automation`. `cli` and `runtime` declare vitest `projects`, where a root-level `env` is inert, so each project's own `env` carries it beside `OS_REGISTRY_LOG`. `driver-turso` re-exported the preset, so it now restates the preset's one setting in its own config, the shape `driver-sqlite-wasm` took in #27245. - **`driver-sqlite-wasm`** (the #27245 precedent) now reads the same helper instead of its private copy. Its behaviour is unchanged. - Each config records its own before and after reading in a comment. No product source changed. No case, assertion, fixture, timeout or budget changed, and no retry was added (the #27076 ruling). ## Why `os.tmpdir()` reads `TMPDIR`, and a child process inherits it. Every database file and scaffold tree these suites write was measured to sit under `os.tmpdir()` (the card asked for this reading per package). So the one setting moves all of it off the runner's shared disk. On a Test Core shard, a case that fsyncs, or a hook that unlinks a tree, waits for the other suites' I/O, so its wall time measures the neighbours rather than the code. **create-objectstack, stated in three parts, as the seat asked:** 1. **The failing case in main's two reds is NOT MEASURED.** Push CI runs 40713919581 and 40718500281 (`Test Core (5/8)`) annotate only `packages/create-objectstack … pnpm run test exited (1)`. Their full job logs and artifacts are served from a storage host that this cloud container's egress proxy refuses (403). The log API returns only the last 5,000 lines, and the failing segment sits near line 33,848 of 42,326. 2. **The local signature matches, but it is not proven to be the same failure.** Beside four `dd conv=fsync` writers, all 301 cases passed while two FILES failed with `Hook timed out in 10000ms`. The timeouts were in the `afterAll` that removes the scaffolded tree (`created-summary.test.ts:134`, `scaffold-skills-single-copy.test.ts:289`). That gives a red package with no failed test named, which is the shape of the annotation. 3. **The mechanism is metadata disk load, not fsync.** `strace -f -c` over the whole suite counted 4 fsync, 4,018 unlink and 6,202 rmdir calls. The scaffolds' creation and removal is what waits on the loaded disk. The same isolation fixes it, because every path is under `os.tmpdir()`. For the other packages, the mechanism is SQLite durability I/O. Under strace, one `cli` file (`organization-ownership-apply.generation-1.test.ts`) made 3,388 fsync calls. One `runtime` file (`package-revert-stored-members.integration.test.ts`) made 784 fsync and 982 unlink calls, the second being SQLite's per-transaction journal deletes. ## Tests Each package was measured three ways on the same files: idle on the base, beside four `dd if=/dev/zero bs=1M count=256 conv=fsync` writers on the same disk on the base, and beside the same writers on the head. All runs were on tree `objectstack@6d7634a` (base) and this branch (head), in a 4 vCPU cloud container with `/` on `/dev/vda` and `/dev/shm` on tmpfs, using `vitest run --maxWorkers=2`. Four synthetic writers are a stress, not a replay of a shard, so a red here means "exposed", not "will go red". The absolute seconds are shared-box readings. | Package (files measured) | Idle, base | Under load, base | Under load, head | |:--|:--|:--|:--| | `create-objectstack` (all 19) | 19/19 files, 21 s | **2/19 files fail** (afterAll hook timeouts, 0/301 cases), 236 s | 19/19 files, 20 s | | `cli` (the 15 `organization-ownership-*` files) | 232/232, 68 s | **145/232 cases time out**, 7 files, 1,809 s | 232/232, 59 s | | `runtime` (51 files using SQLite under `os.tmpdir()`) | 684/684, 102 s | **25/684 time out**, 7 files (package revert, uninstall, duplicate-adopt, list-commits, audit-meta-item), 501 s | 684/684, 110 s | | `driver-turso` (14 files using `os.tmpdir()`) | 502/502, 17 s | **21/502 time out**, 4 files (inherited-members, external-object-parity, drift-detection, migration-entries), 211 s | 502/502, 17 s | | `driver-sql` (23 files using `os.tmpdir()`) | 192/192, 28 s | **3/192 time out** (posture conformance, reclaim-space, declared-index retired keys), 98 s | 192/192, 28 s | | `service-package` (2 files) | 11/11, slowest case 0.17 s | 11/11 green, slowest case **3.04 s** | 11/11, slowest case 0.15 s | | `service-automation` (4 files) | 86/86, slowest case 0.31 s | 86/86 green, slowest case **4.35 s** of its 5 s budget | 86/86, slowest case 0.34 s | Every failure under load on the base is `Test timed out` or `Hook timed out`. None is an assertion failure, and none fails idle. `service-package` and `service-automation` stayed green in this load run (the card's survey had 5 of 11 and 1 of 8 failing). They are treated because the card lists them as exposed and the readings put them at 61% and 87% of budget for no change in code. Full package suites on the head: each affected package's own suite, idle, on the final head `110f05aa` (after merging `origin/main`), unless noted: - `cli` unit layer (`vitest run --project unit`): 346/346 files, 5,032/5,032 tests. The integration layer is CI's; its 14 measured `organization-ownership-*` integration files are in the table above. - `runtime` (`pnpm test`): 471 files, 7,668 passed and 19 skipped. - On `fb2d18d7`, before the merge of `main`, which touched no file of these five: `driver-turso` 95 files, 2,492 passed and 34 skipped; `driver-sql` 236 files passed and 12 skipped, 3,847 tests passed and 214 skipped; `service-package` 10/10 files, 127/127; `service-automation` 217/217 files, 2,768/2,768; `create-objectstack` 19/19 files, 301/301; `driver-sqlite-wasm` 39/39 files, 746/746. ## Acceptance notes - `service-print` (the card's Chromium-profile exception) and the other fsyncing packages in the card's survey (`dogfood`, `plugin-auth`, `verify`, `service-datasource`, `plugin-security`, `plugin-pinyin-search`, `rest`) are not touched. The card's own table measured the last four of those green under load, and `dogfood`, `verify` and `rest` were not in its exposure table. - Some `cli` tests deliberately scaffold under `packages/cli/node_modules/…` or `packages/cli/tmp` (they need the workspace's module resolution) and are not moved by `TMPDIR`. They were not in the measured failing family, and this PR leaves them alone. - The survey's evidence file (`pm-orders/server/evidence/27076/fsync-survey.md`) was not reachable from this account, so every reading above was re-measured here. - No changeset: the diff touches only vitest configs and a root test helper, none of which is in any package's published `files`. `Check Changeset` reports whatever it reports, and the seat decides on the label. ## Verification All on the final head `110f05aa`. <details><summary>Derived gate families: 45 derived, 45 run, all exit 0</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 45 families from the 9 changed paths. Each was run and its exit code recorded, then reconciled with `--ran`: `✓ dispatch-gates --ran: 45 derived famil(ies) accounted for — 45 run, 0 NOT-MEASURED`. Two notes. `check-plugin-teardown-shape.mjs --self-test` first exited 3 on a shallow clone (its pinned fixture commit was unreachable) and exited 0 after `git fetch --unshallow`. `check:dts-closure` and `check:dual-build-cjs-loads` read built output, so they were run after a full `turbo run build`. </details> <details><summary>Lint</summary> `eslint --no-inline-config` over the nine touched files: exit 0. The repo-wide `pnpm lint` is CI's. </details> <details><summary>How the load was made</summary> Four loops of `dd if=/dev/zero of=FILE bs=1M count=256 conv=fsync` on the same disk as `/tmp`, started 3 s before the suite and stopped after it. Each base/head pair used the same file list and `--maxWorkers=2`. The per-file and per-case durations come from vitest's JSON reporter. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (eslint on the nine touched files, exit 0; the repo-wide run is CI's) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (publishes nothing) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [ ] The *Before Submitting* list in CONTRIBUTING.md is done. (the local subset is above; the full farm is CI's) --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CZiAsVSvj5E1XZbWrG)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ad2c286 commit cf218ee

10 files changed

Lines changed: 168 additions & 25 deletions

File tree

‎packages/cli/vitest.config.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,7 @@ import {
651651
} from '../qa/vitest-filter-preflight/src/index.js';
652652
import { integrationTestFiles, unitTestFiles } from './vitest-tiers.js';
653653
import { readTierMode } from '../../scripts/nightly-tiers.mjs';
654+
import { memoryBackedTmpEnv } from '../../scripts/vitest-memory-tmp.mjs';
654655

655656
// The two tiers, DERIVED from what the files DO — never written down — over
656657
// the population `OS_TEST_TIERS` selects. `vitest-tiers.ts` holds the
@@ -989,6 +990,15 @@ export default defineConfig({
989990
// `server.deps.external` entry above; each repeats the console-intercept
990991
// disarm — and, since #17647, the registry log level — because a
991992
// root-level value is inert under projects.
993+
//
994+
// Each project's `env` also moves `os.tmpdir()` onto /dev/shm when it is
995+
// writable (#27306; scripts/vitest-memory-tmp.mjs). Every SQLite database
996+
// these tests migrate lives in an `mkdtemp` under it. Measured on the
997+
// fifteen `organization-ownership-*` files beside four `dd … conv=fsync`
998+
// writers on the same disk: 145 of 232 cases timed out and the run took
999+
// 1,809 s, against 232 of 232 green in 68 s idle; with TMPDIR there, under
1000+
// the same writers, 232 of 232 green in 59 s. Assertions and budgets are
1001+
// untouched.
9921002
projects: [
9931003
{
9941004
extends: true,
@@ -999,7 +1009,9 @@ export default defineConfig({
9991009
// #13517: quiet the registry's per-item registration chatter — the
10001010
// engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped
10011011
// default. Enforced by scripts/check-registry-log-declared.mjs.
1002-
env: { OS_REGISTRY_LOG: 'warn' },
1012+
// TMPDIR on a memory-backed directory (#27306): see the root `test`
1013+
// block's note above `projects`.
1014+
env: { OS_REGISTRY_LOG: 'warn', ...memoryBackedTmpEnv() },
10031015
include: UNIT_FILES,
10041016
},
10051017
},
@@ -1012,7 +1024,9 @@ export default defineConfig({
10121024
// #13517: quiet the registry's per-item registration chatter — the
10131025
// engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped
10141026
// default. Enforced by scripts/check-registry-log-declared.mjs.
1015-
env: { OS_REGISTRY_LOG: 'warn' },
1027+
// TMPDIR on a memory-backed directory (#27306): see the root `test`
1028+
// block's note above `projects`.
1029+
env: { OS_REGISTRY_LOG: 'warn', ...memoryBackedTmpEnv() },
10161030
include: INTEGRATION_FILES,
10171031
},
10181032
},

‎packages/create-objectstack/vitest.config.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import path from 'node:path';
22
import { fileURLToPath } from 'node:url';
33
import { configDefaults, defineConfig } from 'vitest/config';
4+
import { memoryBackedTmpEnv } from '../../scripts/vitest-memory-tmp.mjs';
45

56
const HERE = path.dirname(fileURLToPath(import.meta.url));
67

@@ -38,5 +39,17 @@ export default defineConfig({
3839
// its `test` script.
3940
exclude: [...configDefaults.exclude, 'src/templates/**'],
4041
environment: 'node',
42+
// Every scaffold these tests write lands under `os.tmpdir()`, children
43+
// included (#27306; scripts/vitest-memory-tmp.mjs). The cost on a loaded
44+
// disk is not fsync (four calls in the whole suite, by strace) but creating
45+
// and removing those trees: ~4,000 unlinks and ~6,000 rmdirs. Measured
46+
// beside four `dd … conv=fsync` writers on the same disk: all 301 cases
47+
// passed, but the `afterAll` that removes the scaffolded tree in
48+
// `created-summary.test.ts` and `scaffold-skills-single-copy.test.ts` ran
49+
// past its 10 s hook budget, so two files failed with no failed case, and
50+
// the run took 236 s against 21 s idle. With TMPDIR there, under the same
51+
// writers, 19 of 19 files green in 20 s. Assertions and budgets are
52+
// untouched.
53+
env: memoryBackedTmpEnv(),
4154
},
4255
});

‎packages/drivers/driver-sql/vitest.config.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { defineConfig } from 'vitest/config';
44
import path from 'path';
5+
import { memoryBackedTmpEnv } from '../../../scripts/vitest-memory-tmp.mjs';
56

67
export default defineConfig({
78
test: {
@@ -14,6 +15,15 @@ export default defineConfig({
1415
disableConsoleIntercept: true,
1516
globals: true,
1617
environment: 'node',
18+
// TMPDIR on /dev/shm when it is writable (#27306;
19+
// scripts/vitest-memory-tmp.mjs): the SQLite file tests open their
20+
// databases in an `mkdtemp` under it. Measured on the 23 test files that
21+
// do, beside four `dd … conv=fsync` writers on the same disk: 3 of 192
22+
// cases timed out (posture conformance, reclaim-space, declared-index
23+
// retired keys) and the run took 98 s, against all green in 28 s idle; with
24+
// TMPDIR there, under the same writers, all green in 28 s. Assertions and
25+
// budgets are untouched.
26+
env: memoryBackedTmpEnv(),
1727
// #9350: the live-dialect files each own a schema (Postgres) / database
1828
// (MySQL), named in the connection so knex's `client.database()` and the
1929
// session cannot disagree. That naming has an ordering constraint — MySQL

‎packages/drivers/driver-sqlite-wasm/vitest.config.ts‎

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import { accessSync, constants } from 'node:fs';
43
import { defineConfig } from 'vitest/config';
4+
import { memoryBackedTmpEnv } from '../../../scripts/vitest-memory-tmp.mjs';
55

66
// This package re-exported the shared preset until #27076. It now carries one
77
// key of its own, `test.env.TMPDIR`, so the preset's one setting is restated
@@ -27,20 +27,9 @@ import { defineConfig } from 'vitest/config';
2727
// tmpfs the same code still runs, `sync()` included: what each case asserts
2828
// (bytes on disk, atomic rename, the flush chain, the failure report) is
2929
// unchanged; only the wait for unrelated I/O is gone. Budgets, fixtures and
30-
// cases are untouched.
31-
//
32-
// A host without a writable /dev/shm (macOS, Windows) keeps its own temp
33-
// directory: the setting is absent there, not an error.
34-
const MEMORY_BACKED_TMP = '/dev/shm';
35-
36-
function writableDir(dir: string): boolean {
37-
try {
38-
accessSync(dir, constants.W_OK);
39-
return true;
40-
} catch {
41-
return false;
42-
}
43-
}
30+
// cases are untouched. The setting itself, and the host without a writable
31+
// /dev/shm, live in scripts/vitest-memory-tmp.mjs, shared since #27306 with the
32+
// other packages whose tests write SQLite files under `os.tmpdir()`.
4433

4534
export default defineConfig({
4635
test: {
@@ -51,6 +40,6 @@ export default defineConfig({
5140
// Mechanism + measured costs: examples/app-showcase/vitest.config.ts.
5241
// Enforced repo-wide by scripts/check-console-intercept-disarm.mjs.
5342
disableConsoleIntercept: true,
54-
env: writableDir(MEMORY_BACKED_TMP) ? { TMPDIR: MEMORY_BACKED_TMP } : {},
43+
env: memoryBackedTmpEnv(),
5544
},
5645
});
Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,31 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
// This package needs nothing beyond the shared preset; see its header.
4-
export { default } from '../../../vitest.preset.mts';
3+
import { defineConfig } from 'vitest/config';
4+
import { memoryBackedTmpEnv } from '../../../scripts/vitest-memory-tmp.mjs';
5+
6+
// This package re-exported the shared preset until #27306. It now carries one
7+
// key of its own, `test.env`, so the preset's one setting is restated below (a
8+
// merged or wrapped preset is not what scripts/check-console-intercept-disarm.mjs
9+
// judges in a config's place). Every other key stays on vitest's defaults, as
10+
// under the bare re-export.
11+
//
12+
// `test.env` moves `os.tmpdir()` onto /dev/shm when it is writable
13+
// (scripts/vitest-memory-tmp.mjs): the local and remote-face tests open `file:`
14+
// databases in an `mkdtemp` under it. Measured on the 14 test files that do,
15+
// beside four `dd … conv=fsync` writers on the same disk: 21 of 502 cases
16+
// timed out across 4 files (inherited-members, external-object-parity,
17+
// drift-detection, migration-entries) and the run took 211 s, against all
18+
// green in 17 s idle; with TMPDIR there, under the same writers, all green in
19+
// 17 s. Assertions and budgets are untouched.
20+
export default defineConfig({
21+
test: {
22+
// A late console.* must not redden a green suite (#10374): vitest's worker
23+
// forwards console output over RPC and discards the promise, and a write
24+
// landing after teardown's rpcDone() snapshot is rejected into an unhandled
25+
// error — a fully green run that exits 1. Disarming removes the mechanism.
26+
// Mechanism + measured costs: examples/app-showcase/vitest.config.ts.
27+
// Enforced repo-wide by scripts/check-console-intercept-disarm.mjs.
28+
disableConsoleIntercept: true,
29+
env: memoryBackedTmpEnv(),
30+
},
31+
});

‎packages/runtime/vitest.config.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
// author can see it.
3535

3636
import { configDefaults, defineConfig } from 'vitest/config';
37+
import { memoryBackedTmpEnv } from '../../scripts/vitest-memory-tmp.mjs';
3738
import { readFileSync } from 'node:fs';
3839
import path from 'node:path';
3940
import { parseCLI } from 'vitest/node';
@@ -231,6 +232,15 @@ export default defineConfig({
231232
// Each project re-declares the root block's test options: a ROOT-level
232233
// value is inert for a project run (measured on vitest 4.1.10, see
233234
// check-registry-log-declared / check-console-intercept-disarm).
235+
//
236+
// Each project's `env` also moves `os.tmpdir()` onto /dev/shm when it is
237+
// writable (#27306; scripts/vitest-memory-tmp.mjs): the package-lifecycle
238+
// integration tests keep their SQLite databases in an `mkdtemp` under it.
239+
// Measured on the 51 test files that do, beside four `dd … conv=fsync`
240+
// writers on the same disk: 25 of 685 cases timed out across 7 files
241+
// (package revert, uninstall, duplicate-adopt, list-commits) and the run
242+
// took 501 s, against all green in 102 s idle; with TMPDIR there, under the
243+
// same writers, all green in 110 s. Assertions and budgets are untouched.
234244
projects: [
235245
{
236246
extends: true,
@@ -242,7 +252,9 @@ export default defineConfig({
242252
// #13517: quiet the registry's per-item registration chatter — the
243253
// engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped
244254
// default. Enforced by scripts/check-registry-log-declared.mjs.
245-
env: { OS_REGISTRY_LOG: 'warn' },
255+
// TMPDIR on a memory-backed directory (#27306): see the note above
256+
// `projects`.
257+
env: { OS_REGISTRY_LOG: 'warn', ...memoryBackedTmpEnv() },
246258
// A late console.* must not redden a green suite (#10374); see the root
247259
// block. A ROOT-level value is inert for a project run, so it is
248260
// declared here as well (scripts/check-console-intercept-disarm.mjs).
@@ -258,7 +270,9 @@ export default defineConfig({
258270
// #13517: quiet the registry's per-item registration chatter — the
259271
// engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped
260272
// default. Enforced by scripts/check-registry-log-declared.mjs.
261-
env: { OS_REGISTRY_LOG: 'warn' },
273+
// TMPDIR on a memory-backed directory (#27306): see the note above
274+
// `projects`.
275+
env: { OS_REGISTRY_LOG: 'warn', ...memoryBackedTmpEnv() },
262276
// A late console.* must not redden a green suite (#10374); see the root
263277
// block. A ROOT-level value is inert for a project run, so it is
264278
// declared here as well (scripts/check-console-intercept-disarm.mjs).

‎packages/services/service-automation/vitest.config.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { defineConfig } from 'vitest/config';
22
import path from 'node:path';
3+
import { memoryBackedTmpEnv } from '../../../scripts/vitest-memory-tmp.mjs';
34

45
/**
56
* [#12157] `plugin.ts` registers the ADR-0126 §4 activation ledger's object,
@@ -38,7 +39,13 @@ export default defineConfig({
3839
// ride a bare `console.warn` this level never gates — measured green with
3940
// the declaration in place. Enforced by
4041
// scripts/check-registry-log-declared.mjs.
41-
env: { OS_REGISTRY_LOG: 'warn' },
42+
// TMPDIR on /dev/shm when it is writable (#27306;
43+
// scripts/vitest-memory-tmp.mjs): the three tests that persist runs to a
44+
// SQLite file do it in an `mkdtemp` under it. Measured beside four
45+
// `dd … conv=fsync` writers on the same disk: green in that run, but the
46+
// slowest case went from 0.31 s idle to 4.35 s, 87% of its 5 s budget, for
47+
// no change in the code; with TMPDIR there, under the same writers, 0.34 s.
48+
env: { OS_REGISTRY_LOG: 'warn', ...memoryBackedTmpEnv() },
4249
},
4350
resolve: {
4451
alias: [

‎packages/services/service-package/vitest.config.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
// This config exists for three settings; everything else stays on vitest's
3+
// This config exists for four settings; everything else stays on vitest's
44
// defaults, deliberately — a key added here re-specifies behaviour for every
55
// test file in the package (packages/cli/vitest.config.ts's header records the
66
// incident that taught that).
77
import { defineConfig } from 'vitest/config';
88
import path from 'node:path';
9+
import { memoryBackedTmpEnv } from '../../../scripts/vitest-memory-tmp.mjs';
910

1011
export default defineConfig({
1112
test: {
@@ -19,7 +20,13 @@ export default defineConfig({
1920
// #13517: quiet the registry's per-item registration chatter — the
2021
// engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped
2122
// default. Enforced by scripts/check-registry-log-declared.mjs.
22-
env: { OS_REGISTRY_LOG: 'warn' },
23+
// TMPDIR on /dev/shm when it is writable (#27306;
24+
// scripts/vitest-memory-tmp.mjs): the two tests that open a SQLite file do
25+
// it in an `mkdtemp` under it. Measured beside four `dd … conv=fsync`
26+
// writers on the same disk: green in that run, but the slowest case went
27+
// from 0.17 s idle to 3.04 s, 61% of its 5 s budget, for no change in the
28+
// code; with TMPDIR there, under the same writers, 0.15 s.
29+
env: { OS_REGISTRY_LOG: 'warn', ...memoryBackedTmpEnv() },
2330
},
2431
resolve: {
2532
// `hydration-convert-forward.test.ts` boots the REAL engine, protocol and

‎scripts/vitest-memory-tmp.d.mts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Types for `vitest-memory-tmp.mjs`, published for the package vitest configs
2+
// that import it. Several of those configs sit inside a package's
3+
// `tsconfig.test.json` program, where an untyped `.mjs` import is TS7016. See
4+
// `invoked-as.d.mts` for why this pairing exists; this file follows the same
5+
// convention, and `check:declaration-mirrors` holds it in step with the module.
6+
7+
/**
8+
* The `test.env` that moves `os.tmpdir()` onto /dev/shm:
9+
* `{ TMPDIR: '/dev/shm' }` when that directory is writable, `{}` otherwise.
10+
*/
11+
export function memoryBackedTmpEnv(): Record<string, string>;

‎scripts/vitest-memory-tmp.mjs‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { accessSync, constants } from 'node:fs';
4+
5+
// The `test.env` that moves a suite's temp directory onto a memory-backed
6+
// filesystem. A package whose tests write real files under `os.tmpdir()`
7+
// (SQLite databases, scaffolded projects) spreads it into its own config:
8+
//
9+
// test: { disableConsoleIntercept: true, env: memoryBackedTmpEnv() }
10+
//
11+
// Not a key of `vitest.preset.mts`: that preset carries one setting for every
12+
// re-exporter, and this one belongs only to the suites whose wall time was
13+
// measured against the disk. A `.mjs` with a hand-written `.d.mts` beside it,
14+
// like `tsup-drop-sources-content.mjs`: a package's `tsconfig.test.json`
15+
// type-checks its `vitest.config.ts`, where a `.mts` import path is TS5097
16+
// and an untyped `.mjs` one is TS7016 (`check:declaration-mirrors` holds the
17+
// pair in step).
18+
//
19+
// ## Why
20+
//
21+
// On a Test Core shard the runner's disk is shared by every suite running
22+
// beside this one. A case that fsyncs a file, or a hook that unlinks a tree of
23+
// them, waits for that disk's queue, so its wall time measures the
24+
// neighbours' I/O, not the code. Measured with four `dd … conv=fsync` writers
25+
// on the same disk (#27076, #27306): cases that take tens of milliseconds idle
26+
// took seconds and timed out, while with TMPDIR on /dev/shm under the same
27+
// writers they stayed at their idle times. The code under test runs unchanged
28+
// on a tmpfs, `fsync` included; only the wait for unrelated I/O is gone.
29+
//
30+
// `os.tmpdir()` reads TMPDIR on every call, and a child process the suite
31+
// spawns inherits it, so the one setting covers both. A path built from the
32+
// package or the repository does not follow TMPDIR and is not moved by this.
33+
//
34+
// A host without a writable /dev/shm (macOS, Windows) keeps its own temp
35+
// directory: the setting is absent there, not an error.
36+
const MEMORY_BACKED_TMP = '/dev/shm';
37+
38+
/** @param {string} dir */
39+
function writableDir(dir) {
40+
try {
41+
accessSync(dir, constants.W_OK);
42+
return true;
43+
} catch {
44+
return false;
45+
}
46+
}
47+
48+
/** @returns {Record<string, string>} */
49+
export function memoryBackedTmpEnv() {
50+
return writableDir(MEMORY_BACKED_TMP) ? { TMPDIR: MEMORY_BACKED_TMP } : {};
51+
}

0 commit comments

Comments
 (0)