mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 19:32:20 +03:00
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host. Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean. Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
190 lines
6.2 KiB
TypeScript
190 lines
6.2 KiB
TypeScript
/**
|
|
* getDatabaseStats() must survive a SQLite build without the `dbstat` virtual
|
|
* table.
|
|
*
|
|
* `dbstat` is compile-time optional (ENABLE_DBSTAT_VTAB) and is absent from
|
|
* sql.js/WASM builds. Before the fix, the unguarded per-table `SELECT SUM(pgsize)
|
|
* FROM dbstat` threw, which propagated out of getDatabaseStats() and made
|
|
* GET/PATCH /api/settings/database return HTTP 500 — the whole database settings
|
|
* page became unusable on those runtimes.
|
|
*/
|
|
|
|
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
|
|
import { getDatabaseStats } from "@/lib/db/stats";
|
|
import type { PreparedStatement, SqliteAdapter } from "@/lib/db/adapters/types";
|
|
|
|
type FakeOptions = {
|
|
/** Error message thrown by any statement touching `dbstat`. */
|
|
dbstatError?: string;
|
|
/** Tables reported by sqlite_master. */
|
|
tables?: string[];
|
|
/** Make the dbstat probe succeed but fail for this specific table. */
|
|
failOnlyOn?: string;
|
|
/** Return `{ size: null }` from dbstat, as SUM() does over an empty table. */
|
|
nullSize?: boolean;
|
|
};
|
|
|
|
/**
|
|
* Minimal in-memory SqliteAdapter double. Only the surface getDatabaseStats()
|
|
* actually touches is implemented; everything else throws so an accidental new
|
|
* dependency shows up loudly instead of silently passing.
|
|
*/
|
|
function createFakeDb({
|
|
dbstatError,
|
|
tables = ["alpha", "beta"],
|
|
failOnlyOn,
|
|
nullSize,
|
|
}: FakeOptions = {}): SqliteAdapter {
|
|
const prepare = (sql: string): PreparedStatement => {
|
|
const touchesDbstat = /\bdbstat\b/i.test(sql);
|
|
|
|
return {
|
|
run() {
|
|
throw new Error(`unexpected run(): ${sql}`);
|
|
},
|
|
get(...params: unknown[]) {
|
|
if (touchesDbstat) {
|
|
const probing = params[0] === "sqlite_master";
|
|
// `failOnlyOn` models a driver that answers the probe but fails later.
|
|
if (failOnlyOn) {
|
|
if (params[0] === failOnlyOn) throw new Error(dbstatError ?? "no such table: dbstat");
|
|
} else if (dbstatError) {
|
|
throw new Error(dbstatError);
|
|
}
|
|
if (probing) return { size: 0 };
|
|
return { size: nullSize ? null : 4096 };
|
|
}
|
|
if (/COUNT\(\*\)/i.test(sql)) return { count: 7 };
|
|
throw new Error(`unexpected get(): ${sql}`);
|
|
},
|
|
all() {
|
|
if (/type='table'/i.test(sql)) return tables.map((name) => ({ name }));
|
|
if (/type='index'/i.test(sql)) {
|
|
return tables.length ? [{ name: "idx_alpha", tableName: "alpha" }] : [];
|
|
}
|
|
throw new Error(`unexpected all(): ${sql}`);
|
|
},
|
|
};
|
|
};
|
|
|
|
return {
|
|
driver: "sql.js",
|
|
open: true,
|
|
name: ":memory:",
|
|
prepare,
|
|
exec() {},
|
|
pragma(pragmaStr: string) {
|
|
if (pragmaStr === "page_size") return 4096;
|
|
if (pragmaStr === "page_count") return 100;
|
|
if (pragmaStr === "cache_size") return -65536;
|
|
throw new Error(`unexpected pragma: ${pragmaStr}`);
|
|
},
|
|
transaction<T>(fn: (...args: unknown[]) => T) {
|
|
return fn;
|
|
},
|
|
immediate(fn: () => void) {
|
|
fn();
|
|
},
|
|
async backup() {},
|
|
checkpoint() {},
|
|
close() {},
|
|
raw: null,
|
|
} satisfies SqliteAdapter;
|
|
}
|
|
|
|
test("getDatabaseStats reports per-table sizes when dbstat is available", () => {
|
|
const stats = getDatabaseStats(createFakeDb());
|
|
|
|
assert.equal(stats.totalSize, 4096 * 100);
|
|
assert.deepEqual(
|
|
stats.tables.map((t) => [t.name, t.rowCount, t.size]),
|
|
[
|
|
["alpha", 7, 4096],
|
|
["beta", 7, 4096],
|
|
]
|
|
);
|
|
});
|
|
|
|
test("getDatabaseStats degrades to size 0 when dbstat module is missing", () => {
|
|
const stats = getDatabaseStats(createFakeDb({ dbstatError: "no such module: dbstat" }));
|
|
|
|
// The call must succeed; only per-table byte sizes are lost.
|
|
assert.deepEqual(
|
|
stats.tables.map((t) => [t.name, t.rowCount, t.size]),
|
|
[
|
|
["alpha", 7, 0],
|
|
["beta", 7, 0],
|
|
]
|
|
);
|
|
// Database-level numbers come from pragmas and stay accurate.
|
|
assert.equal(stats.totalSize, 4096 * 100);
|
|
assert.equal(stats.pageCount, 100);
|
|
assert.equal(stats.cacheSize, -65536);
|
|
assert.equal(stats.indexes.length, 1);
|
|
});
|
|
|
|
test("getDatabaseStats degrades when the driver reports dbstat as a missing table", () => {
|
|
// SQLite builds lacking ENABLE_DBSTAT_VTAB commonly report this variant.
|
|
const stats = getDatabaseStats(createFakeDb({ dbstatError: "no such table: dbstat" }));
|
|
|
|
assert.deepEqual(
|
|
stats.tables.map((t) => t.size),
|
|
[0, 0]
|
|
);
|
|
});
|
|
|
|
test("getDatabaseStats degrades when the driver prefixes its error class", () => {
|
|
// Real drivers stringify as "SqliteError: ..." / "RuntimeError: ...", so the
|
|
// guard must not be anchored to the start of the message.
|
|
for (const message of [
|
|
"SqliteError: no such table: dbstat",
|
|
"RuntimeError: no such module: dbstat",
|
|
]) {
|
|
const stats = getDatabaseStats(createFakeDb({ dbstatError: message }));
|
|
assert.deepEqual(
|
|
stats.tables.map((t) => t.size),
|
|
[0, 0],
|
|
`expected degradation for ${message}`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("getDatabaseStats handles a database with no user tables", () => {
|
|
// The shape a fresh install hits before any migration has run.
|
|
const stats = getDatabaseStats(createFakeDb({ tables: [] }));
|
|
|
|
assert.deepEqual(stats.tables, []);
|
|
assert.deepEqual(stats.indexes, []);
|
|
assert.equal(stats.totalSize, 4096 * 100);
|
|
});
|
|
|
|
test("getDatabaseStats maps a NULL dbstat sum to 0", () => {
|
|
// SUM(pgsize) returns NULL when a table occupies no pages.
|
|
const stats = getDatabaseStats(createFakeDb({ nullSize: true }));
|
|
|
|
assert.deepEqual(
|
|
stats.tables.map((t) => t.size),
|
|
[0, 0]
|
|
);
|
|
});
|
|
|
|
test("getDatabaseStats propagates a dbstat failure that appears after the probe", () => {
|
|
// Documents current behaviour: the probe establishes availability once, so a
|
|
// later per-table failure is treated as a genuine fault rather than a missing
|
|
// module. Anything else would mask real I/O errors mid-iteration.
|
|
assert.throws(
|
|
() => getDatabaseStats(createFakeDb({ failOnlyOn: "beta" })),
|
|
/no such table: dbstat/
|
|
);
|
|
});
|
|
|
|
test("getDatabaseStats still propagates unrelated dbstat failures", () => {
|
|
// A genuine fault (disk I/O, corruption) must not be silently swallowed.
|
|
assert.throws(
|
|
() => getDatabaseStats(createFakeDb({ dbstatError: "database disk image is malformed" })),
|
|
/database disk image is malformed/
|
|
);
|
|
});
|