feat(runtime): dynamic SQLite runtime installer with 5-step fallback chain

Adds bin/cli/runtime/sqliteRuntime.mjs that resolves better-sqlite3 from:
(1) bundled optionalDependency, (2) ~/.omniroute/runtime/ install,
(3) lazy npm install into runtime dir, (4) node:sqlite stdlib (Node >=22.5),
(5) bundled sql.js WASM. Each native binary is validated against expected
platform magic bytes (ELF/Mach-O/PE) before load.

Adds bin/cli/runtime/magicBytes.mjs with validateBinaryMagic() helper
(9 tests). Adds bin/cli/runtime/index.mjs as warmUpRuntimes() orchestrator.

Adds scripts/postinstall.mjs warm-up hook (non-fatal, skipped in CI).
Integrates it as the last step of scripts/build/postinstall.mjs.

Extends src/lib/db/core.ts with ensureDbInitialized() (async, idempotent)
and getDriverInfo() so the startup orchestrator can await the resolver
before any DB access, enabling graceful degradation without crashing the
process on missing better-sqlite3.

Solves Windows EBUSY error on 'npm install -g omniroute@latest' while the
previous version is still running, and works in environments without C++
build tools or with unreachable npm registry.

Documents OMNIROUTE_SKIP_POSTINSTALL in .env.example and ENVIRONMENT.md.

Ref: 9router/cli/hooks/sqliteRuntime.js (pattern origin).
This commit is contained in:
diegosouzapw
2026-05-15 00:05:49 -03:00
parent af80efe75a
commit 8f915b18b0
12 changed files with 481 additions and 13 deletions

View File

@@ -412,6 +412,10 @@ PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
# Used by: open-sse/services/compression/engines/rtk/filterLoader.ts. Default: 0.
#OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=0
# Skip the postinstall native-runtime warm-up (useful in CI / headless installs). Default: 0.
# Used by: scripts/postinstall.mjs.
#OMNIROUTE_SKIP_POSTINSTALL=0
# Force a DB healthcheck regardless of cadence. Default: 0.
# Used by: src/lib/db/core.ts::shouldRunDbHealthCheck().
#OMNIROUTE_FORCE_DB_HEALTHCHECK=0

19
bin/cli/runtime/index.mjs Normal file
View File

@@ -0,0 +1,19 @@
import { loadSqliteRuntime } from "./sqliteRuntime.mjs";
let warmed = false;
/**
* Pre-resolves native runtimes at startup so the first DB access is fast
* and EBUSY-resilient on Windows.
*
* Tray runtime (systray2) is warmed lazily by initTray() in bin/cli/tray/.
*/
export async function warmUpRuntimes() {
if (warmed) return;
warmed = true;
try {
await loadSqliteRuntime();
} catch {}
}
export { loadSqliteRuntime };

View File

@@ -0,0 +1,48 @@
import { readFileSync, existsSync } from "node:fs";
/**
* Validates that a file starts with a known native-binary magic number.
* Returns the platform label ("elf" | "macho" | "macho-le" | "macho-fat" | "pe")
* or null if unrecognized / missing / unreadable.
*
* Ref: 9router/cli/hooks/sqliteRuntime.js (algorithm origin).
*/
export function validateBinaryMagic(path) {
if (!existsSync(path)) return null;
let buf;
try {
buf = readFileSync(path, { encoding: null }).subarray(0, 16);
} catch {
return null;
}
if (buf.length < 4) return null;
// ELF: 7F 45 4C 46
if (buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46) return "elf";
// Mach-O big-endian: FE ED FA CF (64-bit) / FE ED FA CE (32-bit)
if (buf[0] === 0xfe && buf[1] === 0xed && buf[2] === 0xfa && buf[3] === 0xcf) return "macho";
if (buf[0] === 0xfe && buf[1] === 0xed && buf[2] === 0xfa && buf[3] === 0xce) return "macho";
// Mach-O little-endian: CF FA ED FE (64-bit) / CE FA ED FE (32-bit)
if (buf[0] === 0xcf && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) return "macho-le";
if (buf[0] === 0xce && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) return "macho-le";
// Mach-O fat (universal binary): CA FE BA BE
if (buf[0] === 0xca && buf[1] === 0xfe && buf[2] === 0xba && buf[3] === 0xbe) return "macho-fat";
// PE (Windows .node — DLL): MZ at offset 0
if (buf[0] === 0x4d && buf[1] === 0x5a) return "pe";
return null;
}
/**
* Returns the expected magic label for the current platform.
* Used to validate that a runtime-installed binary matches this OS.
*/
export function platformBinaryLabel() {
if (process.platform === "win32") return "pe";
if (process.platform === "darwin") return "macho";
return "elf";
}

View File

@@ -0,0 +1,126 @@
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { execSync } from "node:child_process";
import { validateBinaryMagic, platformBinaryLabel } from "./magicBytes.mjs";
const RUNTIME_DIR = join(homedir(), ".omniroute", "runtime");
const BETTER_SQLITE3_VERSION = "better-sqlite3@^12.6.2";
let resolvedCached = null;
/**
* Resolves a SQLite driver through a 5-step fallback chain:
* 1. Bundled better-sqlite3 (optionalDependency)
* 2. Runtime-installed better-sqlite3 in ~/.omniroute/runtime/
* 3. Lazy npm install into runtime dir
* 4. node:sqlite (Node ≥22.5 stdlib)
* 5. sql.js (bundled WASM, always available)
*
* Returns { driver, source } where source is one of:
* "bundled" | "runtime" | "runtime-installed-now" | "node-sqlite" | "sql-js"
*/
export async function loadSqliteRuntime() {
if (resolvedCached) return resolvedCached;
const bundled = await tryLoadBundled();
if (bundled) {
resolvedCached = { driver: bundled, source: "bundled" };
return resolvedCached;
}
const runtimeInstalled = await tryLoadRuntimeInstalled();
if (runtimeInstalled) {
resolvedCached = { driver: runtimeInstalled, source: "runtime" };
return resolvedCached;
}
try {
await installRuntime();
const after = await tryLoadRuntimeInstalled();
if (after) {
resolvedCached = { driver: after, source: "runtime-installed-now" };
return resolvedCached;
}
} catch (err) {
console.warn(`[omniroute] runtime install failed: ${err.message}`);
}
try {
const nodeSqlite = await import("node:sqlite");
resolvedCached = {
driver: { kind: "node-sqlite", DatabaseSync: nodeSqlite.DatabaseSync },
source: "node-sqlite",
};
return resolvedCached;
} catch {}
const sqljs = await import("sql.js");
resolvedCached = {
driver: { kind: "sql-js", initSqlJs: sqljs.default ?? sqljs.initSqlJs },
source: "sql-js",
};
return resolvedCached;
}
async function tryLoadBundled() {
try {
const mod = await import("better-sqlite3");
return { kind: "better-sqlite3", Database: mod.default ?? mod };
} catch {
return null;
}
}
async function tryLoadRuntimeInstalled() {
const pkgRoot = join(RUNTIME_DIR, "node_modules", "better-sqlite3");
if (!existsSync(join(pkgRoot, "package.json"))) return null;
const buildDir = join(pkgRoot, "build", "Release");
if (existsSync(buildDir)) {
const nodeFile = readdirSync(buildDir).find((f) => f.endsWith(".node"));
if (nodeFile) {
const magic = validateBinaryMagic(join(buildDir, nodeFile));
const expected = platformBinaryLabel();
if (!magic || (magic !== expected && magic !== "macho-le" && magic !== "macho-fat")) {
console.warn(
`[omniroute] runtime sqlite binary magic mismatch (${magic}${expected}) — skipping`
);
return null;
}
}
}
try {
const mod = await import(pkgRoot);
return { kind: "better-sqlite3", Database: mod.default ?? mod };
} catch {
return null;
}
}
function ensureRuntimeDir() {
if (!existsSync(RUNTIME_DIR)) mkdirSync(RUNTIME_DIR, { recursive: true });
const pkg = join(RUNTIME_DIR, "package.json");
if (!existsSync(pkg)) {
writeFileSync(
pkg,
JSON.stringify({ name: "omniroute-runtime", private: true, type: "commonjs" }),
"utf-8"
);
}
}
async function installRuntime() {
ensureRuntimeDir();
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
execSync(
`${npm} install --prefix "${RUNTIME_DIR}" ${BETTER_SQLITE3_VERSION} --no-audit --no-fund --silent`,
{ stdio: ["ignore", "ignore", "pipe"], timeout: 180_000 }
);
}
/** Clears the cached resolved driver (for testing). */
export function clearRuntimeCache() {
resolvedCached = null;
}

View File

@@ -0,0 +1,80 @@
# SQLite Runtime Resolution
OmniRoute resolves its SQLite driver at startup through a 5-step fallback chain:
1. **Bundled `better-sqlite3`** (via `dependencies` in `package.json`)
— fastest, native binary, installed by `npm install` when build tools are present.
2. **Runtime-installed `better-sqlite3`** (in `~/.omniroute/runtime/`)
— installed lazily on first run **OR** by `scripts/build/postinstall.mjs → scripts/postinstall.mjs`.
Validates native `.node` magic bytes (ELF / Mach-O / PE) before loading
to guard against corrupt or wrong-platform binaries.
3. **`node:sqlite`** (Node ≥22.5 stdlib) — no native build needed; used when
both better-sqlite3 paths fail. Limited feature set.
4. **`sql.js`** (WASM) — final fallback. Works everywhere but is slower
and writes data on an interval rather than synchronously.
## Why this complexity?
- **Windows EBUSY**: `npm install -g omniroute@latest` can fail if the previous
version's `better_sqlite3.node` is locked by a running process. The runtime
install in `~/.omniroute/runtime/` sidesteps the global npm cache.
- **No build tools**: Some environments (corporate Windows without VS Build
Tools, minimal Docker images) cannot compile `better-sqlite3`. The runtime
installer resolves a pre-built binary from the npm registry; the fallback
drivers ensure OmniRoute still boots even if that fails.
- **Air-gapped systems**: If the npm registry is unreachable, `node:sqlite`
or `sql.js` guarantee baseline functionality.
## Magic-byte validation
Before loading a runtime-installed `.node` file, OmniRoute reads the first 8
bytes and matches against known platform magics:
| Platform | Bytes (hex) | Label |
| --------------------- | ------------- | ----------- |
| Linux | `7F 45 4C 46` | `elf` |
| macOS 64-bit BE | `FE ED FA CF` | `macho` |
| macOS 64-bit LE | `CF FA ED FE` | `macho-le` |
| macOS fat (universal) | `CA FE BA BE` | `macho-fat` |
| Windows | `4D 5A` (MZ) | `pe` |
A mismatched magic → file is ignored, fallback continues to the next step.
## Checking the active driver
```typescript
import { getDriverInfo } from "@/lib/db/core";
const info = getDriverInfo();
// { source: "bundled" | "runtime" | "runtime-installed-now" | "node-sqlite" | "sql-js",
// kind: "better-sqlite3" | "node-sqlite" | "sql-js" }
```
## Manual control
```bash
# Skip postinstall warm-up (for fast CI installs)
OMNIROUTE_SKIP_POSTINSTALL=1 npm install -g omniroute
# Force-reinstall runtime better-sqlite3
rm -rf ~/.omniroute/runtime
omniroute # will reinstall on next start
# Check what driver is active
omniroute config db-info # (if CLI command exists)
```
## Reference
Pattern adapted from `9router/cli/hooks/sqliteRuntime.js`.
Implementation:
- `bin/cli/runtime/magicBytes.mjs` — binary magic-byte validation helpers
- `bin/cli/runtime/sqliteRuntime.mjs` — 5-step runtime resolver + lazy installer
- `bin/cli/runtime/index.mjs` — startup orchestrator (`warmUpRuntimes()`)
- `scripts/postinstall.mjs` — npm post-install hook (non-fatal warm-up)
- `src/lib/db/core.ts``ensureDbInitialized()` / `getDriverInfo()` exports

View File

@@ -78,19 +78,20 @@ echo "OMNIROUTE_WS_BRIDGE_SECRET=$(openssl rand -base64 32)"
OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These variables control data location, encryption, and lifecycle.
| Variable | Default | Source File | Description |
| -------------------------------------- | -------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `DATA_DIR` | `~/.omniroute/` | `src/lib/db/core.ts` | Root directory for SQLite DB, backups, and data files. Override for Docker volumes or custom paths. |
| `STORAGE_ENCRYPTION_KEY` | _(empty = disabled)_ | `src/lib/db/encryption.ts` | AES key for full SQLite database encryption at rest. Generate with `openssl rand -hex 32`. |
| `STORAGE_ENCRYPTION_KEY_VERSION` | `v1` | `scripts/build/bootstrap-env.mjs`, `electron/main.js` | Version label for the encryption key. Increment when performing key rotation to support decryption of old backups. |
| `DISABLE_SQLITE_AUTO_BACKUP` | `false` | `src/lib/db/backup.ts` | When `true`, skips the automatic database backup that runs before migrations on every startup. |
| `OMNIROUTE_CRYPT_KEY` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** for `STORAGE_ENCRYPTION_KEY`. Accepted as a fallback when the primary variable is absent. |
| `OMNIROUTE_API_KEY_BASE64` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** (Base64-encoded form) accepted as a fallback. Decoded automatically before use. |
| `OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS` | _(unset)_ | `src/lib/db/core.ts` | Override the periodic SQLite healthcheck interval (ms). When unset, defaults are derived from `NODE_ENV`. |
| `OMNIROUTE_FORCE_DB_HEALTHCHECK` | `0` | `src/lib/db/core.ts` | Set to `1` to force the DB healthcheck loop on, even when it would normally be skipped (e.g., short-lived tasks). |
| `OMNIROUTE_MIGRATIONS_DIR` | _(auto-detect)_ | `src/lib/db/migrationRunner.ts` | Override the directory that the migration runner scans. Useful when shipping bundled migrations in custom builds. |
| `OMNIROUTE_SPEND_FLUSH_INTERVAL_MS` | _(default in code)_ | `src/lib/spend/batchWriter.ts` | Flush interval (ms) for the batched spend/cost writer. Lower values reduce write coalescing; higher values reduce DB contention. |
| `OMNIROUTE_SPEND_MAX_BUFFER_SIZE` | _(default in code)_ | `src/lib/spend/batchWriter.ts` | Max buffered spend entries before a forced flush. Raise on high-QPS deployments; lower when bounded memory matters more. |
| Variable | Default | Source File | Description |
| -------------------------------------- | -------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `DATA_DIR` | `~/.omniroute/` | `src/lib/db/core.ts` | Root directory for SQLite DB, backups, and data files. Override for Docker volumes or custom paths. |
| `STORAGE_ENCRYPTION_KEY` | _(empty = disabled)_ | `src/lib/db/encryption.ts` | AES key for full SQLite database encryption at rest. Generate with `openssl rand -hex 32`. |
| `STORAGE_ENCRYPTION_KEY_VERSION` | `v1` | `scripts/build/bootstrap-env.mjs`, `electron/main.js` | Version label for the encryption key. Increment when performing key rotation to support decryption of old backups. |
| `DISABLE_SQLITE_AUTO_BACKUP` | `false` | `src/lib/db/backup.ts` | When `true`, skips the automatic database backup that runs before migrations on every startup. |
| `OMNIROUTE_CRYPT_KEY` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** for `STORAGE_ENCRYPTION_KEY`. Accepted as a fallback when the primary variable is absent. |
| `OMNIROUTE_API_KEY_BASE64` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** (Base64-encoded form) accepted as a fallback. Decoded automatically before use. |
| `OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS` | _(unset)_ | `src/lib/db/core.ts` | Override the periodic SQLite healthcheck interval (ms). When unset, defaults are derived from `NODE_ENV`. |
| `OMNIROUTE_FORCE_DB_HEALTHCHECK` | `0` | `src/lib/db/core.ts` | Set to `1` to force the DB healthcheck loop on, even when it would normally be skipped (e.g., short-lived tasks). |
| `OMNIROUTE_SKIP_POSTINSTALL` | `0` | `scripts/postinstall.mjs` | Set to `1` to skip the native-runtime warm-up during `npm install`. Useful in CI/headless installs where sqlite is already built. |
| `OMNIROUTE_MIGRATIONS_DIR` | _(auto-detect)_ | `src/lib/db/migrationRunner.ts` | Override the directory that the migration runner scans. Useful when shipping bundled migrations in custom builds. |
| `OMNIROUTE_SPEND_FLUSH_INTERVAL_MS` | _(default in code)_ | `src/lib/spend/batchWriter.ts` | Flush interval (ms) for the batched spend/cost writer. Lower values reduce write coalescing; higher values reduce DB contention. |
| `OMNIROUTE_SPEND_MAX_BUFFER_SIZE` | _(default in code)_ | `src/lib/spend/batchWriter.ts` | Max buffered spend entries before a forced flush. Raise on high-QPS deployments; lower when bounded memory matters more. |
### Scenarios

View File

@@ -25,6 +25,8 @@
"src/shared/utils/nodeRuntimeSupport.ts",
".env.example",
"scripts/build/postinstall.mjs",
"bin/cli/runtime/",
"scripts/postinstall.mjs",
"scripts/build/postinstallSupport.mjs",
"scripts/dev/responses-ws-proxy.mjs",
"scripts/check/check-supported-node-runtime.ts",

View File

@@ -314,3 +314,11 @@ await fixBetterSqliteBinary();
await fixWreqJsBinary();
await ensureSwcHelpers();
await syncProjectEnv();
// Warm up native runtimes (better-sqlite3 in ~/.omniroute/runtime/).
// Non-fatal: errors are caught inside postinstall.mjs.
try {
await import("../postinstall.mjs");
} catch {
// Silently skip — runtime warm-up is best-effort.
}

27
scripts/postinstall.mjs Normal file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env node
/**
* Post-install warm-up for OmniRoute native runtimes.
*
* Runs after the main `scripts/build/postinstall.mjs` binary-copy step.
* Tries to pre-resolve better-sqlite3 into ~/.omniroute/runtime/ so that
* the first execution is fast and EBUSY-resilient on Windows.
*
* Non-fatal: any error is printed as a warning and install continues.
*/
if (
process.env.OMNIROUTE_SKIP_POSTINSTALL === "1" ||
process.env.CI === "true" ||
process.env.CI === "1"
) {
process.exit(0);
}
(async () => {
try {
const { warmUpRuntimes } = await import("../bin/cli/runtime/index.mjs");
await warmUpRuntimes();
} catch (err) {
console.warn(`[omniroute] postinstall warm-up skipped: ${err?.message ?? err}`);
}
})();

View File

@@ -1398,6 +1398,49 @@ export function resetDbInstance() {
closeDbInstance();
}
// ──────────────── Runtime Driver Info ────────────────
type DbDriverInfo = { source: string; kind: string };
let driverInfoCached: DbDriverInfo | null = null;
function setDriverInfo(info: DbDriverInfo) {
driverInfoCached = info;
}
/** Returns how better-sqlite3 was resolved (bundled / runtime / etc.). Null if not yet init. */
export function getDriverInfo(): DbDriverInfo | null {
return driverInfoCached;
}
/**
* Async initializer that pre-resolves the SQLite runtime before first DB access.
*
* Call this at process startup (before any call to getDbInstance()) so that
* if the bundled better-sqlite3 binary is unavailable, the runtime installer
* can place it in ~/.omniroute/runtime/ without blocking a synchronous caller.
*
* Idempotent — safe to call multiple times.
*/
export async function ensureDbInitialized(): Promise<void> {
if (getDb()) return;
try {
const runtimeModule = await import("../../../bin/cli/runtime/sqliteRuntime.mjs" as any);
const { driver, source } = await runtimeModule.loadSqliteRuntime();
setDriverInfo({ source, kind: driver.kind as string });
if ((driver.kind as string) !== "better-sqlite3") {
console.warn(
`[DB] better-sqlite3 unavailable (resolved via ${source}/${driver.kind}). ` +
`OmniRoute may fall back to read-only or limited functionality.`
);
}
} catch {
// Runtime loader unavailable (CLI context only) — DB init falls through to normal path.
}
getDbInstance();
}
// ──────────────── JSON → SQLite Migration ────────────────
function migrateFromJson(db: SqliteDatabase, jsonPath: string) {

View File

@@ -0,0 +1,62 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { validateBinaryMagic, platformBinaryLabel } from "../../../bin/cli/runtime/magicBytes.mjs";
const dir = mkdtempSync(join(tmpdir(), "magic-test-"));
test("detects ELF", () => {
const p = join(dir, "fake.so");
writeFileSync(p, Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0, 0, 0, 0]));
assert.equal(validateBinaryMagic(p), "elf");
});
test("detects Mach-O 64-bit BE", () => {
const p = join(dir, "fake.dylib");
writeFileSync(p, Buffer.from([0xfe, 0xed, 0xfa, 0xcf, 0, 0, 0, 0]));
assert.equal(validateBinaryMagic(p), "macho");
});
test("detects Mach-O LE", () => {
const p = join(dir, "fake-le.dylib");
writeFileSync(p, Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 0, 0, 0, 0]));
assert.equal(validateBinaryMagic(p), "macho-le");
});
test("detects Mach-O fat binary", () => {
const p = join(dir, "fake.fat");
writeFileSync(p, Buffer.from([0xca, 0xfe, 0xba, 0xbe, 0, 0, 0, 0]));
assert.equal(validateBinaryMagic(p), "macho-fat");
});
test("detects PE (MZ)", () => {
const p = join(dir, "fake.node");
writeFileSync(p, Buffer.from([0x4d, 0x5a, 0, 0, 0, 0, 0, 0]));
assert.equal(validateBinaryMagic(p), "pe");
});
test("returns null for non-binary", () => {
const p = join(dir, "fake.txt");
writeFileSync(p, "hello world", "utf-8");
assert.equal(validateBinaryMagic(p), null);
});
test("returns null for missing file", () => {
assert.equal(validateBinaryMagic(join(dir, "nope.bin")), null);
});
test("returns null for too-short file", () => {
const p = join(dir, "short.bin");
writeFileSync(p, Buffer.from([0x7f, 0x45]));
assert.equal(validateBinaryMagic(p), null);
});
test("platformBinaryLabel matches process.platform", () => {
const expected =
process.platform === "win32" ? "pe" : process.platform === "darwin" ? "macho" : "elf";
assert.equal(platformBinaryLabel(), expected);
});
test.after(() => rmSync(dir, { recursive: true, force: true }));

View File

@@ -0,0 +1,48 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { loadSqliteRuntime, clearRuntimeCache } from "../../../bin/cli/runtime/sqliteRuntime.mjs";
test("loadSqliteRuntime returns a result with driver and source", async () => {
clearRuntimeCache();
const r = await loadSqliteRuntime();
assert.ok(r, "result is truthy");
assert.ok(typeof r.driver === "object", "driver is object");
assert.ok(typeof r.source === "string", "source is string");
});
test("loaded driver is one of the known kinds", async () => {
clearRuntimeCache();
const r = await loadSqliteRuntime();
assert.ok(
["better-sqlite3", "node-sqlite", "sql-js"].includes(r.driver.kind),
`kind="${r.driver.kind}" must be known`
);
});
test("loadSqliteRuntime caches the result (same object reference)", async () => {
clearRuntimeCache();
const a = await loadSqliteRuntime();
const b = await loadSqliteRuntime();
assert.strictEqual(a, b, "second call returns cached object");
});
test("runtime dir exists after loadSqliteRuntime when runtime source used", async () => {
clearRuntimeCache();
const r = await loadSqliteRuntime();
if (r.source === "runtime-installed-now" || r.source === "runtime") {
const runtimeDir = join(homedir(), ".omniroute", "runtime");
assert.ok(existsSync(runtimeDir), "runtime dir created");
}
});
test("clearRuntimeCache allows fresh resolution", async () => {
clearRuntimeCache();
const first = await loadSqliteRuntime();
clearRuntimeCache();
const second = await loadSqliteRuntime();
// Both should resolve to the same kind, but are different call results
assert.equal(first.driver.kind, second.driver.kind, "same driver kind after cache clear");
});