mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 22:32:22 +03:00
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!
212 lines
7.7 KiB
TypeScript
212 lines
7.7 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.
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import Database from "better-sqlite3";
|
|
import { resetDbInstance } from "../../src/lib/db/core.ts";
|
|
|
|
// Regression guard for #6260:
|
|
// 1. The mass-migration safety-abort message must tell the operator how to
|
|
// bypass the check (OMNIROUTE_MAX_PENDING_MIGRATIONS=0) — e.g. after
|
|
// restoring a backup where the migration tracking table was wiped.
|
|
// 2. Repeated runMigrations() calls on the same over-threshold DB must throw
|
|
// the SAME memoized MigrationSafetyAbortError instance, so downstream
|
|
// subsystems re-opening the DB do not re-compute + re-log the full abort
|
|
// banner 11+ times (the cascade described in the issue).
|
|
|
|
const serial = { concurrency: false };
|
|
|
|
async function importFresh(modulePath: string) {
|
|
const url = pathToFileURL(path.resolve(modulePath)).href;
|
|
return import(`${url}?test=${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
}
|
|
|
|
function withMockedMigrationFs<T>(files: Record<string, string>, fn: () => T): T {
|
|
const originalExistsSync = fs.existsSync;
|
|
const originalReaddirSync = fs.readdirSync;
|
|
const originalReadFileSync = fs.readFileSync;
|
|
|
|
const isMigrationDir = (target: unknown) =>
|
|
String(target).replaceAll("\\", "/").endsWith("/src/lib/db/migrations") ||
|
|
String(target).replaceAll("\\", "/").endsWith("/migrations");
|
|
|
|
fs.existsSync = ((target: fs.PathLike) => {
|
|
if (isMigrationDir(target)) return true;
|
|
const fileName = path.basename(String(target));
|
|
if (Object.hasOwn(files, fileName)) return true;
|
|
return originalExistsSync(target);
|
|
}) as typeof fs.existsSync;
|
|
|
|
fs.readdirSync = ((target: fs.PathLike, options?: unknown) => {
|
|
if (isMigrationDir(target)) return Object.keys(files);
|
|
return (originalReaddirSync as (t: fs.PathLike, o?: unknown) => unknown)(target, options);
|
|
}) as typeof fs.readdirSync;
|
|
|
|
fs.readFileSync = ((target: fs.PathOrFileDescriptor, options?: unknown) => {
|
|
const fileName = path.basename(String(target));
|
|
if (Object.hasOwn(files, fileName)) return files[fileName];
|
|
return (originalReadFileSync as (t: fs.PathOrFileDescriptor, o?: unknown) => unknown)(
|
|
target,
|
|
options
|
|
);
|
|
}) as typeof fs.readFileSync;
|
|
|
|
try {
|
|
return fn();
|
|
} finally {
|
|
fs.existsSync = originalExistsSync;
|
|
fs.readdirSync = originalReaddirSync;
|
|
fs.readFileSync = originalReadFileSync;
|
|
}
|
|
}
|
|
|
|
function withNonTestEnvironment<T>(fn: () => T): T {
|
|
const originalNodeEnv = process.env.NODE_ENV;
|
|
const originalVitest = process.env.VITEST;
|
|
const originalDisableAutoBackup = process.env.DISABLE_SQLITE_AUTO_BACKUP;
|
|
const originalArgv = [...process.argv];
|
|
const originalExecArgv = [...process.execArgv];
|
|
|
|
delete process.env.NODE_ENV;
|
|
delete process.env.VITEST;
|
|
delete process.env.DISABLE_SQLITE_AUTO_BACKUP;
|
|
process.argv = process.argv.filter((arg) => !arg.includes("test"));
|
|
// #7359 made isAutomatedTestProcess() also scan process.execArgv (so `node --test`
|
|
// is caught even when NODE_ENV/VITEST/argv are clean). This harness runs under
|
|
// `node --test`, so execArgv always carries `--test` — strip it here too, or the
|
|
// "non-test" simulation is a no-op.
|
|
process.execArgv = process.execArgv.filter((arg) => !arg.includes("test"));
|
|
|
|
try {
|
|
return fn();
|
|
} finally {
|
|
process.argv = originalArgv;
|
|
process.execArgv = originalExecArgv;
|
|
if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
|
|
else process.env.NODE_ENV = originalNodeEnv;
|
|
if (originalVitest === undefined) delete process.env.VITEST;
|
|
else process.env.VITEST = originalVitest;
|
|
if (originalDisableAutoBackup === undefined) delete process.env.DISABLE_SQLITE_AUTO_BACKUP;
|
|
else process.env.DISABLE_SQLITE_AUTO_BACKUP = originalDisableAutoBackup;
|
|
}
|
|
}
|
|
|
|
// Existing DB with only the migrations table + one applied row and no physical
|
|
// schema sentinel tables, so inferPhysicalSchemaBaseline() returns null and the
|
|
// abort decision depends purely on the resolved threshold.
|
|
function seedExistingDbWithoutPhysicalBaseline(db: InstanceType<typeof Database>) {
|
|
db.exec(`
|
|
CREATE TABLE _omniroute_migrations (
|
|
version TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
`);
|
|
db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run(
|
|
"001",
|
|
"initial_schema"
|
|
);
|
|
}
|
|
|
|
function buildMockMigrationFiles(startVersion: number, endVersion: number, prefix: string) {
|
|
const files: Record<string, string> = {};
|
|
for (let version = startVersion; version <= endVersion; version++) {
|
|
const padded = String(version).padStart(3, "0");
|
|
const fileName = version === 1 ? "001_initial_schema.sql" : `${padded}_${prefix}_${padded}.sql`;
|
|
files[fileName] = `CREATE TABLE ${prefix}_${padded} (id INTEGER);`;
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function createDb() {
|
|
return new Database(":memory:");
|
|
}
|
|
|
|
test.after(() => {
|
|
resetDbInstance();
|
|
});
|
|
|
|
test(
|
|
"abort message tells the operator to set OMNIROUTE_MAX_PENDING_MIGRATIONS=0 to bypass (#6260)",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
try {
|
|
seedExistingDbWithoutPhysicalBaseline(db);
|
|
let thrown: unknown;
|
|
assert.throws(() => {
|
|
try {
|
|
withNonTestEnvironment(() =>
|
|
withMockedMigrationFs(buildMockMigrationFiles(1, 60, "bypass_hint"), () =>
|
|
runner.runMigrations(db)
|
|
)
|
|
);
|
|
} catch (err) {
|
|
thrown = err;
|
|
throw err;
|
|
}
|
|
});
|
|
const message = thrown instanceof Error ? thrown.message : String(thrown);
|
|
assert.match(message, /OMNIROUTE_MAX_PENDING_MIGRATIONS=0/);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"two consecutive aborts on the same over-threshold DB throw the SAME memoized instance (#6260)",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
try {
|
|
seedExistingDbWithoutPhysicalBaseline(db);
|
|
|
|
const runOnce = () =>
|
|
withNonTestEnvironment(() =>
|
|
withMockedMigrationFs(buildMockMigrationFiles(1, 60, "cascade"), () =>
|
|
runner.runMigrations(db)
|
|
)
|
|
);
|
|
|
|
let first: unknown;
|
|
let second: unknown;
|
|
assert.throws(() => {
|
|
try {
|
|
runOnce();
|
|
} catch (err) {
|
|
first = err;
|
|
throw err;
|
|
}
|
|
});
|
|
assert.throws(() => {
|
|
try {
|
|
runOnce();
|
|
} catch (err) {
|
|
second = err;
|
|
throw err;
|
|
}
|
|
});
|
|
|
|
assert.ok(first instanceof runner.MigrationSafetyAbortError);
|
|
assert.ok(second instanceof runner.MigrationSafetyAbortError);
|
|
assert.strictEqual(first, second, "cascade re-triggers must reuse the memoized instance");
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|