feat: add legacy combo reference canonicalization to database health check and improve table existence handling

This commit is contained in:
diegosouzapw
2026-04-13 19:09:08 -03:00
parent 3e83402470
commit 5fa5be2136
4 changed files with 136 additions and 42 deletions

20
package-lock.json generated
View File

@@ -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": {

View File

@@ -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",

View File

@@ -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);

View File

@@ -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 () => {