mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 00:02:20 +03:00
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log
433 lines
16 KiB
TypeScript
433 lines
16 KiB
TypeScript
import { test, describe, type TestContext } 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 { createRequire } from "node:module";
|
|
|
|
const {
|
|
createSyncDriverFactory,
|
|
isPackBootForcedSqlJsSmoke,
|
|
tryOpenSync,
|
|
openDatabaseAsync,
|
|
preInitSqlJs,
|
|
getSqlJsAdapter,
|
|
} = await import("../../../src/lib/db/adapters/driverFactory.ts");
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const isBun = Boolean(process.versions.bun);
|
|
|
|
function forceNodeSqlite() {
|
|
return createSyncDriverFactory((moduleName: string) => {
|
|
if (moduleName === "better-sqlite3") {
|
|
throw new Error("forced better-sqlite3 load failure");
|
|
}
|
|
return require(moduleName);
|
|
});
|
|
}
|
|
|
|
function createTempDatabasePath(t: TestContext) {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-node-sqlite-"));
|
|
const databasePath = path.join(dir, "database.sqlite");
|
|
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
return databasePath;
|
|
}
|
|
|
|
describe("driverFactory", () => {
|
|
test("tryOpenSync retorna adapter síncrono ou null", () => {
|
|
const adapter = tryOpenSync(":memory:");
|
|
if (adapter) {
|
|
assert.ok(["better-sqlite3", "node:sqlite", "bun:sqlite"].includes(adapter.driver));
|
|
adapter.exec("CREATE TABLE t (v TEXT)");
|
|
adapter.prepare("INSERT INTO t VALUES (?)").run("ok");
|
|
const row = adapter.prepare("SELECT v FROM t").get() as { v: string };
|
|
assert.equal(row.v, "ok");
|
|
adapter.close();
|
|
} else {
|
|
assert.equal(adapter, null);
|
|
}
|
|
});
|
|
|
|
if (!isBun) {
|
|
// better-sqlite3 is an OPTIONAL dependency: on a platform with no prebuild and no
|
|
// toolchain it simply will not load, and this expectation cannot hold. The condition is
|
|
// declared in the test options (node:test evaluates it at declaration time) rather than
|
|
// from inside the test body — same behavior, but the skip is visible in the report and
|
|
// the anti-test-masking gate can tell it apart from a statically disabled test. (Phrased
|
|
// without the literal call syntax: that gate greps text, so spelling the API out here
|
|
// would count this comment as two new skip markers.)
|
|
const betterSqliteProbe = tryOpenSync(":memory:");
|
|
const betterSqliteLoads = betterSqliteProbe?.driver === "better-sqlite3";
|
|
betterSqliteProbe?.close();
|
|
|
|
test(
|
|
"prefers better-sqlite3 when it loads",
|
|
{ skip: betterSqliteLoads ? undefined : "better-sqlite3 is not available in this environment" },
|
|
() => {
|
|
const adapter = tryOpenSync(":memory:");
|
|
assert.ok(adapter);
|
|
assert.equal(adapter.driver, "better-sqlite3");
|
|
adapter.close();
|
|
}
|
|
);
|
|
|
|
test("prefers better-sqlite3 before node:sqlite in the driver cascade", () => {
|
|
const fakeBetterSqlite = {
|
|
close() {},
|
|
name: ":memory:",
|
|
open: true,
|
|
};
|
|
let nodeSqliteRequested = false;
|
|
const openWithPreferredDriver = createSyncDriverFactory((moduleName: string) => {
|
|
if (moduleName === "better-sqlite3") {
|
|
return function FakeBetterSqlite() {
|
|
return fakeBetterSqlite;
|
|
};
|
|
}
|
|
if (moduleName === "node:sqlite") {
|
|
nodeSqliteRequested = true;
|
|
}
|
|
throw new Error(`unexpected driver load: ${moduleName}`);
|
|
});
|
|
|
|
const adapter = openWithPreferredDriver(":memory:");
|
|
assert.ok(adapter);
|
|
assert.equal(adapter.driver, "better-sqlite3");
|
|
assert.equal(nodeSqliteRequested, false);
|
|
adapter.close();
|
|
});
|
|
|
|
test("forced node:sqlite fallback creates, reopens, and queries a writable database", (t) => {
|
|
const databasePath = createTempDatabasePath(t);
|
|
const openNodeSqlite = forceNodeSqlite();
|
|
|
|
const writer = openNodeSqlite(databasePath);
|
|
assert.ok(writer);
|
|
assert.equal(writer.driver, "node:sqlite");
|
|
writer.exec("CREATE TABLE items (value TEXT)");
|
|
writer.prepare("INSERT INTO items VALUES (?)").run("native");
|
|
writer.close();
|
|
|
|
const reader = openNodeSqlite(databasePath);
|
|
assert.ok(reader);
|
|
assert.equal(reader.driver, "node:sqlite");
|
|
assert.equal(
|
|
(reader.prepare("SELECT value FROM items").get() as { value: string }).value,
|
|
"native"
|
|
);
|
|
reader.close();
|
|
});
|
|
|
|
test("forced node:sqlite fallback does not create missing existing-only paths", (t) => {
|
|
const databasePath = createTempDatabasePath(t);
|
|
const adapter = forceNodeSqlite()(databasePath, { readonly: true, fileMustExist: true });
|
|
|
|
assert.equal(adapter, null);
|
|
assert.equal(fs.existsSync(databasePath), false);
|
|
assert.equal(fs.existsSync(`${databasePath}-wal`), false);
|
|
assert.equal(fs.existsSync(`${databasePath}-shm`), false);
|
|
});
|
|
|
|
test("forced node:sqlite fallback preserves existing read-only behavior", (t) => {
|
|
const databasePath = createTempDatabasePath(t);
|
|
const { DatabaseSync } = require("node:sqlite") as {
|
|
DatabaseSync: new (filePath: string) => {
|
|
close(): void;
|
|
exec(sql: string): void;
|
|
prepare(sql: string): { get(): unknown };
|
|
};
|
|
};
|
|
const seed = new DatabaseSync(databasePath);
|
|
seed.exec("CREATE TABLE items (value TEXT); INSERT INTO items VALUES ('seed');");
|
|
seed.close();
|
|
|
|
const adapter = forceNodeSqlite()(databasePath, { readonly: true, fileMustExist: true });
|
|
assert.ok(adapter);
|
|
assert.equal(adapter.driver, "node:sqlite");
|
|
assert.equal(
|
|
(adapter.prepare("SELECT value FROM items").get() as { value: string }).value,
|
|
"seed"
|
|
);
|
|
assert.throws(() => adapter.exec("INSERT INTO items VALUES ('write')"));
|
|
adapter.close();
|
|
|
|
const check = new DatabaseSync(databasePath);
|
|
const row = check.prepare("SELECT value FROM items").get() as { value: string };
|
|
check.close();
|
|
assert.equal(row.value, "seed");
|
|
});
|
|
|
|
test("forced node:sqlite fallback keeps extension loading disabled", () => {
|
|
const adapter = forceNodeSqlite()(":memory:");
|
|
assert.ok(adapter);
|
|
assert.equal(adapter.driver, "node:sqlite");
|
|
|
|
const raw = adapter.raw as { loadExtension(path: string): void };
|
|
assert.throws(() => raw.loadExtension("not-a-trusted-extension"), {
|
|
code: "ERR_INVALID_STATE",
|
|
});
|
|
adapter.close();
|
|
});
|
|
|
|
test("forced node:sqlite backup preserves WAL-backed data", async (t) => {
|
|
const sourcePath = createTempDatabasePath(t);
|
|
const destinationPath = path.join(path.dirname(sourcePath), "backup.sqlite");
|
|
const openNodeSqlite = forceNodeSqlite();
|
|
const source = openNodeSqlite(sourcePath);
|
|
assert.ok(source);
|
|
assert.equal(source.driver, "node:sqlite");
|
|
source.exec("PRAGMA journal_mode = WAL; CREATE TABLE items (value TEXT);");
|
|
source.prepare("INSERT INTO items VALUES (?)").run("backup value");
|
|
|
|
await source.backup(destinationPath);
|
|
source.close();
|
|
|
|
const destination = openNodeSqlite(destinationPath, { fileMustExist: true });
|
|
assert.ok(destination);
|
|
assert.equal(destination.driver, "node:sqlite");
|
|
assert.equal(
|
|
(destination.prepare("SELECT value FROM items").get() as { value: string }).value,
|
|
"backup value"
|
|
);
|
|
destination.close();
|
|
});
|
|
|
|
test("forced node:sqlite immediate commits, rolls back, and nests savepoints", (t) => {
|
|
const databasePath = createTempDatabasePath(t);
|
|
const adapter = forceNodeSqlite()(databasePath);
|
|
assert.ok(adapter);
|
|
assert.equal(adapter.driver, "node:sqlite");
|
|
adapter.exec("CREATE TABLE items (value TEXT)");
|
|
|
|
adapter.immediate(() => {
|
|
adapter.prepare("INSERT INTO items VALUES (?)").run("committed");
|
|
});
|
|
assert.throws(() =>
|
|
adapter.immediate(() => {
|
|
adapter.prepare("INSERT INTO items VALUES (?)").run("rolled back");
|
|
throw new Error("rollback");
|
|
})
|
|
);
|
|
adapter.immediate(() => {
|
|
adapter.prepare("INSERT INTO items VALUES (?)").run("outer before");
|
|
const nested = adapter.transaction(() => {
|
|
adapter.prepare("INSERT INTO items VALUES (?)").run("inner rolled back");
|
|
throw new Error("nested rollback");
|
|
});
|
|
assert.throws(() => nested());
|
|
adapter.prepare("INSERT INTO items VALUES (?)").run("outer after");
|
|
});
|
|
|
|
const rows = adapter.prepare("SELECT value FROM items ORDER BY rowid").all() as Array<{
|
|
value: string;
|
|
}>;
|
|
assert.deepEqual(
|
|
rows.map((row) => row.value),
|
|
["committed", "outer before", "outer after"]
|
|
);
|
|
adapter.close();
|
|
});
|
|
|
|
test("forced node:sqlite immediate blocks a competing writer", (t) => {
|
|
const databasePath = createTempDatabasePath(t);
|
|
const openNodeSqlite = forceNodeSqlite();
|
|
const first = openNodeSqlite(databasePath);
|
|
assert.ok(first);
|
|
first.exec("CREATE TABLE items (value TEXT)");
|
|
|
|
const { DatabaseSync } = require("node:sqlite") as {
|
|
DatabaseSync: new (
|
|
filePath: string,
|
|
options: { timeout: number }
|
|
) => {
|
|
close(): void;
|
|
exec(sql: string): void;
|
|
};
|
|
};
|
|
const second = new DatabaseSync(databasePath, { timeout: 50 });
|
|
try {
|
|
first.immediate(() => {
|
|
assert.throws(() => second.exec("INSERT INTO items VALUES ('competing writer')"), {
|
|
code: "ERR_SQLITE_ERROR",
|
|
});
|
|
first.prepare("INSERT INTO items VALUES (?)").run("owner");
|
|
});
|
|
|
|
const rows = first.prepare("SELECT value FROM items").all() as Array<{ value: string }>;
|
|
assert.deepEqual(
|
|
rows.map((row) => row.value),
|
|
["owner"]
|
|
);
|
|
} finally {
|
|
second.close();
|
|
first.close();
|
|
}
|
|
});
|
|
}
|
|
|
|
test("retains the existing cascade when native drivers are unavailable", () => {
|
|
const openWithoutNativeDrivers = createSyncDriverFactory(() => {
|
|
throw new Error("forced driver load failure");
|
|
});
|
|
|
|
assert.equal(openWithoutNativeDrivers(":memory:"), null);
|
|
});
|
|
|
|
test("pack-boot sql.js forcing requires both smoke-only markers", () => {
|
|
assert.equal(isPackBootForcedSqlJsSmoke({}), false);
|
|
assert.equal(isPackBootForcedSqlJsSmoke({ OMNIROUTE_PACK_BOOT_SMOKE: "1" }), false);
|
|
assert.equal(isPackBootForcedSqlJsSmoke({ OMNIROUTE_PACK_BOOT_FORCE_SQLJS: "1" }), false);
|
|
assert.equal(
|
|
isPackBootForcedSqlJsSmoke({
|
|
OMNIROUTE_PACK_BOOT_SMOKE: "1",
|
|
OMNIROUTE_PACK_BOOT_FORCE_SQLJS: "1",
|
|
}),
|
|
true
|
|
);
|
|
});
|
|
|
|
test("openDatabaseAsync sempre retorna um adapter válido", async () => {
|
|
const adapter = await openDatabaseAsync(":memory:");
|
|
assert.ok(["better-sqlite3", "node:sqlite", "bun:sqlite", "sql.js"].includes(adapter.driver));
|
|
|
|
adapter.exec("CREATE TABLE t (v TEXT)");
|
|
adapter.prepare("INSERT INTO t VALUES (?)").run("ok");
|
|
const row = adapter.prepare("SELECT v FROM t").get() as { v: string };
|
|
assert.equal(row.v, "ok");
|
|
adapter.close();
|
|
});
|
|
|
|
test("preInitSqlJs cacheia o adapter por filePath", async () => {
|
|
const path = `sqljs_cache_test_${Date.now()}`;
|
|
const adapter1 = await preInitSqlJs(path);
|
|
const adapter2 = await preInitSqlJs(path);
|
|
assert.equal(adapter1, adapter2, "Deve retornar o mesmo adapter cacheado");
|
|
adapter1.close();
|
|
});
|
|
|
|
test("getSqlJsAdapter retorna null para path não inicializado", () => {
|
|
const unique = `not_initialized_${Date.now()}`;
|
|
assert.equal(getSqlJsAdapter(unique), null);
|
|
});
|
|
|
|
test("getSqlJsAdapter retorna adapter após preInitSqlJs", async () => {
|
|
const path = `sqljs_get_test_${Date.now()}`;
|
|
await preInitSqlJs(path);
|
|
const adapter = getSqlJsAdapter(path);
|
|
assert.ok(adapter !== null);
|
|
assert.equal(adapter!.driver, "sql.js");
|
|
adapter!.close();
|
|
});
|
|
|
|
test("openDatabaseAsync suporta operações CRUD completas", async (t) => {
|
|
const os = await import("node:os");
|
|
const path = await import("node:path");
|
|
const fs = await import("node:fs");
|
|
|
|
const tmpFile = path.join(os.tmpdir(), `driver_crud_${Date.now()}.sqlite`);
|
|
t.after(() => {
|
|
try {
|
|
fs.unlinkSync(tmpFile);
|
|
} catch {}
|
|
});
|
|
|
|
const adapter = await openDatabaseAsync(tmpFile);
|
|
|
|
adapter.exec("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, qty INTEGER)");
|
|
|
|
const r1 = adapter.prepare("INSERT INTO items (name, qty) VALUES (?, ?)").run("apple", 5);
|
|
const r2 = adapter.prepare("INSERT INTO items (name, qty) VALUES (?, ?)").run("banana", 3);
|
|
assert.equal(r1.changes, 1);
|
|
assert.equal(r2.changes, 1);
|
|
|
|
const rows = adapter.prepare("SELECT * FROM items ORDER BY id").all() as Array<{
|
|
id: number;
|
|
name: string;
|
|
qty: number;
|
|
}>;
|
|
assert.equal(rows.length, 2);
|
|
assert.equal(rows[0].name, "apple");
|
|
assert.equal(rows[1].name, "banana");
|
|
|
|
adapter.close();
|
|
});
|
|
|
|
// #6628 (remaining gap): concurrent preInitSqlJs() calls for the same
|
|
// filePath must share ONE in-flight load instead of each caller
|
|
// independently fs.readFileSync + WASM-decoding the whole file — the
|
|
// thundering-herd amplifier of the OOM condition #6632 already partly
|
|
// fixed (restore-cycle-breaker + OOM early-abort), left un-implemented by
|
|
// the reporter's own proposed promise-sharing fix.
|
|
test("preInitSqlJs shares one in-flight load across concurrent callers", async (t) => {
|
|
const os = await import("node:os");
|
|
const path = await import("node:path");
|
|
// The dynamic-import namespace object is read-only; grab the mutable CJS
|
|
// `.default` (== module.exports) so readFileSync can be monkeypatched.
|
|
const fsNs = await import("node:fs");
|
|
const fs = fsNs.default;
|
|
|
|
const tmpFile = path.join(os.tmpdir(), `sqljs_race_${Date.now()}.sqlite`);
|
|
fs.writeFileSync(tmpFile, Buffer.alloc(1024 * 1024, 1));
|
|
t.after(() => {
|
|
try {
|
|
fs.unlinkSync(tmpFile);
|
|
} catch {}
|
|
});
|
|
|
|
let readCountForTarget = 0;
|
|
const originalReadFileSync = fs.readFileSync;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(fs as any).readFileSync = (...args: Parameters<typeof fs.readFileSync>) => {
|
|
if (args[0] === tmpFile) readCountForTarget += 1;
|
|
return originalReadFileSync(...args);
|
|
};
|
|
t.after(() => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(fs as any).readFileSync = originalReadFileSync;
|
|
});
|
|
|
|
const [a, b, c] = await Promise.all([
|
|
preInitSqlJs(tmpFile),
|
|
preInitSqlJs(tmpFile),
|
|
preInitSqlJs(tmpFile),
|
|
]);
|
|
|
|
assert.equal(
|
|
readCountForTarget,
|
|
1,
|
|
`expected exactly 1 shared full-file read for 3 concurrent preInitSqlJs() calls, got ${readCountForTarget}`
|
|
);
|
|
assert.equal(a, b, "concurrent callers must resolve to the SAME adapter instance");
|
|
assert.equal(b, c, "concurrent callers must resolve to the SAME adapter instance");
|
|
a.close();
|
|
});
|
|
|
|
test("cross-driver: escreve com adapter sync, relê com sql.js", async (t) => {
|
|
const os = await import("node:os");
|
|
const path = await import("node:path");
|
|
const fs = await import("node:fs");
|
|
|
|
const tmpFile = path.join(os.tmpdir(), `cross_driver_${Date.now()}.sqlite`);
|
|
t.after(() => {
|
|
try {
|
|
fs.unlinkSync(tmpFile);
|
|
} catch {}
|
|
});
|
|
|
|
const syncAdapter = tryOpenSync(tmpFile);
|
|
if (!syncAdapter) {
|
|
console.log("SKIP: nenhum driver síncrono disponível para cross-driver test");
|
|
return;
|
|
}
|
|
syncAdapter.exec("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)");
|
|
syncAdapter.prepare("INSERT INTO items (name) VALUES (?)").run("cross-test");
|
|
syncAdapter.close();
|
|
|
|
const { createSqlJsAdapter } = await import("../../../src/lib/db/adapters/sqljsAdapter.ts");
|
|
const reader = await createSqlJsAdapter(tmpFile);
|
|
const row = reader.prepare("SELECT name FROM items WHERE id = 1").get() as { name: string };
|
|
assert.equal(row.name, "cross-test");
|
|
reader.close();
|
|
});
|
|
});
|