mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-20 22:02:19 +03:00
Merged after a maintainer rework that kept every one of @HouMinXi's commits intact. **What the rework added on top of the contribution:** the new DB health-check behaviour is gated behind a default-off feature flag (`src/shared/constants/featureFlagDefinitions.ts`, `defaultValue: "false"`), documented in `docs/reference/FEATURE_FLAGS.md` with the description key carried into all 66 locales, so the release default is unchanged and the new bounds only apply when an operator opts in. The optional-FTS5 migration set was reconciled by hand with the "180" entry that landed meanwhile (`src/lib/db/migrationRunner/constants.ts`). **Carried from your rebased head:** the `/api/db/health` local-only classification in `src/server/authz/routeGuard.ts` plus its `routeGuard` assertion — `runManagedDbHealthCheck()` forks native diagnostics into a child process, so Hard Rules #15/#17 apply. Re-verified here: 37 pass / 0 fail. Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run. Thank you for the depth of this one — the resource-bounds suite and the sql.js startup/backup coverage are the kind of tests that keep a database layer honest.
212 lines
8.3 KiB
TypeScript
212 lines
8.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { createSqlJsAdapter } from "../../src/lib/db/adapters/sqljsAdapter.ts";
|
|
import { setDbInstance } from "../../src/lib/db/singleton.ts";
|
|
import { createManagedDbBackup } from "../../src/lib/db/managedBackup.ts";
|
|
import { runDbHealthCheck } from "../../src/lib/db/healthCheck.ts";
|
|
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "health-sqljs-"));
|
|
process.env.DATA_DIR = dir;
|
|
process.env.OMNIROUTE_SKIP_DB_HEALTHCHECK = "1";
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
|
|
for (const failOpen of [false, true]) {
|
|
test(`sql.js snapshot retains connection state with destination failure=${failOpen}`, async () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "health-sqljs-state-"));
|
|
const db = await createSqlJsAdapter(":memory:");
|
|
const raw = db.raw as {
|
|
create_function(name: string, callback: () => number): void;
|
|
prepare(sql: string): { step(): boolean; get(): unknown[]; free(): void };
|
|
};
|
|
const originalOpen = fs.openSync;
|
|
try {
|
|
db.exec("CREATE TABLE sample (id INTEGER); INSERT INTO sample VALUES (1)");
|
|
db.pragma("temp_store=2");
|
|
db.exec("CREATE TEMP TABLE pending (id INTEGER); INSERT INTO pending VALUES (42)");
|
|
raw.create_function("health_marker", () => 42);
|
|
const statement = raw.prepare("SELECT health_marker()");
|
|
const keys = ["cache_size", "temp_store", "busy_timeout", "synchronous"];
|
|
for (const setting of [
|
|
"cache_size=-65536",
|
|
"temp_store=2",
|
|
"busy_timeout=2000",
|
|
"synchronous=1",
|
|
])
|
|
db.pragma(setting);
|
|
const before = keys.map((key) => db.pragma(key, { simple: true }));
|
|
if (failOpen)
|
|
fs.openSync = () => {
|
|
throw new Error("destination failure");
|
|
};
|
|
let ok: boolean;
|
|
try {
|
|
ok = createManagedDbBackup(db, "state", tmp);
|
|
} finally {
|
|
fs.openSync = originalOpen;
|
|
}
|
|
assert.equal(ok, !failOpen);
|
|
assert.deepEqual(
|
|
keys.map((key) => db.pragma(key, { simple: true })),
|
|
before
|
|
);
|
|
assert.deepEqual(db.prepare("SELECT * FROM pending").all(), [{ id: 42 }]);
|
|
assert.deepEqual(db.prepare("SELECT health_marker() AS value").get(), { value: 42 });
|
|
assert.equal(statement.step(), true);
|
|
assert.deepEqual(statement.get(), [42]);
|
|
statement.free();
|
|
} finally {
|
|
fs.openSync = originalOpen;
|
|
db.close();
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
}
|
|
});
|
|
}
|
|
|
|
test("sql.js snapshot filename collision cannot unlink an unrelated database", async () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "health-sqljs-collision-"));
|
|
const owner = await createSqlJsAdapter(":memory:");
|
|
const random = Math.random;
|
|
let other: Awaited<ReturnType<typeof createSqlJsAdapter>> | undefined;
|
|
try {
|
|
owner.exec("CREATE TABLE sample(id INTEGER); INSERT INTO sample VALUES(42)");
|
|
Math.random = () => 0.125;
|
|
other = await createSqlJsAdapter(":memory:");
|
|
other.exec("CREATE TABLE keep(id INTEGER); INSERT INTO keep VALUES(99)");
|
|
assert.equal(createManagedDbBackup(owner, "collision", tmp), false);
|
|
assert.deepEqual(owner.prepare("SELECT * FROM sample").all(), [{ id: 42 }]);
|
|
const image = (other.raw as { export(): Uint8Array }).export();
|
|
assert.ok(image.length > 0, "colliding database file must still exist");
|
|
assert.deepEqual(other.prepare("SELECT * FROM keep").all(), [{ id: 99 }]);
|
|
assert.deepEqual(fs.readdirSync(tmp), []);
|
|
} finally {
|
|
Math.random = random;
|
|
other?.close();
|
|
owner.close();
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
for (const mode of ["transaction", "cursor", "query-only"]) {
|
|
test(`sql.js snapshot refuses ${mode} without disturbing owner`, async () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "health-sqljs-busy-"));
|
|
const db = await createSqlJsAdapter(":memory:");
|
|
const raw = db.raw as {
|
|
prepare(sql: string): { step(): boolean; get(): unknown[]; free(): void };
|
|
};
|
|
let cursor: ReturnType<typeof raw.prepare> | undefined;
|
|
try {
|
|
db.exec("CREATE TABLE sample(id INTEGER); INSERT INTO sample VALUES(1),(2)");
|
|
if (mode === "transaction") db.exec("BEGIN; INSERT INTO sample VALUES(3)");
|
|
if (mode === "cursor") {
|
|
cursor = raw.prepare("SELECT id FROM sample ORDER BY id");
|
|
assert.ok(cursor.step());
|
|
}
|
|
if (mode === "query-only") db.pragma("query_only=ON");
|
|
assert.equal(createManagedDbBackup(db, mode, tmp), false);
|
|
assert.deepEqual(fs.readdirSync(tmp), []);
|
|
if (mode === "transaction") {
|
|
assert.deepEqual(db.prepare("SELECT id FROM sample WHERE id=3").all(), [{ id: 3 }]);
|
|
db.exec("ROLLBACK");
|
|
}
|
|
if (cursor) {
|
|
assert.ok(cursor.step());
|
|
assert.deepEqual(cursor.get(), [2]);
|
|
}
|
|
if (mode === "query-only") assert.equal(db.pragma("query_only", { simple: true }), 1);
|
|
} finally {
|
|
cursor?.free();
|
|
db.close();
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
}
|
|
});
|
|
}
|
|
|
|
test("managed sql.js repair preserves a reopenable pre-repair host snapshot", async () => {
|
|
const db = await createSqlJsAdapter(path.join(dir, "storage.sqlite"));
|
|
db.exec(
|
|
"CREATE TABLE db_meta (key TEXT PRIMARY KEY, value TEXT); INSERT INTO db_meta VALUES ('schema_version','0')"
|
|
);
|
|
setDbInstance(db);
|
|
try {
|
|
const result = await core.runManagedDbHealthCheck({ autoRepair: true });
|
|
assert.equal(result.backupCreated, true);
|
|
assert.equal(result.repairedCount, 1);
|
|
assert.equal((db.prepare("SELECT value FROM db_meta").get() as { value: string }).value, "1");
|
|
const backups = fs.readdirSync(path.join(dir, "db_backups"));
|
|
assert.equal(backups.length, 1);
|
|
const restored = await createSqlJsAdapter(path.join(dir, "db_backups", backups[0]));
|
|
try {
|
|
assert.equal(
|
|
(restored.prepare("SELECT value FROM db_meta").get() as { value: string }).value,
|
|
"0"
|
|
);
|
|
assert.equal(restored.pragma("integrity_check", { simple: true }), "ok");
|
|
} finally {
|
|
restored.close();
|
|
}
|
|
} finally {
|
|
// resetDbInstance (not just closeDbInstance) also invalidates the read
|
|
// cache the singleton left behind, matching this project's DB-test
|
|
// cleanup convention (AGENTS.md > "Database Handles in Tests").
|
|
core.resetDbInstance();
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("sql.js memory snapshots include unpersisted data and backup errors block repair", async () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "health-sqljs-memory-"));
|
|
const db = await createSqlJsAdapter(":memory:");
|
|
db.exec(
|
|
"CREATE TABLE db_meta (key TEXT PRIMARY KEY, value TEXT); INSERT INTO db_meta VALUES ('schema_version','0')"
|
|
);
|
|
try {
|
|
const blocked = path.join(tmp, "not-a-directory");
|
|
fs.writeFileSync(blocked, "blocked");
|
|
assert.throws(
|
|
() =>
|
|
runDbHealthCheck(db, {
|
|
autoRepair: true,
|
|
createBackupBeforeRepair: () => createManagedDbBackup(db, "repair", blocked),
|
|
}),
|
|
/backup creation failed/
|
|
);
|
|
assert.equal((db.prepare("SELECT value FROM db_meta").get() as { value: string }).value, "0");
|
|
const originalFsync = fs.fsyncSync;
|
|
fs.fsyncSync = () => {
|
|
throw new Error("disk sync failed");
|
|
};
|
|
try {
|
|
assert.throws(
|
|
() =>
|
|
runDbHealthCheck(db, {
|
|
autoRepair: true,
|
|
createBackupBeforeRepair: () => createManagedDbBackup(db, "sync-failure", tmp),
|
|
}),
|
|
/backup creation failed/
|
|
);
|
|
assert.deepEqual(fs.readdirSync(tmp), ["not-a-directory"]);
|
|
assert.equal((db.prepare("SELECT value FROM db_meta").get() as { value: string }).value, "0");
|
|
} finally {
|
|
fs.fsyncSync = originalFsync;
|
|
}
|
|
assert.equal(createManagedDbBackup(db, "memory", tmp), true);
|
|
const file = fs.readdirSync(tmp).find((x) => x.endsWith(".sqlite"));
|
|
assert.ok(file);
|
|
const restored = await createSqlJsAdapter(path.join(tmp, file));
|
|
try {
|
|
assert.equal(
|
|
(restored.prepare("SELECT value FROM db_meta").get() as { value: string }).value,
|
|
"0"
|
|
);
|
|
} finally {
|
|
restored.close();
|
|
}
|
|
} finally {
|
|
db.close();
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
}
|
|
});
|