Files
OmniRoute/tests/_setup/isolateDataDir.ts
Diego Rodrigues de Sa e Souza 240f466f54 test(infra): isolate DATA_DIR per test process; raise Stryker concurrency 1→4 (#4078)
* test(infra): isolate DATA_DIR per test process; raise Stryker concurrency 1→4

Every test process resolved DATA_DIR to the same default (~/.omniroute) when the env
var was unset (src/lib/dataPaths.ts::resolveDataDir), so concurrent test files opened
the SAME on-disk storage.sqlite. node:test spawns a process per file and Stryker spawns
one per sandbox, so this shared file caused cross-file state races:
- SQLite lock contention that hung `npm run test:unit` under high --test-concurrency
  (the ~95-min local hang), and
- the non-deterministic baseline that forced stryker.conf.json to concurrency: 1, which
  in turn could not finish the ~15k-mutant run inside the nightly timeout (the cancelled
  2026-06-16/17 nightly-mutation runs) — blocking Quality Gate v2 / Fase 9 Onda 2.

open-sse/utils/setupPolyfill.ts could NOT host the fix: it is imported by production
(bin/omniroute.mjs, proxyFetch.ts, proxyDispatcher.ts), where redirecting DATA_DIR would
point the live SQLite DB at a throwaway temp dir. So this adds a TEST-ONLY
tests/_setup/isolateDataDir.ts that gives each process its own temp DATA_DIR when none is
set (tests that set DATA_DIR explicitly still win), wired via --import into the test,
mutation and CI invocations.

Verified:
- Stryker dry-run A/B at concurrency=4: FAILS without the isolation import
  (account-fallback-service tap exit 9, a cross-file race) and PASSES with it.
- Full `npm run test:unit` green with isolation (0 fail; a one-off
  chatcore-translation-paths timeout flake did not reproduce and passes 3/3 isolated)
  and noticeably faster — the DB lock contention is gone.
- New tests/unit/isolate-datadir.test.ts guards the contract (unique temp DATA_DIR when
  unset; explicit DATA_DIR respected).

Wired the --import into: package.json (13 test scripts), stryker.conf.json (tap.nodeArgs
+ concurrency 1→4), .github/workflows/quality.yml (TIA step), ci.yml (the 5
unit/coverage/integration commands), and bumped nightly-mutation.yml timeout 120→180 for
the first cold run before the incremental cache is seeded.

* ci(quality): run the TIA gate at CI concurrency (4) to stop oversubscription flakes

The TIA "Impacted unit tests" step (made blocking in #4069) ran its fail-safe via
`npm run test:unit` — concurrency=20, tuned for multi-core dev machines. On a 4-vCPU CI
runner that is 5x oversubscribed, so timing-sensitive tests flake under the load (e.g.
`db-backup-extended` "The database connection is not open", `chatcore-translation-paths`
upstream-timeout). That intermittently fails a blocking gate on legitimate PRs — exactly
what surfaced on the DATA_DIR-isolation PR, whose package.json/workflow changes trip the
__RUN_ALL__ fail-safe.

Run both the impacted set and the fail-safe at --test-concurrency=4, matching the stable
ci.yml unit job. Adds a `test:unit:ci` script (test:unit at concurrency=4). The DATA_DIR
isolation in this PR keeps the parallel run race-free, so the only change here is matching
the runner's core count. Verified locally: db-backup-extended passes 8/8 in isolation
(5 with isolation, 3 without).
2026-06-17 13:46:28 -03:00

37 lines
1.7 KiB
TypeScript

// Test-only DATA_DIR isolation.
//
// Loaded via `node --import ./tests/_setup/isolateDataDir.ts` from the test/mutation
// invocations (package.json test scripts, stryker.conf.json tap.nodeArgs, the
// quality.yml TIA step, and the CI test jobs) — NEVER from production. It MUST stay
// out of open-sse/utils/setupPolyfill.ts, which is also imported by production
// (bin/omniroute.mjs, proxyFetch.ts, proxyDispatcher.ts) where redirecting DATA_DIR
// would point the live SQLite DB at a throwaway temp dir.
//
// Why: node:test spawns a process per test file and Stryker spawns one per sandbox,
// but every process resolves DATA_DIR to the SAME default (~/.omniroute) when the env
// var is unset (see src/lib/dataPaths.ts::resolveDataDir). Concurrent processes then
// open the SAME on-disk storage.sqlite, causing cross-file state races: SQLite lock
// contention that hangs `test:unit` under high `--test-concurrency`, and the
// non-deterministic baseline that forced Stryker to `concurrency: 1`.
//
// Giving each process its own DATA_DIR under the OS temp dir removes the shared file,
// so concurrent test processes never collide. Tests that set DATA_DIR explicitly keep
// winning — this only fills in an isolated default when none was chosen.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
if (!process.env.DATA_DIR) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-test-"));
process.env.DATA_DIR = dir;
// Best-effort cleanup so a long suite run does not leak hundreds of temp DBs.
process.on("exit", () => {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
// ignore — the OS reaps its temp dir eventually.
}
});
}