Files
OmniRoute/tests/unit/db-migration-runner-extra-dirs.test.ts
Armin Anton” ∴ 8f390efffd feat(codex): self-contained codex app-server transport (executor + provider + sign-in) (#11205)
Merged after conflict resolution: the 5 conflicting test files were the base-red drains that #11201 already landed on the tip — kept the tip versions; the feature content is untouched. Validated on the combined batch board + this branch: codex-app-server + codex-gpt56-catalog 25/25, typecheck:core clean, docs-counts green (351 providers), provider-consistency 268/351/0. The opt-in codex-app-server transport (JSON-RPC-over-WS, turn/completed-awaited close, Responses SSE bridge) leaves the default codex path untouched. Thank you @arminanton — a 3.4k-line transport with the docs wave and tests to match!
2026-08-23 10:20:06 -03:00

293 lines
12 KiB
TypeScript

// ENVIRONMENT NOTE (sandbox better-sqlite3 / glibc limitation, not a code defect):
// This test constructs or exercises a real better-sqlite3-backed SQLite database.
// better-sqlite3 is a native addon; production and CI load it normally, but some
// sandboxes/dev boxes ship a system glibc older than the prebuilt binary requires
// ("GLIBC_2.29 not found"), so the native module fails to dlopen and any test that
// reaches better-sqlite3 directly (or asserts stdout that the load-failure warning
// would pollute) fails HERE while passing in CI. This is a known environment
// limitation, not a defect in the code under test: the OmniRoute runtime itself
// cascades to node:sqlite/sql.js when better-sqlite3 is unavailable. See
// tests/unit/_helpers/betterSqlite3Availability.ts for a guard helper.
/**
* tests/unit/db-migration-runner-extra-dirs.test.ts
*
* Extra migration directories with a namespaced version space.
*
* The runner reads exactly one directory (`MIGRATIONS_DIR`) and requires every
* file to be `NNN_name.sql`, recording the bare number as the version. That makes
* the numeric slots a single global namespace: any distribution that ships its own
* migrations alongside the upstream set has to pick numbers out of the same range,
* and upstream keeps appending to it. When both sides claim a number, the runner
* records one name for it and silently treats the other as already applied — the
* migration never runs, on every already-provisioned database.
*
* This suite pins the extension point: `OMNIROUTE_EXTRA_MIGRATIONS_DIRS` maps
* `namespace=directory` entries (separated by `path.delimiter`), and files found
* there are recorded as `<namespace>-<number>` so they can never collide with the
* upstream numeric slots. Unset (the default, and always the case for a plain
* install) the runner behaves exactly as before.
*
* Misconfiguration fails LOUDLY rather than silently skipping schema — a typo'd
* namespace or a moved directory is the same class of defect this mechanism
* exists to prevent.
*
* TEST SHAPE — read before editing. Two constraints, both learned from CI:
*
* 1. `MIGRATIONS_DIR` is resolved ONCE, at module evaluation of
* migrationRunner.ts, so the core directory is fixed here BEFORE the first
* import and never changed again. Re-importing under a cache-busting query
* string to pick up a new value is not reliable under the loader chain CI uses
* (`--import tsx/esm --import setupPolyfill --import isolateDataDir`); an
* earlier revision did that, passed locally and failed in CI. The extra
* directories, by contrast, are resolved at CALL time, so each test varies only
* `OMNIROUTE_EXTRA_MIGRATIONS_DIRS`.
*
* 2. Tests are registered WITHOUT top-level `await`. The bodies are synchronous,
* so awaiting each one drains the event loop between tests and `--test-force-exit`
* (used by every CI test script) cancels the rest of the file.
*/
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 Database from "better-sqlite3";
const tempDirs: string[] = [];
function mkTempDir(prefix: string): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
function writeMigrations(dir: string, files: Record<string, string>): void {
for (const [name, sql] of Object.entries(files)) {
fs.writeFileSync(path.join(dir, name), sql, "utf-8");
}
}
// ── The core directory must be fixed BEFORE migrationRunner.ts is imported ──────
const CORE_DIR = mkTempDir("mig-core-");
writeMigrations(CORE_DIR, {
"001_initial_schema.sql": "CREATE TABLE core_one (id INTEGER);",
"002_core_two.sql": "CREATE TABLE core_two (id INTEGER);",
});
process.env.OMNIROUTE_MIGRATIONS_DIR = CORE_DIR;
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
const { runMigrations } = await import("../../src/lib/db/migrationRunner.ts");
// Cleanup on process exit, NOT via test.after(): the root after-hook fires as soon
// as the first top-level test settles, while a later test has already registered its
// directories and is sitting on an `await` — it would delete a directory still in use.
process.on("exit", () => {
for (const dir of tempDirs) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
/* ignore */
}
}
});
interface RunResult {
count: number;
rows: Array<{ version: string; name: string }>;
tables: string[];
}
/** Run the migrations against a fresh in-memory DB with the given extra-dir spec. */
function runWithExtras(extraSpec: string | null): RunResult {
const prev = process.env.OMNIROUTE_EXTRA_MIGRATIONS_DIRS;
if (extraSpec === null) delete process.env.OMNIROUTE_EXTRA_MIGRATIONS_DIRS;
else process.env.OMNIROUTE_EXTRA_MIGRATIONS_DIRS = extraSpec;
const db = new Database(":memory:");
try {
const count = runMigrations(db as never, { isNewDb: true });
const rows = db
.prepare("SELECT version, name FROM _omniroute_migrations ORDER BY rowid")
.all() as Array<{ version: string; name: string }>;
const tables = (
db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all() as Array<{
name: string;
}>
).map((r) => r.name);
return { count, rows, tables };
} finally {
db.close();
if (prev === undefined) delete process.env.OMNIROUTE_EXTRA_MIGRATIONS_DIRS;
else process.env.OMNIROUTE_EXTRA_MIGRATIONS_DIRS = prev;
}
}
test("sem a env, o runner só enxerga o diretório core (comportamento atual)", async () => {
const r = runWithExtras(null);
assert.deepEqual(
r.rows.map((x) => x.version),
["001", "002"]
);
assert.ok(r.tables.includes("core_one") && r.tables.includes("core_two"));
});
test("migration de diretório extra é aplicada e gravada com versão namespaced", async () => {
const eeDir = mkTempDir("mig-ee-");
writeMigrations(eeDir, { "001_ee_lending.sql": "CREATE TABLE ee_lending (id INTEGER);" });
const r = runWithExtras(`ee=${eeDir}`);
assert.ok(
r.tables.includes("ee_lending"),
`a migration do diretório extra deve ter rodado; count=${r.count} ` +
`rows=${JSON.stringify(r.rows)} tabelas=${r.tables.join(", ")}`
);
assert.deepEqual(
r.rows.map((x) => x.version),
["001", "002", "ee-001"],
"o número do diretório extra é gravado prefixado pelo namespace e depois das core"
);
assert.equal(r.rows.at(-1)?.name, "ee_lending");
});
test("o mesmo número em core e em diretório extra NÃO colide — ambas rodam", async () => {
const eeDir = mkTempDir("mig-collide-");
// Mesmo prefixo numérico de uma migration core: é exatamente o caso que hoje
// faz uma das duas ser silenciosamente considerada já aplicada.
writeMigrations(eeDir, { "002_ee_same_slot.sql": "CREATE TABLE ee_same_slot (id INTEGER);" });
const r = runWithExtras(`ee=${eeDir}`);
assert.ok(r.tables.includes("core_two"), "a core 002 deve ter rodado");
assert.ok(r.tables.includes("ee_same_slot"), "a extra 002 deve ter rodado também");
assert.deepEqual(
r.rows.map((x) => x.version),
["001", "002", "ee-002"]
);
});
test("dois namespaces extras coexistem, cada um no seu espaço de versão", async () => {
const a = mkTempDir("mig-nsa-");
const b = mkTempDir("mig-nsb-");
writeMigrations(a, { "001_from_a.sql": "CREATE TABLE from_a (id INTEGER);" });
writeMigrations(b, { "001_from_b.sql": "CREATE TABLE from_b (id INTEGER);" });
const r = runWithExtras(`ee=${a}${path.delimiter}lab=${b}`);
assert.ok(r.tables.includes("from_a") && r.tables.includes("from_b"));
assert.deepEqual(
r.rows.map((x) => x.version),
["001", "002", "ee-001", "lab-001"]
);
});
test("número duplicado DENTRO de um namespace extra é erro (não pode ser pulado em silêncio)", async () => {
const eeDir = mkTempDir("mig-dup-");
writeMigrations(eeDir, {
"003_first.sql": "CREATE TABLE ee_first (id INTEGER);",
"003_second.sql": "CREATE TABLE ee_second (id INTEGER);",
});
assert.throws(
() => runWithExtras(`ee=${eeDir}`),
/collision/i,
"duas migrations com o mesmo número no mesmo namespace têm que estourar"
);
});
test("spec malformada estoura em vez de ignorar o diretório", async () => {
const eeDir = mkTempDir("mig-malformed-");
writeMigrations(eeDir, { "001_x.sql": "CREATE TABLE x (id INTEGER);" });
assert.throws(
() => runWithExtras(eeDir), // sem "namespace="
/OMNIROUTE_EXTRA_MIGRATIONS_DIRS/,
"entrada sem namespace= é configuração inválida, não um diretório a ignorar"
);
});
test("namespace inválido estoura (só minúsculas/dígitos, começando por letra)", async () => {
const eeDir = mkTempDir("mig-badns-");
writeMigrations(eeDir, { "001_x.sql": "CREATE TABLE x (id INTEGER);" });
assert.throws(
() => runWithExtras(`EE Corp=${eeDir}`),
/namespace/i,
"namespace fora de [a-z][a-z0-9]* tem que estourar"
);
});
test("diretório configurado que não existe estoura (schema faltando em silêncio é o bug)", async () => {
const missing = path.join(os.tmpdir(), `mig-nao-existe-${process.pid}`);
assert.throws(
() => runWithExtras(`ee=${missing}`),
/does not exist/i,
"um diretório explicitamente configurado e ausente é erro de configuração"
);
});
test("arquivos que não casam NNN_nome.sql são ignorados, como no diretório core", async () => {
const eeDir = mkTempDir("mig-junk-");
writeMigrations(eeDir, {
"001_ok.sql": "CREATE TABLE ee_ok (id INTEGER);",
"README.md": "# não é migration",
"rascunho.sql": "CREATE TABLE nope (id INTEGER);",
});
const r = runWithExtras(`ee=${eeDir}`);
assert.ok(r.tables.includes("ee_ok"));
assert.ok(!r.tables.includes("nope"), "arquivo .sql sem prefixo numérico não deve rodar");
assert.deepEqual(
r.rows.map((x) => x.version),
["001", "002", "ee-001"]
);
});
test("diretório core ausente não impede as migrations dos extras", async () => {
// O runner devolve [] assim que MIGRATIONS_DIR não existe. Os extras são um
// conjunto independente: um core ausente não pode fazê-los desaparecer em
// silêncio — é a mesma falha de "schema some sem avisar" que isto previne.
// MIGRATIONS_DIR é lido a cada chamada por fs.existsSync, então basta remover
// o diretório durante o teste e recriá-lo depois.
const eeDir = mkTempDir("mig-extra-only-");
writeMigrations(eeDir, { "001_ee_solo.sql": "CREATE TABLE ee_solo (id INTEGER);" });
const backup = fs.readdirSync(CORE_DIR).map((f) => ({
name: f,
body: fs.readFileSync(path.join(CORE_DIR, f), "utf-8"),
}));
fs.rmSync(CORE_DIR, { recursive: true, force: true });
try {
const r = runWithExtras(`ee=${eeDir}`);
assert.ok(r.tables.includes("ee_solo"), `tabelas: ${r.tables.join(", ")}`);
assert.deepEqual(
r.rows.map((x) => x.version),
["ee-001"]
);
} finally {
fs.mkdirSync(CORE_DIR, { recursive: true });
for (const f of backup) fs.writeFileSync(path.join(CORE_DIR, f.name), f.body, "utf-8");
}
});
test("rodar duas vezes não reaplica as migrations do diretório extra", async () => {
const eeDir = mkTempDir("mig-idem-");
writeMigrations(eeDir, { "001_ee_idem.sql": "CREATE TABLE ee_idem (id INTEGER);" });
const prev = process.env.OMNIROUTE_EXTRA_MIGRATIONS_DIRS;
process.env.OMNIROUTE_EXTRA_MIGRATIONS_DIRS = `ee=${eeDir}`;
const db = new Database(":memory:");
try {
const first = runMigrations(db as never, { isNewDb: true });
const second = runMigrations(db as never);
assert.equal(first, 3, "primeira execução aplica core 001/002 + ee-001");
assert.equal(second, 0, "segunda execução não tem nada pendente");
} finally {
db.close();
if (prev === undefined) delete process.env.OMNIROUTE_EXTRA_MIGRATIONS_DIRS;
else process.env.OMNIROUTE_EXTRA_MIGRATIONS_DIRS = prev;
}
});