diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts index 1dc74c5b2ac..397ae9d490a 100644 --- a/packages/cli/vitest.config.ts +++ b/packages/cli/vitest.config.ts @@ -651,6 +651,7 @@ import { } from '../qa/vitest-filter-preflight/src/index.js'; import { integrationTestFiles, unitTestFiles } from './vitest-tiers.js'; import { readTierMode } from '../../scripts/nightly-tiers.mjs'; +import { memoryBackedTmpEnv } from '../../scripts/vitest-memory-tmp.mjs'; // The two tiers, DERIVED from what the files DO — never written down — over // the population `OS_TEST_TIERS` selects. `vitest-tiers.ts` holds the @@ -989,6 +990,15 @@ export default defineConfig({ // `server.deps.external` entry above; each repeats the console-intercept // disarm — and, since #17647, the registry log level — because a // root-level value is inert under projects. + // + // Each project's `env` also moves `os.tmpdir()` onto /dev/shm when it is + // writable (#27306; scripts/vitest-memory-tmp.mjs). Every SQLite database + // these tests migrate lives in an `mkdtemp` under it. Measured on the + // fifteen `organization-ownership-*` files beside four `dd … conv=fsync` + // writers on the same disk: 145 of 232 cases timed out and the run took + // 1,809 s, against 232 of 232 green in 68 s idle; with TMPDIR there, under + // the same writers, 232 of 232 green in 59 s. Assertions and budgets are + // untouched. projects: [ { extends: true, @@ -999,7 +1009,9 @@ export default defineConfig({ // #13517: quiet the registry's per-item registration chatter — the // engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped // default. Enforced by scripts/check-registry-log-declared.mjs. - env: { OS_REGISTRY_LOG: 'warn' }, + // TMPDIR on a memory-backed directory (#27306): see the root `test` + // block's note above `projects`. + env: { OS_REGISTRY_LOG: 'warn', ...memoryBackedTmpEnv() }, include: UNIT_FILES, }, }, @@ -1012,7 +1024,9 @@ export default defineConfig({ // #13517: quiet the registry's per-item registration chatter — the // engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped // default. Enforced by scripts/check-registry-log-declared.mjs. - env: { OS_REGISTRY_LOG: 'warn' }, + // TMPDIR on a memory-backed directory (#27306): see the root `test` + // block's note above `projects`. + env: { OS_REGISTRY_LOG: 'warn', ...memoryBackedTmpEnv() }, include: INTEGRATION_FILES, }, }, diff --git a/packages/create-objectstack/vitest.config.ts b/packages/create-objectstack/vitest.config.ts index d7ace906a70..7983fa08a77 100644 --- a/packages/create-objectstack/vitest.config.ts +++ b/packages/create-objectstack/vitest.config.ts @@ -1,6 +1,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { configDefaults, defineConfig } from 'vitest/config'; +import { memoryBackedTmpEnv } from '../../scripts/vitest-memory-tmp.mjs'; const HERE = path.dirname(fileURLToPath(import.meta.url)); @@ -38,5 +39,17 @@ export default defineConfig({ // its `test` script. exclude: [...configDefaults.exclude, 'src/templates/**'], environment: 'node', + // Every scaffold these tests write lands under `os.tmpdir()`, children + // included (#27306; scripts/vitest-memory-tmp.mjs). The cost on a loaded + // disk is not fsync (four calls in the whole suite, by strace) but creating + // and removing those trees: ~4,000 unlinks and ~6,000 rmdirs. Measured + // beside four `dd … conv=fsync` writers on the same disk: all 301 cases + // passed, but the `afterAll` that removes the scaffolded tree in + // `created-summary.test.ts` and `scaffold-skills-single-copy.test.ts` ran + // past its 10 s hook budget, so two files failed with no failed case, and + // the run took 236 s against 21 s idle. With TMPDIR there, under the same + // writers, 19 of 19 files green in 20 s. Assertions and budgets are + // untouched. + env: memoryBackedTmpEnv(), }, }); diff --git a/packages/drivers/driver-sql/vitest.config.ts b/packages/drivers/driver-sql/vitest.config.ts index cfa7ef78c07..872d77edefe 100644 --- a/packages/drivers/driver-sql/vitest.config.ts +++ b/packages/drivers/driver-sql/vitest.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'vitest/config'; import path from 'path'; +import { memoryBackedTmpEnv } from '../../../scripts/vitest-memory-tmp.mjs'; export default defineConfig({ test: { @@ -14,6 +15,15 @@ export default defineConfig({ disableConsoleIntercept: true, globals: true, environment: 'node', + // TMPDIR on /dev/shm when it is writable (#27306; + // scripts/vitest-memory-tmp.mjs): the SQLite file tests open their + // databases in an `mkdtemp` under it. Measured on the 23 test files that + // do, beside four `dd … conv=fsync` writers on the same disk: 3 of 192 + // cases timed out (posture conformance, reclaim-space, declared-index + // retired keys) and the run took 98 s, against all green in 28 s idle; with + // TMPDIR there, under the same writers, all green in 28 s. Assertions and + // budgets are untouched. + env: memoryBackedTmpEnv(), // #9350: the live-dialect files each own a schema (Postgres) / database // (MySQL), named in the connection so knex's `client.database()` and the // session cannot disagree. That naming has an ordering constraint — MySQL diff --git a/packages/drivers/driver-sqlite-wasm/vitest.config.ts b/packages/drivers/driver-sqlite-wasm/vitest.config.ts index 08fbf987a6e..a267a3b3f06 100644 --- a/packages/drivers/driver-sqlite-wasm/vitest.config.ts +++ b/packages/drivers/driver-sqlite-wasm/vitest.config.ts @@ -1,7 +1,7 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -import { accessSync, constants } from 'node:fs'; import { defineConfig } from 'vitest/config'; +import { memoryBackedTmpEnv } from '../../../scripts/vitest-memory-tmp.mjs'; // This package re-exported the shared preset until #27076. It now carries one // key of its own, `test.env.TMPDIR`, so the preset's one setting is restated @@ -27,20 +27,9 @@ import { defineConfig } from 'vitest/config'; // tmpfs the same code still runs, `sync()` included: what each case asserts // (bytes on disk, atomic rename, the flush chain, the failure report) is // unchanged; only the wait for unrelated I/O is gone. Budgets, fixtures and -// cases are untouched. -// -// A host without a writable /dev/shm (macOS, Windows) keeps its own temp -// directory: the setting is absent there, not an error. -const MEMORY_BACKED_TMP = '/dev/shm'; - -function writableDir(dir: string): boolean { - try { - accessSync(dir, constants.W_OK); - return true; - } catch { - return false; - } -} +// cases are untouched. The setting itself, and the host without a writable +// /dev/shm, live in scripts/vitest-memory-tmp.mjs, shared since #27306 with the +// other packages whose tests write SQLite files under `os.tmpdir()`. export default defineConfig({ test: { @@ -51,6 +40,6 @@ export default defineConfig({ // Mechanism + measured costs: examples/app-showcase/vitest.config.ts. // Enforced repo-wide by scripts/check-console-intercept-disarm.mjs. disableConsoleIntercept: true, - env: writableDir(MEMORY_BACKED_TMP) ? { TMPDIR: MEMORY_BACKED_TMP } : {}, + env: memoryBackedTmpEnv(), }, }); diff --git a/packages/drivers/driver-turso/vitest.config.ts b/packages/drivers/driver-turso/vitest.config.ts index 092f3349553..cf628317c5e 100644 --- a/packages/drivers/driver-turso/vitest.config.ts +++ b/packages/drivers/driver-turso/vitest.config.ts @@ -1,4 +1,31 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// This package needs nothing beyond the shared preset; see its header. -export { default } from '../../../vitest.preset.mts'; +import { defineConfig } from 'vitest/config'; +import { memoryBackedTmpEnv } from '../../../scripts/vitest-memory-tmp.mjs'; + +// This package re-exported the shared preset until #27306. It now carries one +// key of its own, `test.env`, so the preset's one setting is restated below (a +// merged or wrapped preset is not what scripts/check-console-intercept-disarm.mjs +// judges in a config's place). Every other key stays on vitest's defaults, as +// under the bare re-export. +// +// `test.env` moves `os.tmpdir()` onto /dev/shm when it is writable +// (scripts/vitest-memory-tmp.mjs): the local and remote-face tests open `file:` +// databases in an `mkdtemp` under it. Measured on the 14 test files that do, +// beside four `dd … conv=fsync` writers on the same disk: 21 of 502 cases +// timed out across 4 files (inherited-members, external-object-parity, +// drift-detection, migration-entries) and the run took 211 s, against all +// green in 17 s idle; with TMPDIR there, under the same writers, all green in +// 17 s. Assertions and budgets are untouched. +export default defineConfig({ + test: { + // A late console.* must not redden a green suite (#10374): vitest's worker + // forwards console output over RPC and discards the promise, and a write + // landing after teardown's rpcDone() snapshot is rejected into an unhandled + // error — a fully green run that exits 1. Disarming removes the mechanism. + // Mechanism + measured costs: examples/app-showcase/vitest.config.ts. + // Enforced repo-wide by scripts/check-console-intercept-disarm.mjs. + disableConsoleIntercept: true, + env: memoryBackedTmpEnv(), + }, +}); diff --git a/packages/runtime/vitest.config.ts b/packages/runtime/vitest.config.ts index 85e39ce7a1d..f48e8a2a312 100644 --- a/packages/runtime/vitest.config.ts +++ b/packages/runtime/vitest.config.ts @@ -34,6 +34,7 @@ // author can see it. import { configDefaults, defineConfig } from 'vitest/config'; +import { memoryBackedTmpEnv } from '../../scripts/vitest-memory-tmp.mjs'; import { readFileSync } from 'node:fs'; import path from 'node:path'; import { parseCLI } from 'vitest/node'; @@ -231,6 +232,15 @@ export default defineConfig({ // Each project re-declares the root block's test options: a ROOT-level // value is inert for a project run (measured on vitest 4.1.10, see // check-registry-log-declared / check-console-intercept-disarm). + // + // Each project's `env` also moves `os.tmpdir()` onto /dev/shm when it is + // writable (#27306; scripts/vitest-memory-tmp.mjs): the package-lifecycle + // integration tests keep their SQLite databases in an `mkdtemp` under it. + // Measured on the 51 test files that do, beside four `dd … conv=fsync` + // writers on the same disk: 25 of 685 cases timed out across 7 files + // (package revert, uninstall, duplicate-adopt, list-commits) and the run + // took 501 s, against all green in 102 s idle; with TMPDIR there, under the + // same writers, all green in 110 s. Assertions and budgets are untouched. projects: [ { extends: true, @@ -242,7 +252,9 @@ export default defineConfig({ // #13517: quiet the registry's per-item registration chatter — the // engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped // default. Enforced by scripts/check-registry-log-declared.mjs. - env: { OS_REGISTRY_LOG: 'warn' }, + // TMPDIR on a memory-backed directory (#27306): see the note above + // `projects`. + env: { OS_REGISTRY_LOG: 'warn', ...memoryBackedTmpEnv() }, // A late console.* must not redden a green suite (#10374); see the root // block. A ROOT-level value is inert for a project run, so it is // declared here as well (scripts/check-console-intercept-disarm.mjs). @@ -258,7 +270,9 @@ export default defineConfig({ // #13517: quiet the registry's per-item registration chatter — the // engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped // default. Enforced by scripts/check-registry-log-declared.mjs. - env: { OS_REGISTRY_LOG: 'warn' }, + // TMPDIR on a memory-backed directory (#27306): see the note above + // `projects`. + env: { OS_REGISTRY_LOG: 'warn', ...memoryBackedTmpEnv() }, // A late console.* must not redden a green suite (#10374); see the root // block. A ROOT-level value is inert for a project run, so it is // declared here as well (scripts/check-console-intercept-disarm.mjs). diff --git a/packages/services/service-automation/vitest.config.ts b/packages/services/service-automation/vitest.config.ts index 903ec636486..8827ab3306c 100644 --- a/packages/services/service-automation/vitest.config.ts +++ b/packages/services/service-automation/vitest.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from 'vitest/config'; import path from 'node:path'; +import { memoryBackedTmpEnv } from '../../../scripts/vitest-memory-tmp.mjs'; /** * [#12157] `plugin.ts` registers the ADR-0126 §4 activation ledger's object, @@ -38,7 +39,13 @@ export default defineConfig({ // ride a bare `console.warn` this level never gates — measured green with // the declaration in place. Enforced by // scripts/check-registry-log-declared.mjs. - env: { OS_REGISTRY_LOG: 'warn' }, + // TMPDIR on /dev/shm when it is writable (#27306; + // scripts/vitest-memory-tmp.mjs): the three tests that persist runs to a + // SQLite file do it in an `mkdtemp` under it. Measured beside four + // `dd … conv=fsync` writers on the same disk: green in that run, but the + // slowest case went from 0.31 s idle to 4.35 s, 87% of its 5 s budget, for + // no change in the code; with TMPDIR there, under the same writers, 0.34 s. + env: { OS_REGISTRY_LOG: 'warn', ...memoryBackedTmpEnv() }, }, resolve: { alias: [ diff --git a/packages/services/service-package/vitest.config.ts b/packages/services/service-package/vitest.config.ts index 0d74c903d67..102d2036cc7 100644 --- a/packages/services/service-package/vitest.config.ts +++ b/packages/services/service-package/vitest.config.ts @@ -1,11 +1,12 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// This config exists for three settings; everything else stays on vitest's +// This config exists for four settings; everything else stays on vitest's // defaults, deliberately — a key added here re-specifies behaviour for every // test file in the package (packages/cli/vitest.config.ts's header records the // incident that taught that). import { defineConfig } from 'vitest/config'; import path from 'node:path'; +import { memoryBackedTmpEnv } from '../../../scripts/vitest-memory-tmp.mjs'; export default defineConfig({ test: { @@ -19,7 +20,13 @@ export default defineConfig({ // #13517: quiet the registry's per-item registration chatter — the // engine's own `OS_REGISTRY_LOG` seam, not a change to its shipped // default. Enforced by scripts/check-registry-log-declared.mjs. - env: { OS_REGISTRY_LOG: 'warn' }, + // TMPDIR on /dev/shm when it is writable (#27306; + // scripts/vitest-memory-tmp.mjs): the two tests that open a SQLite file do + // it in an `mkdtemp` under it. Measured beside four `dd … conv=fsync` + // writers on the same disk: green in that run, but the slowest case went + // from 0.17 s idle to 3.04 s, 61% of its 5 s budget, for no change in the + // code; with TMPDIR there, under the same writers, 0.15 s. + env: { OS_REGISTRY_LOG: 'warn', ...memoryBackedTmpEnv() }, }, resolve: { // `hydration-convert-forward.test.ts` boots the REAL engine, protocol and diff --git a/scripts/vitest-memory-tmp.d.mts b/scripts/vitest-memory-tmp.d.mts new file mode 100644 index 00000000000..8a67e1fcafe --- /dev/null +++ b/scripts/vitest-memory-tmp.d.mts @@ -0,0 +1,11 @@ +// Types for `vitest-memory-tmp.mjs`, published for the package vitest configs +// that import it. Several of those configs sit inside a package's +// `tsconfig.test.json` program, where an untyped `.mjs` import is TS7016. See +// `invoked-as.d.mts` for why this pairing exists; this file follows the same +// convention, and `check:declaration-mirrors` holds it in step with the module. + +/** + * The `test.env` that moves `os.tmpdir()` onto /dev/shm: + * `{ TMPDIR: '/dev/shm' }` when that directory is writable, `{}` otherwise. + */ +export function memoryBackedTmpEnv(): Record; diff --git a/scripts/vitest-memory-tmp.mjs b/scripts/vitest-memory-tmp.mjs new file mode 100644 index 00000000000..b64f3ac0804 --- /dev/null +++ b/scripts/vitest-memory-tmp.mjs @@ -0,0 +1,51 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { accessSync, constants } from 'node:fs'; + +// The `test.env` that moves a suite's temp directory onto a memory-backed +// filesystem. A package whose tests write real files under `os.tmpdir()` +// (SQLite databases, scaffolded projects) spreads it into its own config: +// +// test: { disableConsoleIntercept: true, env: memoryBackedTmpEnv() } +// +// Not a key of `vitest.preset.mts`: that preset carries one setting for every +// re-exporter, and this one belongs only to the suites whose wall time was +// measured against the disk. A `.mjs` with a hand-written `.d.mts` beside it, +// like `tsup-drop-sources-content.mjs`: a package's `tsconfig.test.json` +// type-checks its `vitest.config.ts`, where a `.mts` import path is TS5097 +// and an untyped `.mjs` one is TS7016 (`check:declaration-mirrors` holds the +// pair in step). +// +// ## Why +// +// On a Test Core shard the runner's disk is shared by every suite running +// beside this one. A case that fsyncs a file, or a hook that unlinks a tree of +// them, waits for that disk's queue, so its wall time measures the +// neighbours' I/O, not the code. Measured with four `dd … conv=fsync` writers +// on the same disk (#27076, #27306): cases that take tens of milliseconds idle +// took seconds and timed out, while with TMPDIR on /dev/shm under the same +// writers they stayed at their idle times. The code under test runs unchanged +// on a tmpfs, `fsync` included; only the wait for unrelated I/O is gone. +// +// `os.tmpdir()` reads TMPDIR on every call, and a child process the suite +// spawns inherits it, so the one setting covers both. A path built from the +// package or the repository does not follow TMPDIR and is not moved by this. +// +// A host without a writable /dev/shm (macOS, Windows) keeps its own temp +// directory: the setting is absent there, not an error. +const MEMORY_BACKED_TMP = '/dev/shm'; + +/** @param {string} dir */ +function writableDir(dir) { + try { + accessSync(dir, constants.W_OK); + return true; + } catch { + return false; + } +} + +/** @returns {Record} */ +export function memoryBackedTmpEnv() { + return writableDir(MEMORY_BACKED_TMP) ? { TMPDIR: MEMORY_BACKED_TMP } : {}; +}