feat(FN-5284): surface db corruption via health checks, self-healing, and d
- fix(FN-5284): add db corruption audit mutation type - feat(FN-5284): complete Step 7 — document corruption surfacing - feat(FN-5284): complete Step 6 — add db corruption banner - feat(FN-5284): complete Step 5 — extend health payload - feat(FN-5284): complete Step 3 — surface db corruption in self-healing - feat(FN-5284): complete Step 2 — add db corruption notification event - feat(FN-5284): complete Step 1 — expose integrity check errors Fusion-Task-Id: FN-5284
This commit is contained in:
committed by
gsxdsm
parent
12a6ee9af1
commit
1b49cbc94d
@@ -465,6 +465,8 @@ describe("Database", () => {
|
||||
expect(dbB.integrityCheckLastRunAt).toBeTruthy();
|
||||
expect(dbA.corruptionDetected).toBe(false);
|
||||
expect(dbB.corruptionDetected).toBe(false);
|
||||
expect(dbA.integrityCheckErrors).toEqual([]);
|
||||
expect(dbB.integrityCheckErrors).toEqual([]);
|
||||
} finally {
|
||||
dbA.close();
|
||||
dbB.close();
|
||||
@@ -478,7 +480,7 @@ describe("Database", () => {
|
||||
vi.useFakeTimers();
|
||||
const integritySpy = vi.spyOn(Database.prototype, "integrityCheck").mockReturnValue({
|
||||
ok: false,
|
||||
errors: ["malformed database"],
|
||||
errors: ["malformed database", "broken index"],
|
||||
});
|
||||
const freshDir = makeTmpDir();
|
||||
const freshFusionDir = join(freshDir, ".fusion");
|
||||
@@ -498,6 +500,8 @@ describe("Database", () => {
|
||||
expect(dbB.integrityCheckLastRunAt).toBeTruthy();
|
||||
expect(dbA.corruptionDetected).toBe(true);
|
||||
expect(dbB.corruptionDetected).toBe(true);
|
||||
expect(dbA.integrityCheckErrors).toEqual(["malformed database", "broken index"]);
|
||||
expect(dbB.integrityCheckErrors).toEqual(["malformed database", "broken index"]);
|
||||
} finally {
|
||||
dbA.close();
|
||||
dbB.close();
|
||||
@@ -1036,6 +1040,7 @@ describe("Database", () => {
|
||||
it("returns ok for healthy databases and leaves corruption flag false", () => {
|
||||
expect(db.corruptionDetected).toBe(false);
|
||||
expect(db.integrityCheck()).toEqual({ ok: true });
|
||||
expect(db.integrityCheckErrors).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps corruptionDetected false after init for healthy database", () => {
|
||||
|
||||
@@ -16,6 +16,8 @@ describe("TaskStore.getDatabaseHealth", () => {
|
||||
const health = store.getDatabaseHealth();
|
||||
|
||||
expect(health.healthy).toBe(true);
|
||||
expect(health.corruptionDetected).toBe(false);
|
||||
expect(health.corruptionErrors).toEqual([]);
|
||||
expect(health.isRunning).toBe(false);
|
||||
expect(health.lastCheckedAt).toBeNull();
|
||||
});
|
||||
@@ -27,19 +29,34 @@ describe("TaskStore.getDatabaseHealth", () => {
|
||||
const health = store.getDatabaseHealth();
|
||||
|
||||
expect(health.healthy).toBe(true);
|
||||
expect(health.corruptionDetected).toBe(false);
|
||||
expect(health.corruptionErrors).toEqual([]);
|
||||
expect(health.isRunning).toBe(true);
|
||||
});
|
||||
|
||||
it("reports unhealthy when corruption has been detected", () => {
|
||||
const db = store.getDatabase();
|
||||
db.corruptionDetected = true;
|
||||
db.integrityCheckErrors = ["bad row", "bad index"];
|
||||
db.integrityCheckPending = false;
|
||||
db.integrityCheckLastRunAt = "2026-05-11T12:34:56.000Z";
|
||||
|
||||
const health = store.getDatabaseHealth();
|
||||
|
||||
expect(health.healthy).toBe(false);
|
||||
expect(health.corruptionDetected).toBe(true);
|
||||
expect(health.corruptionErrors).toEqual(["bad row", "bad index"]);
|
||||
expect(health.isRunning).toBe(false);
|
||||
expect(health.lastCheckedAt?.toISOString()).toBe("2026-05-11T12:34:56.000Z");
|
||||
});
|
||||
|
||||
it("caps corruption errors to the first five entries", () => {
|
||||
const db = store.getDatabase();
|
||||
db.corruptionDetected = true;
|
||||
db.integrityCheckErrors = ["one", "two", "three", "four", "five", "six"];
|
||||
|
||||
const health = store.getDatabaseHealth();
|
||||
|
||||
expect(health.corruptionErrors).toEqual(["one", "two", "three", "four", "five"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1251,6 +1251,7 @@ export class Database {
|
||||
/** Returns the database file path (or ":memory:" for in-memory databases). */
|
||||
get path(): string { return this.dbPath; }
|
||||
corruptionDetected = false;
|
||||
integrityCheckErrors: string[] = [];
|
||||
integrityCheckPending = false;
|
||||
integrityCheckLastRunAt: string | null = null;
|
||||
/** Tracks transaction nesting depth for savepoint-based nested transactions. */
|
||||
@@ -3684,6 +3685,7 @@ export class Database {
|
||||
participant.integrityCheckPending = false;
|
||||
participant.integrityCheckLastRunAt = startedAt;
|
||||
participant.corruptionDetected = !integrity.ok;
|
||||
participant.integrityCheckErrors = integrity.ok ? [] : [...integrity.errors];
|
||||
}
|
||||
|
||||
if (!integrity.ok) {
|
||||
|
||||
@@ -9329,11 +9329,16 @@ ${stepsSection}`;
|
||||
|
||||
getDatabaseHealth(): {
|
||||
healthy: boolean;
|
||||
corruptionDetected: boolean;
|
||||
corruptionErrors: string[];
|
||||
lastCheckedAt: Date | null;
|
||||
isRunning: boolean;
|
||||
} {
|
||||
const corruptionDetected = this.db.corruptionDetected;
|
||||
return {
|
||||
healthy: !this.db.corruptionDetected,
|
||||
healthy: !corruptionDetected,
|
||||
corruptionDetected,
|
||||
corruptionErrors: this.db.integrityCheckErrors.slice(0, 5),
|
||||
lastCheckedAt: this.db.integrityCheckLastRunAt ? new Date(this.db.integrityCheckLastRunAt) : null,
|
||||
isRunning: this.db.integrityCheckPending,
|
||||
};
|
||||
|
||||
@@ -404,6 +404,7 @@ export type NtfyNotificationEvent =
|
||||
| "planning-awaiting-input"
|
||||
| "gridlock"
|
||||
| "board-stall-unrecovered"
|
||||
| "db-corruption-detected"
|
||||
| "fallback-used"
|
||||
| "memory-dreams-processed"
|
||||
| "token-budget"
|
||||
@@ -422,6 +423,7 @@ export const NOTIFICATION_EVENTS = [
|
||||
"planning-awaiting-input",
|
||||
"gridlock",
|
||||
"board-stall-unrecovered",
|
||||
"db-corruption-detected",
|
||||
"fallback-used",
|
||||
"memory-dreams-processed",
|
||||
"token-budget",
|
||||
|
||||
Reference in New Issue
Block a user