1491 lines
46 KiB
TypeScript
1491 lines
46 KiB
TypeScript
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";
|
|
|
|
const serial = { concurrency: false };
|
|
|
|
async function importFresh(modulePath) {
|
|
const url = pathToFileURL(path.resolve(modulePath)).href;
|
|
return import(`${url}?test=${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
}
|
|
|
|
function withMockedMigrationFs(files, fn) {
|
|
const originalExistsSync = fs.existsSync;
|
|
const originalReaddirSync = fs.readdirSync;
|
|
const originalReadFileSync = fs.readFileSync;
|
|
|
|
const isMigrationDir = (target) =>
|
|
String(target).replaceAll("\\", "/").endsWith("/src/lib/db/migrations") ||
|
|
String(target).replaceAll("\\", "/").endsWith("/migrations");
|
|
|
|
fs.existsSync = (target) => {
|
|
if (files === null && isMigrationDir(target)) return false;
|
|
if (files && isMigrationDir(target)) return true;
|
|
|
|
const fileName = path.basename(String(target));
|
|
if (files && Object.hasOwn(files, fileName)) return true;
|
|
|
|
return originalExistsSync(target);
|
|
};
|
|
|
|
fs.readdirSync = ((target: string, options?: any) => {
|
|
if (files && isMigrationDir(target)) {
|
|
return Object.keys(files);
|
|
}
|
|
|
|
return originalReaddirSync(target, options);
|
|
}) as any;
|
|
|
|
fs.readFileSync = (target, options) => {
|
|
const fileName = path.basename(String(target));
|
|
if (files && Object.hasOwn(files, fileName)) {
|
|
return files[fileName];
|
|
}
|
|
|
|
return originalReadFileSync(target, options);
|
|
};
|
|
|
|
try {
|
|
return fn();
|
|
} finally {
|
|
fs.existsSync = originalExistsSync;
|
|
fs.readdirSync = originalReaddirSync;
|
|
fs.readFileSync = originalReadFileSync;
|
|
}
|
|
}
|
|
|
|
function createDb() {
|
|
return new Database(":memory:");
|
|
}
|
|
|
|
function createSqlJsLikeDb() {
|
|
const db = createDb();
|
|
|
|
return {
|
|
driver: "sql.js",
|
|
get open() {
|
|
return true;
|
|
},
|
|
get name() {
|
|
return ":memory:";
|
|
},
|
|
prepare(sql) {
|
|
return db.prepare(sql);
|
|
},
|
|
exec(sql) {
|
|
if (/fts5/i.test(sql)) {
|
|
throw new Error("no such module: fts5");
|
|
}
|
|
db.exec(sql);
|
|
},
|
|
pragma(pragmaStr, options) {
|
|
return db.pragma(pragmaStr, options);
|
|
},
|
|
transaction(fn) {
|
|
const tx = db.transaction((...args) => fn(...args));
|
|
return (...args) => tx(...args);
|
|
},
|
|
immediate(fn) {
|
|
fn();
|
|
},
|
|
async backup() {},
|
|
checkpoint() {},
|
|
close() {
|
|
db.close();
|
|
},
|
|
get raw() {
|
|
return db;
|
|
},
|
|
};
|
|
}
|
|
|
|
function createInitialSchemaTables(db) {
|
|
db.exec(`
|
|
CREATE TABLE provider_connections (id TEXT PRIMARY KEY);
|
|
CREATE TABLE combos (id TEXT PRIMARY KEY);
|
|
CREATE TABLE call_logs (id TEXT PRIMARY KEY);
|
|
`);
|
|
}
|
|
|
|
function buildMockMigrationFiles(startVersion, endVersion, prefix) {
|
|
const files = {};
|
|
|
|
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 withNonTestEnvironment(fn) {
|
|
const originalNodeEnv = process.env.NODE_ENV;
|
|
const originalVitest = process.env.VITEST;
|
|
const originalDisableAutoBackup = process.env.DISABLE_SQLITE_AUTO_BACKUP;
|
|
const originalArgv = [...process.argv];
|
|
|
|
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"));
|
|
|
|
try {
|
|
return fn();
|
|
} finally {
|
|
process.argv = originalArgv;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
const REAL_022_ADD_MEMORY_FTS5_SQL = fs.readFileSync(
|
|
path.resolve("src/lib/db/migrations/022_add_memory_fts5.sql"),
|
|
"utf8"
|
|
);
|
|
const REAL_023_FIX_MEMORY_FTS_UUID_SQL = fs.readFileSync(
|
|
path.resolve("src/lib/db/migrations/023_fix_memory_fts_uuid.sql"),
|
|
"utf8"
|
|
);
|
|
|
|
test("migration infrastructure avoids cwd-based repo tracing fallbacks", () => {
|
|
const runnerSource = fs.readFileSync(path.resolve("src/lib/db/migrationRunner.ts"), "utf8");
|
|
const dataPathsSource = fs.readFileSync(path.resolve("src/lib/dataPaths.ts"), "utf8");
|
|
|
|
// dataPaths must never use process.cwd() — it resolves via import.meta.url
|
|
assert.doesNotMatch(dataPathsSource, /process\.cwd\(\)/);
|
|
// migrationRunner uses import.meta.url as the primary strategy (process.cwd is
|
|
// only a last-resort fallback for Windows/CI-built bundles with leaked paths)
|
|
assert.match(runnerSource, /fileURLToPath\(import\.meta\.url\)/);
|
|
});
|
|
|
|
test("runMigrations applies pending files sequentially in version order", serial, async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
const appliedCount = withMockedMigrationFs(
|
|
{
|
|
"010_last.sql": "CREATE TABLE migration_last (id INTEGER);",
|
|
"002_middle.sql": "CREATE TABLE migration_middle (id INTEGER);",
|
|
"001_first.sql": "CREATE TABLE migration_first (id INTEGER);",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
assert.equal(appliedCount, 3);
|
|
assert.deepEqual(
|
|
db.prepare("SELECT version FROM _omniroute_migrations ORDER BY version").all(),
|
|
[{ version: "001" }, { version: "002" }, { version: "010" }]
|
|
);
|
|
assert.ok(
|
|
db
|
|
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
.get("migration_first")
|
|
);
|
|
assert.ok(
|
|
db
|
|
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
.get("migration_last")
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
});
|
|
|
|
test("runMigrations skips versions that are already tracked as applied", serial, async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
withMockedMigrationFs(
|
|
{
|
|
"001_first.sql": "CREATE TABLE skip_first (id INTEGER);",
|
|
"002_second.sql": "CREATE TABLE skip_second (id INTEGER);",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
const secondRun = withMockedMigrationFs(
|
|
{
|
|
"001_first.sql": "CREATE TABLE skip_first (id INTEGER);",
|
|
"002_second.sql": "CREATE TABLE skip_second (id INTEGER);",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
assert.equal(secondRun, 0);
|
|
assert.equal(
|
|
(
|
|
db
|
|
.prepare("SELECT COUNT(*) AS count FROM _omniroute_migrations WHERE version = ?")
|
|
.get("001") as any
|
|
).count,
|
|
1
|
|
);
|
|
assert.equal(
|
|
(
|
|
db
|
|
.prepare("SELECT COUNT(*) AS count FROM _omniroute_migrations WHERE version = ?")
|
|
.get("002") as any
|
|
).count,
|
|
1
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
});
|
|
|
|
test(
|
|
"runMigrations applies api key lifecycle migration idempotently when columns already exist",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE api_keys (
|
|
id TEXT PRIMARY KEY,
|
|
key TEXT NOT NULL,
|
|
revoked_at TEXT
|
|
);
|
|
`);
|
|
|
|
const appliedCount = withMockedMigrationFs(
|
|
{
|
|
"032_apikey_lifecycle.sql": "ALTER TABLE api_keys ADD COLUMN revoked_at TEXT;",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
assert.equal(appliedCount, 1);
|
|
const columns = db.prepare("PRAGMA table_info(api_keys)").all() as Array<{ name: string }>;
|
|
const names = new Set(columns.map((column) => column.name));
|
|
for (const expected of [
|
|
"revoked_at",
|
|
"expires_at",
|
|
"last_used_at",
|
|
"key_prefix",
|
|
"ip_allowlist",
|
|
"scopes",
|
|
]) {
|
|
assert.equal(names.has(expected), true, `${expected} should exist`);
|
|
}
|
|
assert.deepEqual(
|
|
db.prepare("SELECT version, name FROM _omniroute_migrations WHERE version = ?").get("032"),
|
|
{ version: "032", name: "apikey_lifecycle" }
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"runMigrations applies api key lifecycle hardening by version even if filename suffix changes",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE api_keys (
|
|
id TEXT PRIMARY KEY,
|
|
key TEXT NOT NULL
|
|
);
|
|
`);
|
|
|
|
const appliedCount = withMockedMigrationFs(
|
|
{
|
|
"032_renamed_lifecycle_patch.sql": "ALTER TABLE api_keys ADD COLUMN should_not_run TEXT;",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
assert.equal(appliedCount, 1);
|
|
const columns = db.prepare("PRAGMA table_info(api_keys)").all() as Array<{ name: string }>;
|
|
const names = new Set(columns.map((column) => column.name));
|
|
assert.equal(names.has("revoked_at"), true);
|
|
assert.equal(names.has("expires_at"), true);
|
|
assert.equal(names.has("should_not_run"), false);
|
|
assert.deepEqual(
|
|
db.prepare("SELECT version, name FROM _omniroute_migrations WHERE version = ?").get("032"),
|
|
{ version: "032", name: "renamed_lifecycle_patch" }
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test("getMigrationStatus reports applied and pending migrations", serial, async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
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",
|
|
"first"
|
|
);
|
|
|
|
const status = withMockedMigrationFs(
|
|
{
|
|
"001_first.sql": "CREATE TABLE status_first (id INTEGER);",
|
|
"002_second.sql": "CREATE TABLE status_second (id INTEGER);",
|
|
"003_third.sql": "CREATE TABLE status_third (id INTEGER);",
|
|
},
|
|
() => runner.getMigrationStatus(db)
|
|
);
|
|
|
|
assert.deepEqual(
|
|
status.applied.map((row) => row.version),
|
|
["001"]
|
|
);
|
|
assert.deepEqual(
|
|
status.pending.map((row) => row.version),
|
|
["002", "003"]
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
});
|
|
|
|
test(
|
|
"failed migrations roll back their transaction and do not record the version",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
assert.throws(
|
|
() =>
|
|
withMockedMigrationFs(
|
|
{
|
|
"001_ok.sql": "CREATE TABLE rollback_ok (id INTEGER);",
|
|
"002_broken.sql":
|
|
"CREATE TABLE rollback_broken (id INTEGER); INSERT INTO missing_table VALUES (1);",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
),
|
|
/missing_table/i
|
|
);
|
|
|
|
assert.ok(
|
|
db
|
|
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
.get("rollback_ok")
|
|
);
|
|
assert.equal(
|
|
db
|
|
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
.get("rollback_broken"),
|
|
undefined
|
|
);
|
|
assert.equal(
|
|
(
|
|
db
|
|
.prepare("SELECT COUNT(*) AS count FROM _omniroute_migrations WHERE version = ?")
|
|
.get("002") as any
|
|
).count,
|
|
0
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test("missing or empty migration directories are treated as a no-op", serial, async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const missingDb = createDb();
|
|
const emptyDb = createDb();
|
|
|
|
try {
|
|
assert.equal(
|
|
withMockedMigrationFs(null, () => runner.runMigrations(missingDb)),
|
|
0
|
|
);
|
|
assert.equal(
|
|
withMockedMigrationFs({}, () => runner.runMigrations(emptyDb)),
|
|
0
|
|
);
|
|
assert.deepEqual(
|
|
withMockedMigrationFs({}, () => runner.getMigrationStatus(emptyDb)),
|
|
{
|
|
applied: [],
|
|
pending: [],
|
|
}
|
|
);
|
|
} finally {
|
|
missingDb.close();
|
|
emptyDb.close();
|
|
}
|
|
});
|
|
|
|
test("invalid file names are ignored while valid migrations still run", serial, async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
const count = withMockedMigrationFs(
|
|
{
|
|
"README.md": "# ignored",
|
|
"not-a-migration.sql": "CREATE TABLE should_not_exist (id INTEGER);",
|
|
"003_valid.sql": "CREATE TABLE valid_migration (id INTEGER);",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
assert.equal(count, 1);
|
|
assert.deepEqual(
|
|
db.prepare("SELECT version, name FROM _omniroute_migrations ORDER BY version").all(),
|
|
[{ version: "003", name: "valid" }]
|
|
);
|
|
assert.equal(
|
|
db
|
|
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
.get("should_not_exist"),
|
|
undefined
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
});
|
|
|
|
test(
|
|
"new migrations are detected on subsequent runs without replaying old ones",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
withMockedMigrationFs(
|
|
{
|
|
"001_first.sql": "CREATE TABLE rerun_first (id INTEGER);",
|
|
"002_second.sql": "CREATE TABLE rerun_second (id INTEGER);",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
const count = withMockedMigrationFs(
|
|
{
|
|
"001_first.sql": "CREATE TABLE rerun_first (id INTEGER);",
|
|
"002_second.sql": "CREATE TABLE rerun_second (id INTEGER);",
|
|
"003_third.sql": "CREATE TABLE rerun_third (id INTEGER);",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
assert.equal(count, 1);
|
|
assert.deepEqual(
|
|
db.prepare("SELECT version FROM _omniroute_migrations ORDER BY version").all(),
|
|
[{ version: "001" }, { version: "002" }, { version: "003" }]
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"unknown rows in the migration table do not block pending real migrations",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
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(
|
|
"999",
|
|
"ghost"
|
|
);
|
|
|
|
const count = withMockedMigrationFs(
|
|
{
|
|
"001_first.sql": "CREATE TABLE recover_first (id INTEGER);",
|
|
"002_second.sql": "CREATE TABLE recover_second (id INTEGER);",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
assert.equal(count, 2);
|
|
assert.deepEqual(
|
|
db.prepare("SELECT version FROM _omniroute_migrations ORDER BY version").all(),
|
|
[{ version: "001" }, { version: "002" }, { version: "999" }]
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"memory FTS migrations upgrade existing UUID memories without datatype mismatches",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE _omniroute_migrations (
|
|
version TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE memories (
|
|
id TEXT PRIMARY KEY,
|
|
api_key_id TEXT NOT NULL,
|
|
session_id TEXT,
|
|
type TEXT NOT NULL,
|
|
key TEXT,
|
|
content TEXT NOT NULL,
|
|
metadata TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
expires_at TEXT
|
|
);
|
|
`);
|
|
db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run(
|
|
"021",
|
|
"combo_call_log_targets"
|
|
);
|
|
db.prepare(
|
|
"INSERT INTO memories (id, api_key_id, session_id, type, key, content, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
).run(
|
|
"550e8400-e29b-41d4-a716-446655440000",
|
|
"key-1",
|
|
"session-1",
|
|
"factual",
|
|
"topic",
|
|
"memory content",
|
|
"{}"
|
|
);
|
|
|
|
const count = withMockedMigrationFs(
|
|
{
|
|
"022_add_memory_fts5.sql": REAL_022_ADD_MEMORY_FTS5_SQL,
|
|
"023_fix_memory_fts_uuid.sql": REAL_023_FIX_MEMORY_FTS_UUID_SQL,
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
assert.equal(count, 2);
|
|
assert.deepEqual(
|
|
db.prepare("SELECT version FROM _omniroute_migrations ORDER BY version").all(),
|
|
[{ version: "021" }, { version: "022" }, { version: "023" }]
|
|
);
|
|
assert.deepEqual(db.prepare("SELECT memory_id, content FROM memories").get(), {
|
|
memory_id: 1,
|
|
content: "memory content",
|
|
});
|
|
assert.deepEqual(db.prepare("SELECT rowid, content, key FROM memory_fts").get(), {
|
|
rowid: 1,
|
|
content: "memory content",
|
|
key: "topic",
|
|
});
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"runMigrations defers optional FTS migrations when the current driver lacks fts5 support",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createSqlJsLikeDb();
|
|
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE _omniroute_migrations (
|
|
version TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE memories (
|
|
id TEXT PRIMARY KEY,
|
|
api_key_id TEXT NOT NULL,
|
|
session_id TEXT,
|
|
type TEXT NOT NULL,
|
|
key TEXT,
|
|
content TEXT NOT NULL,
|
|
metadata TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
expires_at TEXT
|
|
);
|
|
`);
|
|
db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run(
|
|
"021",
|
|
"combo_call_log_targets"
|
|
);
|
|
|
|
const count = withMockedMigrationFs(
|
|
{
|
|
"022_add_memory_fts5.sql": REAL_022_ADD_MEMORY_FTS5_SQL,
|
|
"023_fix_memory_fts_uuid.sql": REAL_023_FIX_MEMORY_FTS_UUID_SQL,
|
|
"024_after_fts.sql": "CREATE TABLE after_fts (id INTEGER PRIMARY KEY);",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
assert.equal(count, 1);
|
|
assert.deepEqual(
|
|
db.prepare("SELECT version FROM _omniroute_migrations ORDER BY version").all(),
|
|
[{ version: "021" }, { version: "024" }]
|
|
);
|
|
assert.equal(
|
|
db
|
|
.prepare("SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
.get("memory_fts").count,
|
|
0
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"runMigrations allows a large pending set when the physical schema still looks like 001",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
createInitialSchemaTables(db);
|
|
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"
|
|
);
|
|
|
|
const count = withNonTestEnvironment(() =>
|
|
withMockedMigrationFs(buildMockMigrationFiles(1, 7, "legacy_allow"), () =>
|
|
runner.runMigrations(db)
|
|
)
|
|
);
|
|
|
|
assert.equal(count, 6);
|
|
assert.deepEqual(
|
|
db.prepare("SELECT version FROM _omniroute_migrations ORDER BY version").all(),
|
|
[
|
|
{ version: "001" },
|
|
{ version: "002" },
|
|
{ version: "003" },
|
|
{ version: "004" },
|
|
{ version: "005" },
|
|
{ version: "006" },
|
|
{ version: "007" },
|
|
]
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"runMigrations aborts large pending sets when the physical schema proves a newer baseline",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
createInitialSchemaTables(db);
|
|
db.exec(`
|
|
CREATE TABLE request_detail_logs (id TEXT PRIMARY KEY);
|
|
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"
|
|
);
|
|
|
|
assert.throws(
|
|
() =>
|
|
withNonTestEnvironment(() =>
|
|
withMockedMigrationFs(buildMockMigrationFiles(1, 60, "legacy_abort"), () =>
|
|
runner.runMigrations(db)
|
|
)
|
|
),
|
|
/Physical schema already shows 006/i
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"reconcileRenumberedMigrations resolves compression_settings 028→034 upgrade path",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE _omniroute_migrations (
|
|
version TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
`);
|
|
// Simulate a DB where compression_settings was applied at version 028
|
|
db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run(
|
|
"028",
|
|
"compression_settings"
|
|
);
|
|
|
|
// Disk has compression_settings at 034 (current location) and create_files_and_batches at 028
|
|
const consoleErrors: string[] = [];
|
|
const originalError = console.error;
|
|
console.error = (...args: any[]) => {
|
|
consoleErrors.push(args.map(String).join(" "));
|
|
};
|
|
|
|
try {
|
|
withMockedMigrationFs(
|
|
{
|
|
"028_create_files_and_batches.sql":
|
|
"CREATE TABLE IF NOT EXISTS files (id TEXT PRIMARY KEY);",
|
|
"034_compression_settings.sql":
|
|
"CREATE TABLE IF NOT EXISTS compression_settings_table (id TEXT PRIMARY KEY);",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
// The reconcile should have moved 028/compression_settings → 034/compression_settings
|
|
const row028 = db
|
|
.prepare("SELECT version, name FROM _omniroute_migrations WHERE version = ?")
|
|
.get("028") as { version: string; name: string } | undefined;
|
|
const row034 = db
|
|
.prepare("SELECT version, name FROM _omniroute_migrations WHERE version = ?")
|
|
.get("034") as { version: string; name: string } | undefined;
|
|
|
|
// After reconciliation, 028 should be free (or have create_files_and_batches)
|
|
// and 034 should have compression_settings
|
|
assert.equal(row034?.name, "compression_settings");
|
|
|
|
// No CRITICAL renumbering warning for version 028
|
|
const renumberingWarnings = consoleErrors.filter(
|
|
(e) => e.includes("CRITICAL") && e.includes("renumbered")
|
|
);
|
|
assert.equal(
|
|
renumberingWarnings.length,
|
|
0,
|
|
`Expected no renumbering warnings, got: ${renumberingWarnings.join("; ")}`
|
|
);
|
|
} finally {
|
|
console.error = originalError;
|
|
}
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"reconcileRenumberedMigrations resolves compression_analytics 032→038 upgrade path",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE _omniroute_migrations (
|
|
version TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
`);
|
|
// Simulate DB where compression_analytics was applied at version 032
|
|
db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run(
|
|
"032",
|
|
"compression_analytics"
|
|
);
|
|
|
|
const consoleErrors: string[] = [];
|
|
const originalError = console.error;
|
|
console.error = (...args: any[]) => {
|
|
consoleErrors.push(args.map(String).join(" "));
|
|
};
|
|
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE api_keys (
|
|
id TEXT PRIMARY KEY,
|
|
key TEXT NOT NULL
|
|
);
|
|
`);
|
|
withMockedMigrationFs(
|
|
{
|
|
"032_apikey_lifecycle.sql": "ALTER TABLE api_keys ADD COLUMN revoked_at TEXT;",
|
|
"038_compression_analytics.sql":
|
|
"CREATE TABLE IF NOT EXISTS compression_analytics (id TEXT PRIMARY KEY);",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
const row038 = db
|
|
.prepare("SELECT version, name FROM _omniroute_migrations WHERE version = ?")
|
|
.get("038") as { version: string; name: string } | undefined;
|
|
|
|
assert.equal(row038?.name, "compression_analytics");
|
|
|
|
const renumberingWarnings = consoleErrors.filter(
|
|
(e) => e.includes("CRITICAL") && e.includes("renumbered")
|
|
);
|
|
assert.equal(
|
|
renumberingWarnings.length,
|
|
0,
|
|
`Expected no renumbering warnings, got: ${renumberingWarnings.join("; ")}`
|
|
);
|
|
} finally {
|
|
console.error = originalError;
|
|
}
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"reconcileRenumberedMigrations resolves compression_cache_stats 033→039 upgrade path",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE _omniroute_migrations (
|
|
version TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
`);
|
|
// Simulate DB where compression_cache_stats was applied at version 033
|
|
db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run(
|
|
"033",
|
|
"compression_cache_stats"
|
|
);
|
|
|
|
const consoleErrors: string[] = [];
|
|
const originalError = console.error;
|
|
console.error = (...args: any[]) => {
|
|
consoleErrors.push(args.map(String).join(" "));
|
|
};
|
|
|
|
try {
|
|
withMockedMigrationFs(
|
|
{
|
|
"033_create_reasoning_cache.sql":
|
|
"CREATE TABLE IF NOT EXISTS reasoning_cache (id TEXT PRIMARY KEY);",
|
|
"039_compression_cache_stats.sql":
|
|
"CREATE TABLE IF NOT EXISTS compression_cache_stats_table (id TEXT PRIMARY KEY);",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
const row039 = db
|
|
.prepare("SELECT version, name FROM _omniroute_migrations WHERE version = ?")
|
|
.get("039") as { version: string; name: string } | undefined;
|
|
|
|
assert.equal(row039?.name, "compression_cache_stats");
|
|
|
|
const renumberingWarnings = consoleErrors.filter(
|
|
(e) => e.includes("CRITICAL") && e.includes("renumbered")
|
|
);
|
|
assert.equal(
|
|
renumberingWarnings.length,
|
|
0,
|
|
`Expected no renumbering warnings, got: ${renumberingWarnings.join("; ")}`
|
|
);
|
|
} finally {
|
|
console.error = originalError;
|
|
}
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"runMigrations ignores superseded 041 session affinity duplicate when 050 exists",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
const count = withMockedMigrationFs(
|
|
{
|
|
"038_compression_analytics.sql": `
|
|
CREATE TABLE compression_analytics (
|
|
id TEXT PRIMARY KEY,
|
|
request_id TEXT
|
|
);
|
|
`,
|
|
"041_compression_receipts.sql": "-- handled by migrationRunner",
|
|
"041_session_account_affinity.sql": `
|
|
CREATE TABLE duplicate_041_session_account_affinity (id TEXT PRIMARY KEY);
|
|
`,
|
|
"050_session_account_affinity.sql": `
|
|
CREATE TABLE IF NOT EXISTS session_account_affinity (
|
|
session_key TEXT NOT NULL,
|
|
provider TEXT NOT NULL,
|
|
connection_id TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
last_seen_at INTEGER NOT NULL,
|
|
PRIMARY KEY (session_key, provider)
|
|
);
|
|
`,
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
assert.equal(count, 3);
|
|
assert.equal(
|
|
db.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?").get("041")?.name,
|
|
"compression_receipts"
|
|
);
|
|
assert.equal(
|
|
db.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?").get("050")?.name,
|
|
"session_account_affinity"
|
|
);
|
|
assert.deepEqual(
|
|
db
|
|
.prepare(
|
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (?, ?) ORDER BY name"
|
|
)
|
|
.all("duplicate_041_session_account_affinity", "session_account_affinity"),
|
|
[{ name: "session_account_affinity" }]
|
|
);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"reconcileRenumberedMigrations moves legacy 041 session affinity marker to 050",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE _omniroute_migrations (
|
|
version TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE compression_analytics (
|
|
id TEXT PRIMARY KEY,
|
|
request_id TEXT
|
|
);
|
|
`);
|
|
db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run(
|
|
"041",
|
|
"session_account_affinity"
|
|
);
|
|
|
|
const consoleErrors: string[] = [];
|
|
const originalError = console.error;
|
|
console.error = (...args: any[]) => {
|
|
consoleErrors.push(args.map(String).join(" "));
|
|
};
|
|
|
|
try {
|
|
const count = withMockedMigrationFs(
|
|
{
|
|
"041_compression_receipts.sql": "-- handled by migrationRunner",
|
|
"041_session_account_affinity.sql": `
|
|
CREATE TABLE duplicate_041_session_account_affinity (id TEXT PRIMARY KEY);
|
|
`,
|
|
"050_session_account_affinity.sql": `
|
|
CREATE TABLE IF NOT EXISTS session_account_affinity (
|
|
session_key TEXT NOT NULL,
|
|
provider TEXT NOT NULL,
|
|
connection_id TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
last_seen_at INTEGER NOT NULL,
|
|
PRIMARY KEY (session_key, provider)
|
|
);
|
|
`,
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
assert.equal(count, 1);
|
|
assert.equal(
|
|
db.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?").get("041")?.name,
|
|
"compression_receipts"
|
|
);
|
|
assert.equal(
|
|
db.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?").get("050")?.name,
|
|
"session_account_affinity"
|
|
);
|
|
|
|
const renumberingWarnings = consoleErrors.filter(
|
|
(e) => e.includes("CRITICAL") && e.includes("renumbered")
|
|
);
|
|
assert.equal(
|
|
renumberingWarnings.length,
|
|
0,
|
|
`Expected no renumbering warnings, got: ${renumberingWarnings.join("; ")}`
|
|
);
|
|
} finally {
|
|
console.error = originalError;
|
|
}
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"reconcileRenumberedMigrations moves legacy 056 manifest routing marker to 059",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
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(
|
|
"056",
|
|
"manifest_routing"
|
|
);
|
|
|
|
const consoleErrors: string[] = [];
|
|
const originalError = console.error;
|
|
console.error = (...args: any[]) => {
|
|
consoleErrors.push(args.map(String).join(" "));
|
|
};
|
|
|
|
try {
|
|
withMockedMigrationFs(
|
|
{
|
|
"056_mcp_accessibility_compression.sql":
|
|
"CREATE TABLE IF NOT EXISTS mcp_accessibility_compression (id TEXT PRIMARY KEY);",
|
|
"059_manifest_routing.sql":
|
|
"CREATE TABLE IF NOT EXISTS manifest_routing (id TEXT PRIMARY KEY);",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
assert.equal(
|
|
db.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?").get("056")?.name,
|
|
"mcp_accessibility_compression"
|
|
);
|
|
assert.equal(
|
|
db.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?").get("059")?.name,
|
|
"manifest_routing"
|
|
);
|
|
|
|
const renumberingWarnings = consoleErrors.filter(
|
|
(e) => e.includes("CRITICAL") && e.includes("renumbered")
|
|
);
|
|
assert.equal(
|
|
renumberingWarnings.length,
|
|
0,
|
|
`Expected no renumbering warnings, got: ${renumberingWarnings.join("; ")}`
|
|
);
|
|
} finally {
|
|
console.error = originalError;
|
|
}
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"reconcileRenumberedMigrations moves legacy 051 usage history service tier marker to 054",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
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(
|
|
"051",
|
|
"usage_history_service_tier"
|
|
);
|
|
db.exec(`
|
|
CREATE TABLE usage_history (
|
|
id TEXT PRIMARY KEY,
|
|
api_key_id TEXT,
|
|
timestamp TEXT
|
|
);
|
|
`);
|
|
|
|
const consoleErrors: string[] = [];
|
|
const originalError = console.error;
|
|
console.error = (...args: any[]) => {
|
|
consoleErrors.push(args.map(String).join(" "));
|
|
};
|
|
|
|
try {
|
|
withMockedMigrationFs(
|
|
{
|
|
"051_hot_path_db_indexes.sql":
|
|
"CREATE INDEX IF NOT EXISTS idx_usage_history_api_key_id_timestamp ON usage_history(api_key_id, timestamp);",
|
|
"054_usage_history_service_tier.sql":
|
|
"ALTER TABLE usage_history ADD COLUMN service_tier TEXT;",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
assert.equal(
|
|
db.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?").get("051")?.name,
|
|
"hot_path_db_indexes"
|
|
);
|
|
assert.equal(
|
|
db.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?").get("054")?.name,
|
|
"usage_history_service_tier"
|
|
);
|
|
|
|
const renumberingWarnings = consoleErrors.filter(
|
|
(e) => e.includes("CRITICAL") && e.includes("renumbered")
|
|
);
|
|
assert.equal(
|
|
renumberingWarnings.length,
|
|
0,
|
|
`Expected no renumbering warnings, got: ${renumberingWarnings.join("; ")}`
|
|
);
|
|
} finally {
|
|
console.error = originalError;
|
|
}
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"full upgrade simulation: renumbered migrations are reconciled without CRITICAL warnings",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE _omniroute_migrations (
|
|
version TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
`);
|
|
// Simulate a user's DB that has all 3 old migration entries
|
|
const oldMigrations = [
|
|
["027", "skill_mode_and_metadata"],
|
|
["028", "compression_settings"],
|
|
["029", "provider_connection_max_concurrent"],
|
|
["032", "compression_analytics"],
|
|
["033", "compression_cache_stats"],
|
|
["051", "usage_history_service_tier"],
|
|
["056", "manifest_routing"],
|
|
] as const;
|
|
for (const [v, n] of oldMigrations) {
|
|
db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run(v, n);
|
|
}
|
|
|
|
// Disk has the current migration file layout
|
|
const consoleErrors: string[] = [];
|
|
const originalError = console.error;
|
|
console.error = (...args: any[]) => {
|
|
consoleErrors.push(args.map(String).join(" "));
|
|
};
|
|
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE api_keys (
|
|
id TEXT PRIMARY KEY,
|
|
key TEXT NOT NULL
|
|
);
|
|
CREATE TABLE usage_history (
|
|
id TEXT PRIMARY KEY,
|
|
api_key_id TEXT,
|
|
timestamp TEXT
|
|
);
|
|
`);
|
|
withMockedMigrationFs(
|
|
{
|
|
"027_skill_mode_and_metadata.sql":
|
|
"CREATE TABLE IF NOT EXISTS skill_meta (id TEXT PRIMARY KEY);",
|
|
"028_create_files_and_batches.sql":
|
|
"CREATE TABLE IF NOT EXISTS files (id TEXT PRIMARY KEY);",
|
|
"029_provider_connection_max_concurrent.sql":
|
|
"ALTER TABLE provider_connections ADD COLUMN max_concurrent INTEGER;",
|
|
"032_apikey_lifecycle.sql": "ALTER TABLE api_keys ADD COLUMN revoked_at TEXT;",
|
|
"033_create_reasoning_cache.sql":
|
|
"CREATE TABLE IF NOT EXISTS reasoning_cache (id TEXT PRIMARY KEY);",
|
|
"034_compression_settings.sql":
|
|
"CREATE TABLE IF NOT EXISTS compression_settings_table (id TEXT PRIMARY KEY);",
|
|
"038_compression_analytics.sql":
|
|
"CREATE TABLE IF NOT EXISTS compression_analytics (id TEXT PRIMARY KEY);",
|
|
"039_compression_cache_stats.sql":
|
|
"CREATE TABLE IF NOT EXISTS compression_cache_stats_table (id TEXT PRIMARY KEY);",
|
|
"051_hot_path_db_indexes.sql":
|
|
"CREATE INDEX IF NOT EXISTS idx_usage_history_api_key_id_timestamp ON usage_history(api_key_id, timestamp);",
|
|
"054_usage_history_service_tier.sql":
|
|
"ALTER TABLE usage_history ADD COLUMN service_tier TEXT;",
|
|
"056_mcp_accessibility_compression.sql":
|
|
"CREATE TABLE IF NOT EXISTS mcp_accessibility_compression (id TEXT PRIMARY KEY);",
|
|
"059_manifest_routing.sql":
|
|
"CREATE TABLE IF NOT EXISTS manifest_routing (id TEXT PRIMARY KEY);",
|
|
},
|
|
() => runner.runMigrations(db)
|
|
);
|
|
|
|
// No CRITICAL renumbering warnings
|
|
const renumberingWarnings = consoleErrors.filter(
|
|
(e) => e.includes("CRITICAL") && e.includes("renumbered")
|
|
);
|
|
assert.equal(
|
|
renumberingWarnings.length,
|
|
0,
|
|
`Expected no renumbering warnings, got: ${renumberingWarnings.join("; ")}`
|
|
);
|
|
|
|
// Verify the reconciled entries
|
|
const row034 = db
|
|
.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?")
|
|
.get("034") as { name: string } | undefined;
|
|
const row038 = db
|
|
.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?")
|
|
.get("038") as { name: string } | undefined;
|
|
const row039 = db
|
|
.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?")
|
|
.get("039") as { name: string } | undefined;
|
|
const row051 = db
|
|
.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?")
|
|
.get("051") as { name: string } | undefined;
|
|
const row054 = db
|
|
.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?")
|
|
.get("054") as { name: string } | undefined;
|
|
const row056 = db
|
|
.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?")
|
|
.get("056") as { name: string } | undefined;
|
|
const row059 = db
|
|
.prepare("SELECT name FROM _omniroute_migrations WHERE version = ?")
|
|
.get("059") as { name: string } | undefined;
|
|
|
|
assert.equal(row034?.name, "compression_settings");
|
|
assert.equal(row038?.name, "compression_analytics");
|
|
assert.equal(row039?.name, "compression_cache_stats");
|
|
assert.equal(row051?.name, "hot_path_db_indexes");
|
|
assert.equal(row054?.name, "usage_history_service_tier");
|
|
assert.equal(row056?.name, "mcp_accessibility_compression");
|
|
assert.equal(row059?.name, "manifest_routing");
|
|
} finally {
|
|
console.error = originalError;
|
|
}
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
// ── #3416: OMNIROUTE_MAX_PENDING_MIGRATIONS env override ─────────────────────
|
|
// The mass-migration safety threshold must be overridable at runtime so a user
|
|
// restoring a backup can raise (or lower) the limit without code changes. The
|
|
// resolver reads the env var at CALL TIME inside runMigrations(), so these tests
|
|
// set/delete the env around the call and assert the abort message reflects the
|
|
// resolved threshold.
|
|
|
|
// Build an "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) {
|
|
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"
|
|
);
|
|
}
|
|
|
|
test(
|
|
"runMigrations aborts when OMNIROUTE_MAX_PENDING_MIGRATIONS lowers the threshold (#3416)",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
const original = process.env.OMNIROUTE_MAX_PENDING_MIGRATIONS;
|
|
|
|
try {
|
|
seedExistingDbWithoutPhysicalBaseline(db);
|
|
process.env.OMNIROUTE_MAX_PENDING_MIGRATIONS = "5";
|
|
|
|
// 1 applied (001) + files 001..011 → 10 actionable pending > threshold 5.
|
|
assert.throws(
|
|
() =>
|
|
withNonTestEnvironment(() =>
|
|
withMockedMigrationFs(buildMockMigrationFiles(1, 11, "lower_threshold"), () =>
|
|
runner.runMigrations(db)
|
|
)
|
|
),
|
|
/threshold is 5/i
|
|
);
|
|
} finally {
|
|
if (original === undefined) delete process.env.OMNIROUTE_MAX_PENDING_MIGRATIONS;
|
|
else process.env.OMNIROUTE_MAX_PENDING_MIGRATIONS = original;
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"runMigrations allows a large pending set when OMNIROUTE_MAX_PENDING_MIGRATIONS raises the threshold (#3416)",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const db = createDb();
|
|
const original = process.env.OMNIROUTE_MAX_PENDING_MIGRATIONS;
|
|
|
|
try {
|
|
seedExistingDbWithoutPhysicalBaseline(db);
|
|
process.env.OMNIROUTE_MAX_PENDING_MIGRATIONS = "500";
|
|
|
|
// 1 applied (001) + 60 plain pending files at versions 100..159 (chosen to
|
|
// avoid the special-cased migration versions 032/041/042). All 60 exceed the
|
|
// default 50 threshold but stay well under the raised 500 limit, so they apply.
|
|
const pendingFiles = {};
|
|
for (let v = 100; v < 160; v++) {
|
|
pendingFiles[`${v}_raise_threshold_${v}.sql`] =
|
|
`CREATE TABLE raise_threshold_${v} (id INTEGER);`;
|
|
}
|
|
|
|
const count = withNonTestEnvironment(() =>
|
|
withMockedMigrationFs(pendingFiles, () => runner.runMigrations(db))
|
|
);
|
|
|
|
assert.equal(count, 60);
|
|
} finally {
|
|
if (original === undefined) delete process.env.OMNIROUTE_MAX_PENDING_MIGRATIONS;
|
|
else process.env.OMNIROUTE_MAX_PENDING_MIGRATIONS = original;
|
|
db.close();
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"runMigrations keeps the default 50 threshold when OMNIROUTE_MAX_PENDING_MIGRATIONS is unset or invalid (#3416)",
|
|
serial,
|
|
async () => {
|
|
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
|
const original = process.env.OMNIROUTE_MAX_PENDING_MIGRATIONS;
|
|
|
|
try {
|
|
// Case 1: env unset → default 50 abort message.
|
|
delete process.env.OMNIROUTE_MAX_PENDING_MIGRATIONS;
|
|
const dbUnset = createDb();
|
|
try {
|
|
seedExistingDbWithoutPhysicalBaseline(dbUnset);
|
|
assert.throws(
|
|
() =>
|
|
withNonTestEnvironment(() =>
|
|
withMockedMigrationFs(buildMockMigrationFiles(1, 60, "default_unset"), () =>
|
|
runner.runMigrations(dbUnset)
|
|
)
|
|
),
|
|
/threshold is 50/i
|
|
);
|
|
} finally {
|
|
dbUnset.close();
|
|
}
|
|
|
|
// Case 2: invalid (non-numeric) → fall back to default 50.
|
|
process.env.OMNIROUTE_MAX_PENDING_MIGRATIONS = "abc";
|
|
const dbInvalid = createDb();
|
|
try {
|
|
seedExistingDbWithoutPhysicalBaseline(dbInvalid);
|
|
assert.throws(
|
|
() =>
|
|
withNonTestEnvironment(() =>
|
|
withMockedMigrationFs(buildMockMigrationFiles(1, 60, "default_invalid"), () =>
|
|
runner.runMigrations(dbInvalid)
|
|
)
|
|
),
|
|
/threshold is 50/i
|
|
);
|
|
} finally {
|
|
dbInvalid.close();
|
|
}
|
|
} finally {
|
|
if (original === undefined) delete process.env.OMNIROUTE_MAX_PENDING_MIGRATIONS;
|
|
else process.env.OMNIROUTE_MAX_PENDING_MIGRATIONS = original;
|
|
}
|
|
}
|
|
);
|