Files
OmniRoute/tests/unit/db-install-upgrade-schema-parity.test.ts
Diego Rodrigues de Sa e Souza 3d4f3e4960 test(infra): retry recursive temp-dir removal instead of failing a shard on ENOTEMPTY (#11966) (#11968)
* test(infra): retry recursive temp-dir removal instead of failing a shard on ENOTEMPTY (#11966)

Two shards on release/v3.8.51 went red in one day with the same signature —
"ENOTEMPTY, Directory not empty: /tmp/omniroute-<test>-XXXXXX" — from
combo-same-provider-cascade (Unit Tests fast-path 4/4, on a PR that touches only
.github/) and auth-policy-embeddings-webfetch-7785 (the 20k-test TIA step). Both pass
alone and on re-run: the cleanup races something still writing into the directory
(SQLite WAL/-shm checkpoint, a worker, the backup) and under a loaded hosted runner
the window opens. 1154 test files do their own cleanup with
fs.rmSync(dir, { recursive: true, force: true }); 57 already asked for retries.

One-shot codemod (scripts/ad-hoc/codemod-rm-maxretries.mjs, kept for the record):
every rm / rmSync / rmdirSync option object with `recursive: true` and no
`maxRetries` gains `maxRetries: 5, retryDelay: 100` — Node itself then retries
ENOTEMPTY/EBUSY/EPERM for up to ~0.5 s before giving up. 2243 call sites in 1292
files under tests/, the shared tests/_setup/isolateDataDir.ts exit hook included.
Only the option object changes: no call site, assertion or import is touched.

Validation: prettier and ESLint (with the frozen suppressions) clean on all 1292
files; a random 20-file sample runs green (quota-redis-store hangs identically on
the untouched tree — it needs a Redis on localhost, an environment matter). The
four unit shards on this PR are the full run.

* fix(quality): let check-forgotten-sibling-tests read a 1,000-file diff

The gate shells out to `git diff` through execFileSync with Node's default 1 MB
maxBuffer; the 1,292-file codemod in this PR is the first diff large enough to
overflow it, and the gate died with `spawnSync git ENOBUFS` before comparing
anything. 64 MB is far above any real PR and costs nothing when unused.
2026-08-29 01:17:40 -03:00

119 lines
5.0 KiB
TypeScript

// ENVIRONMENT NOTE (sandbox better-sqlite3 / glibc limitation, not a code defect):
// This test opens a real SQLite database through `src/lib/db/core.ts`. better-sqlite3 is a
// native addon; production and CI load it normally, but some sandboxes ship a system glibc
// older than the prebuilt binary requires ("GLIBC_2.29 not found"), in which case the
// runtime cascades to node:sqlite/sql.js. See tests/unit/_helpers/betterSqlite3Availability.ts.
//
// WHY THIS FILE EXISTS
// --------------------
// `npm run check:install-upgrade` (scripts/check/check-install-upgrade.mjs) proves that a
// CLEAN install and an UPGRADE converge on the same schema, but it costs a full `npm pack`
// plus three global installs and three boots (~17 min in CI) and it can only ever run at
// publish time, against an already-published previous version. It is not a development
// feedback loop, and the v3.8.50 publish run is what proved it: the gate reported 15
// "missing" tables, and the deterministic half of that verdict was never checkable locally.
//
// This file pins the deterministic half in milliseconds:
//
// 1. Every migration file on disk is actually reachable by the runner on a fresh install.
// A file the runner never applies is a table no user ever gets.
// 2. `model_capabilities` is created by the MIGRATION SET, not lazily at runtime.
// A table created on demand by `CREATE TABLE IF NOT EXISTS` inside a feature code
// path exists or not depending on whether that feature happened to run before the
// snapshot — so it diverges between the two install paths by TIMING, not by schema.
// That is precisely how `model_capabilities` surfaced as a divergence on the v3.8.50
// publish run (present in the upgraded database, absent from the clean one).
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const MIGRATIONS_DIR = path.join(REPO_ROOT, "src", "lib", "db", "migrations");
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-install-upgrade-parity-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
const core = await import("../../src/lib/db/core.ts");
// A clean install: core.ts applies the inline SCHEMA_SQL, the `ensure*Columns()` helpers,
// then runMigrations(). This is the exact code path Phase A of the gate exercises.
const db = core.getDbInstance();
test.after(() => {
try {
core.resetDbInstance();
} catch {
/* best effort */
}
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
function migrationFiles(): Array<{ version: string; name: string }> {
return fs
.readdirSync(MIGRATIONS_DIR)
.sort()
.map((file) => /^(\d{3,})_(.+)\.sql$/.exec(file))
.filter((m): m is RegExpExecArray => m !== null)
.map((m) => ({ version: m[1], name: m[2] }));
}
function hasTable(name: string): boolean {
return Boolean(
db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)
);
}
test("a clean install applies every migration file on disk", () => {
const ledger = new Set(
(
db.prepare("SELECT version FROM _omniroute_migrations").all() as Array<{ version: string }>
).map((row) => row.version)
);
const unapplied = migrationFiles()
.filter((m) => !ledger.has(m.version))
.map((m) => `${m.version}_${m.name}`);
assert.deepEqual(
unapplied,
[],
"migration files the runner never applied — an upgrade would not create their tables either"
);
});
test("model_capabilities comes from the migration set, not from a lazy runtime CREATE", () => {
assert.ok(
hasTable("model_capabilities"),
"model_capabilities must be created by a migration so a clean install and an upgrade " +
"converge deterministically instead of depending on whether the models.dev sync ran"
);
});
test("the model_capabilities migration does not drift from ensureCapabilitiesTable()", () => {
const source = fs.readFileSync(path.join(REPO_ROOT, "src", "lib", "modelsDevSync.ts"), "utf8");
const ddl = /CREATE TABLE IF NOT EXISTS model_capabilities\s*\(([\s\S]*?)\n\s*\)/.exec(source);
assert.ok(ddl, "ensureCapabilitiesTable() DDL not found in src/lib/modelsDevSync.ts");
const runtimeColumns = ddl[1]
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0 && !/^PRIMARY KEY/i.test(line))
.map((line) => line.replace(/,$/, "").split(/\s+/)[0])
.sort();
const migrationColumns = (
db.prepare("PRAGMA table_info(model_capabilities)").all() as Array<{ name: string }>
)
.map((column) => column.name)
.sort();
assert.deepEqual(
migrationColumns,
runtimeColumns,
"the migration and the runtime helper must create the same columns — a drift here means " +
"an upgraded database keeps the old shape while a clean install gets the new one"
);
});