mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
feat: add legacy combo reference canonicalization to database health check and improve table existence handling
This commit is contained in:
20
package-lock.json
generated
20
package-lock.json
generated
@@ -42,7 +42,7 @@
|
||||
"recharts": "^3.7.0",
|
||||
"selfsigned": "^5.5.0",
|
||||
"tsx": "^4.21.0",
|
||||
"undici": "^8.0.2",
|
||||
"undici": "^7.24.7",
|
||||
"uuid": "^13.0.0",
|
||||
"wreq-js": "^2.0.1",
|
||||
"xxhash-wasm": "^1.1.0",
|
||||
@@ -13099,16 +13099,6 @@
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/undici": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz",
|
||||
"integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
@@ -19735,12 +19725,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-8.1.0.tgz",
|
||||
"integrity": "sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw==",
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz",
|
||||
"integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"recharts": "^3.7.0",
|
||||
"selfsigned": "^5.5.0",
|
||||
"tsx": "^4.21.0",
|
||||
"undici": "^8.0.2",
|
||||
"undici": "^7.24.7",
|
||||
"uuid": "^13.0.0",
|
||||
"wreq-js": "^2.0.1",
|
||||
"xxhash-wasm": "^1.1.0",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { normalizeComboStep } from "@/lib/combos/steps";
|
||||
|
||||
type SqliteDatabase = import("better-sqlite3").Database;
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
@@ -152,9 +154,29 @@ function repairComboRows(
|
||||
const nextModels: unknown[] = [];
|
||||
let removedSteps = 0;
|
||||
let clearedConnectionPins = 0;
|
||||
let normalizedLegacyComboRefs = 0;
|
||||
|
||||
for (const rawStep of currentModels) {
|
||||
for (const [index, rawStep] of currentModels.entries()) {
|
||||
if (!isRecord(rawStep)) {
|
||||
if (typeof rawStep === "string") {
|
||||
const normalizedStep = normalizeComboStep(rawStep, {
|
||||
comboName: row.name,
|
||||
index,
|
||||
allCombos: existingComboNames,
|
||||
});
|
||||
if (normalizedStep?.kind === "combo-ref") {
|
||||
if (
|
||||
normalizedStep.comboName === row.name ||
|
||||
!existingComboNames.has(normalizedStep.comboName)
|
||||
) {
|
||||
removedSteps += 1;
|
||||
continue;
|
||||
}
|
||||
nextModels.push(normalizedStep);
|
||||
normalizedLegacyComboRefs += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
nextModels.push(rawStep);
|
||||
continue;
|
||||
}
|
||||
@@ -181,11 +203,11 @@ function repairComboRows(
|
||||
nextModels.push(rawStep);
|
||||
}
|
||||
|
||||
if (removedSteps === 0 && clearedConnectionPins === 0) {
|
||||
if (removedSteps === 0 && clearedConnectionPins === 0 && normalizedLegacyComboRefs === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
issueCount += removedSteps + clearedConnectionPins;
|
||||
issueCount += removedSteps + clearedConnectionPins + normalizedLegacyComboRefs;
|
||||
if (!options.autoRepair) continue;
|
||||
|
||||
const nextCombo = {
|
||||
@@ -198,6 +220,9 @@ function repairComboRows(
|
||||
clearedConnectionPins > 0
|
||||
? `${clearedConnectionPins} missing connection pin(s) cleared.`
|
||||
: null,
|
||||
normalizedLegacyComboRefs > 0
|
||||
? `${normalizedLegacyComboRefs} legacy combo ref step(s) canonicalized.`
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
@@ -207,7 +232,7 @@ function repairComboRows(
|
||||
};
|
||||
|
||||
updateComboStmt.run(JSON.stringify(nextCombo), checkedAt, row.id);
|
||||
repairedCount += removedSteps + clearedConnectionPins;
|
||||
repairedCount += removedSteps + clearedConnectionPins + normalizedLegacyComboRefs;
|
||||
}
|
||||
|
||||
return { issueCount, repairedCount };
|
||||
@@ -391,22 +416,26 @@ export function runDbHealthCheck(
|
||||
});
|
||||
}
|
||||
|
||||
const comboRows = db
|
||||
.prepare(
|
||||
"SELECT id, name, data, sort_order, created_at, updated_at FROM combos ORDER BY name COLLATE NOCASE ASC"
|
||||
)
|
||||
.all() as ComboRow[];
|
||||
const comboRepair = repairComboRows(db, comboRows, checkedAt, { autoRepair });
|
||||
if (comboRepair.issueCount > 0) {
|
||||
issues.push({
|
||||
type: "broken_reference",
|
||||
table: "combos",
|
||||
description:
|
||||
"Combos contained broken combo references, invalid JSON, or pinned connections that no longer exist.",
|
||||
count: comboRepair.issueCount,
|
||||
});
|
||||
ensureBackupBeforeRepair();
|
||||
repairedCount += comboRepair.repairedCount;
|
||||
if (hasRows(db, "combos")) {
|
||||
const comboRows = db
|
||||
.prepare(
|
||||
"SELECT id, name, data, sort_order, created_at, updated_at FROM combos ORDER BY name COLLATE NOCASE ASC"
|
||||
)
|
||||
.all() as ComboRow[];
|
||||
const comboRepair = repairComboRows(db, comboRows, checkedAt, { autoRepair });
|
||||
if (comboRepair.issueCount > 0) {
|
||||
issues.push({
|
||||
type: "broken_reference",
|
||||
table: "combos",
|
||||
description:
|
||||
"Combos contained broken combo references, legacy combo refs, invalid JSON, or pinned connections that no longer exist.",
|
||||
count: comboRepair.issueCount,
|
||||
});
|
||||
if (autoRepair) {
|
||||
ensureBackupBeforeRepair();
|
||||
repairedCount += comboRepair.repairedCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const orphanQuotaCount = countOrphanQuotaSnapshots(db);
|
||||
|
||||
@@ -77,6 +77,19 @@ test("runDbHealthCheck reports issues without mutating when autoRepair is disabl
|
||||
assert.equal(db.prepare("SELECT COUNT(*) AS count FROM domain_fallback_chains").get().count, 1);
|
||||
});
|
||||
|
||||
test("runDbHealthCheck tolerates databases without a combos table", async () => {
|
||||
const db = core.getDbInstance();
|
||||
db.exec("DROP TABLE combos");
|
||||
|
||||
const result = healthCheckDb.runDbHealthCheck(db, { autoRepair: false });
|
||||
|
||||
assert.equal(result.isHealthy, true);
|
||||
assert.equal(
|
||||
result.issues.some((issue) => issue.table === "combos"),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("runDbHealthCheck auto-repairs orphan rows and invalid JSON payloads", async () => {
|
||||
const db = core.getDbInstance();
|
||||
insertBrokenRows(db);
|
||||
@@ -110,6 +123,25 @@ test("runDbHealthCheck repairs broken combo payloads, combo refs and stale conne
|
||||
name: "Healthy Connection",
|
||||
apiKey: "sk-healthy",
|
||||
});
|
||||
db.prepare(
|
||||
"INSERT INTO combos (id, name, data, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
|
||||
).run(
|
||||
"combo-child",
|
||||
"combo-child",
|
||||
JSON.stringify({
|
||||
id: "combo-child",
|
||||
name: "combo-child",
|
||||
strategy: "priority",
|
||||
models: ["openai/gpt-4o-mini"],
|
||||
config: {},
|
||||
isActive: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}),
|
||||
0,
|
||||
now,
|
||||
now
|
||||
);
|
||||
|
||||
db.prepare(
|
||||
"INSERT INTO combos (id, name, data, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
|
||||
@@ -125,6 +157,8 @@ test("runDbHealthCheck repairs broken combo payloads, combo refs and stale conne
|
||||
name: "combo-broken",
|
||||
strategy: "priority",
|
||||
models: [
|
||||
"combo-child",
|
||||
"combo-broken",
|
||||
{ id: "ref-missing", kind: "combo-ref", comboName: "missing-child", weight: 0 },
|
||||
{
|
||||
id: "model-pinned",
|
||||
@@ -168,17 +202,58 @@ test("runDbHealthCheck repairs broken combo payloads, combo refs and stale conne
|
||||
result.issues.some((issue) => issue.table === "combos"),
|
||||
true
|
||||
);
|
||||
assert.equal(result.repairedCount, 3);
|
||||
assert.equal(result.repairedCount, 5);
|
||||
assert.equal(invalidCombo.isActive, false);
|
||||
assert.match(invalidCombo.repairNote, /invalid JSON/i);
|
||||
assert.equal(repairedCombo.models.length, 2);
|
||||
assert.equal(repairedCombo.models.length, 3);
|
||||
assert.equal(
|
||||
repairedCombo.models.some((step) => step.kind === "combo-ref"),
|
||||
false
|
||||
repairedCombo.models.some(
|
||||
(step) => step.kind === "combo-ref" && step.comboName === "combo-child"
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal("connectionId" in repairedCombo.models[0], false);
|
||||
assert.equal(repairedCombo.models[1].connectionId, activeConnection.id);
|
||||
assert.equal("connectionId" in repairedCombo.models[1], false);
|
||||
assert.equal(repairedCombo.models[2].connectionId, activeConnection.id);
|
||||
assert.match(repairedCombo.repairNote, /broken combo step/i);
|
||||
assert.match(repairedCombo.repairNote, /legacy combo ref/i);
|
||||
});
|
||||
|
||||
test("runDbHealthCheck diagnosis does not request backups for combo-only issues", async () => {
|
||||
const db = core.getDbInstance();
|
||||
const now = new Date().toISOString();
|
||||
let backupAttempts = 0;
|
||||
|
||||
db.prepare(
|
||||
"INSERT INTO combos (id, name, data, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
|
||||
).run(
|
||||
"combo-broken",
|
||||
"combo-broken",
|
||||
JSON.stringify({
|
||||
id: "combo-broken",
|
||||
name: "combo-broken",
|
||||
strategy: "priority",
|
||||
models: [{ id: "ref-missing", kind: "combo-ref", comboName: "missing-child", weight: 0 }],
|
||||
config: {},
|
||||
isActive: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}),
|
||||
1,
|
||||
now,
|
||||
now
|
||||
);
|
||||
|
||||
const result = healthCheckDb.runDbHealthCheck(db, {
|
||||
autoRepair: false,
|
||||
createBackupBeforeRepair: () => {
|
||||
backupAttempts += 1;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.repairedCount, 0);
|
||||
assert.equal(result.backupCreated, false);
|
||||
assert.equal(backupAttempts, 0);
|
||||
});
|
||||
|
||||
test("getDbInstance can auto-repair persisted broken rows when startup repair is forced", async () => {
|
||||
|
||||
Reference in New Issue
Block a user