feat(FN-4192): add task ID integrity detection and dashboard alerts
Implements task ID integrity detection across the stack (FN-4192): a new `task-id-integrity` module in core provides detector logic wired into the store, integrity health is surfaced via the dashboard API and legacy endpoint, and a banner component surfaces alerts in the UI — backed by comprehensive Fusion-Task-Id: FN-4192
This commit is contained in:
111
packages/core/src/__tests__/store-task-id-integrity.test.ts
Normal file
111
packages/core/src/__tests__/store-task-id-integrity.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { mkdir, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { Database } from "../db.js";
|
||||
import { TaskStore } from "../store.js";
|
||||
import { makeTmpDir } from "./store-test-helpers.js";
|
||||
|
||||
async function seedIntegrityPrecondition(rootDir: string): Promise<void> {
|
||||
const fusionDir = join(rootDir, ".fusion");
|
||||
await mkdir(fusionDir, { recursive: true });
|
||||
|
||||
const db = new Database(fusionDir);
|
||||
db.init();
|
||||
const now = new Date().toISOString();
|
||||
db.prepare(
|
||||
"INSERT INTO tasks (id, description, \"column\", createdAt, updatedAt) VALUES (?, '', 'todo', ?, ?)",
|
||||
).run("FN-100", now, now);
|
||||
db.prepare(
|
||||
"INSERT INTO distributed_task_id_state (prefix, nextSequence, committedClusterTaskCount, lastCommittedTaskId, updatedAt) VALUES (?, ?, ?, ?, ?)",
|
||||
).run("FN", 100, 0, null, now);
|
||||
db.close();
|
||||
}
|
||||
|
||||
describe("TaskStore task ID integrity wiring", () => {
|
||||
let rootDir = "";
|
||||
let globalDir = "";
|
||||
|
||||
beforeEach(() => {
|
||||
rootDir = makeTmpDir();
|
||||
globalDir = makeTmpDir();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await rm(rootDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
||||
await rm(globalDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
||||
});
|
||||
|
||||
it("constructs cleanly and exposes an ok integrity report by default", async () => {
|
||||
const store = new TaskStore(rootDir, globalDir);
|
||||
await store.init();
|
||||
|
||||
const report = store.getTaskIdIntegrityReport();
|
||||
|
||||
expect(report.status).toBe("ok");
|
||||
expect(report.anomalies).toEqual([]);
|
||||
expect(report.checkedAt).toEqual(expect.any(String));
|
||||
|
||||
store.close();
|
||||
});
|
||||
|
||||
it("logs a structured core error and exposes anomaly status when startup detects corruption preconditions", async () => {
|
||||
await seedIntegrityPrecondition(rootDir);
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
const store = new TaskStore(rootDir, globalDir);
|
||||
await store.init();
|
||||
|
||||
const report = store.getTaskIdIntegrityReport();
|
||||
expect(report.status).toBe("anomaly");
|
||||
expect(report.anomalies).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "next_sequence_at_or_below_used",
|
||||
prefix: "FN",
|
||||
affectedIds: ["FN-100"],
|
||||
}),
|
||||
);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("[core] [task-id-integrity] anomaly detected"),
|
||||
expect.objectContaining({
|
||||
anomalies: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "next_sequence_at_or_below_used",
|
||||
affectedIds: ["FN-100"],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
|
||||
store.close();
|
||||
});
|
||||
|
||||
it("refreshTaskIdIntegrityReport picks up newly introduced anomalies", async () => {
|
||||
const store = new TaskStore(rootDir, globalDir, { inMemoryDb: true });
|
||||
await store.init();
|
||||
|
||||
const db = store.getDatabase();
|
||||
const now = new Date().toISOString();
|
||||
db.prepare(
|
||||
"INSERT INTO tasks (id, description, \"column\", createdAt, updatedAt) VALUES (?, '', 'todo', ?, ?)",
|
||||
).run("FN-100", now, now);
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO distributed_task_id_state (prefix, nextSequence, committedClusterTaskCount, lastCommittedTaskId, updatedAt) VALUES (?, ?, ?, ?, ?)",
|
||||
).run("FN", 100, 0, null, now);
|
||||
|
||||
const report = store.refreshTaskIdIntegrityReport();
|
||||
|
||||
expect(report.status).toBe("anomaly");
|
||||
expect(report.anomalies).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "next_sequence_at_or_below_used",
|
||||
prefix: "FN",
|
||||
affectedIds: ["FN-100"],
|
||||
}),
|
||||
);
|
||||
expect(store.getTaskIdIntegrityReport()).toEqual(report);
|
||||
|
||||
store.close();
|
||||
});
|
||||
});
|
||||
154
packages/core/src/__tests__/task-id-integrity.test.ts
Normal file
154
packages/core/src/__tests__/task-id-integrity.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { Database } from "../db.js";
|
||||
import { detectTaskIdIntegrityAnomalies } from "../task-id-integrity.js";
|
||||
|
||||
function createDb(): Database {
|
||||
const db = new Database("/tmp/fusion-task-id-integrity-test", { inMemory: true });
|
||||
db.init();
|
||||
return db;
|
||||
}
|
||||
|
||||
function insertTask(db: Database, id: string): void {
|
||||
const now = new Date().toISOString();
|
||||
db.prepare(
|
||||
"INSERT INTO tasks (id, description, \"column\", createdAt, updatedAt) VALUES (?, '', 'todo', ?, ?)",
|
||||
).run(id, now, now);
|
||||
}
|
||||
|
||||
describe("detectTaskIdIntegrityAnomalies", () => {
|
||||
it("returns ok for a clean database", () => {
|
||||
const db = createDb();
|
||||
|
||||
const report = detectTaskIdIntegrityAnomalies(db);
|
||||
|
||||
expect(report.status).toBe("ok");
|
||||
expect(report.checkedAt).toEqual(expect.any(String));
|
||||
expect(report.anomalies).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns ok when allocator tables are missing", () => {
|
||||
const db = createDb();
|
||||
db.exec("DROP TABLE distributed_task_id_reservations");
|
||||
db.exec("DROP TABLE distributed_task_id_state");
|
||||
|
||||
const report = detectTaskIdIntegrityAnomalies(db);
|
||||
|
||||
expect(report.status).toBe("ok");
|
||||
expect(report.anomalies).toEqual([]);
|
||||
});
|
||||
|
||||
it("detects duplicate active task IDs", () => {
|
||||
const db = createDb();
|
||||
db.exec("ALTER TABLE tasks RENAME TO tasks_original");
|
||||
db.exec("CREATE TABLE tasks (id TEXT NOT NULL, description TEXT, \"column\" TEXT, createdAt TEXT, updatedAt TEXT)");
|
||||
db.exec(`
|
||||
INSERT INTO tasks (id, description, "column", createdAt, updatedAt) VALUES
|
||||
('FN-101', '', 'todo', '2026-05-12T00:00:00.000Z', '2026-05-12T00:00:00.000Z'),
|
||||
('FN-101', '', 'todo', '2026-05-12T00:00:01.000Z', '2026-05-12T00:00:01.000Z')
|
||||
`);
|
||||
|
||||
const report = detectTaskIdIntegrityAnomalies(db);
|
||||
|
||||
expect(report.status).toBe("anomaly");
|
||||
expect(report.anomalies).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "duplicate_active_id",
|
||||
prefix: "FN",
|
||||
affectedIds: ["FN-101"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("detects IDs present in both active and archived storage", () => {
|
||||
const db = createDb();
|
||||
insertTask(db, "FN-102");
|
||||
db.prepare("INSERT INTO archivedTasks (id, data, archivedAt) VALUES (?, ?, ?)").run(
|
||||
"FN-102",
|
||||
JSON.stringify({ id: "FN-102" }),
|
||||
new Date().toISOString(),
|
||||
);
|
||||
|
||||
const report = detectTaskIdIntegrityAnomalies(db);
|
||||
|
||||
expect(report.anomalies).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "id_in_active_and_archived",
|
||||
prefix: "FN",
|
||||
affectedIds: ["FN-102"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("detects stale nextSequence values at or below an existing used sequence", () => {
|
||||
const db = createDb();
|
||||
const now = new Date().toISOString();
|
||||
insertTask(db, "FN-100");
|
||||
db.prepare(
|
||||
"INSERT INTO distributed_task_id_state (prefix, nextSequence, committedClusterTaskCount, lastCommittedTaskId, updatedAt) VALUES (?, ?, ?, ?, ?)",
|
||||
).run("FN", 100, 0, null, now);
|
||||
|
||||
const report = detectTaskIdIntegrityAnomalies(db);
|
||||
|
||||
expect(report.anomalies).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "next_sequence_at_or_below_used",
|
||||
prefix: "FN",
|
||||
affectedIds: ["FN-100"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("detects committed reservations that target existing task IDs", () => {
|
||||
const db = createDb();
|
||||
const now = new Date().toISOString();
|
||||
insertTask(db, "FN-103");
|
||||
db.prepare(
|
||||
"INSERT INTO distributed_task_id_state (prefix, nextSequence, committedClusterTaskCount, lastCommittedTaskId, updatedAt) VALUES (?, ?, ?, ?, ?)",
|
||||
).run("FN", 104, 1, "FN-103", now);
|
||||
db.prepare(
|
||||
`INSERT INTO distributed_task_id_reservations (
|
||||
reservationId, prefix, nodeId, sequence, taskId, status, reason, expiresAt, committedAt, createdAt, updatedAt
|
||||
) VALUES (?, ?, ?, ?, ?, 'committed', NULL, ?, ?, ?, ?)`,
|
||||
).run(
|
||||
"res-103",
|
||||
"FN",
|
||||
"node-a",
|
||||
103,
|
||||
"FN-103",
|
||||
new Date(Date.now() + 60_000).toISOString(),
|
||||
now,
|
||||
now,
|
||||
now,
|
||||
);
|
||||
|
||||
const report = detectTaskIdIntegrityAnomalies(db);
|
||||
|
||||
expect(report.anomalies).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "committed_reservation_for_existing_id",
|
||||
prefix: "FN",
|
||||
affectedIds: ["FN-103"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("detects active task rows whose prefix is outside distributed state", () => {
|
||||
const db = createDb();
|
||||
const now = new Date().toISOString();
|
||||
insertTask(db, "KB-001");
|
||||
db.prepare(
|
||||
"INSERT INTO distributed_task_id_state (prefix, nextSequence, committedClusterTaskCount, lastCommittedTaskId, updatedAt) VALUES (?, ?, ?, ?, ?)",
|
||||
).run("FN", 2, 0, null, now);
|
||||
|
||||
const report = detectTaskIdIntegrityAnomalies(db);
|
||||
|
||||
expect(report.anomalies).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "task_row_outside_known_prefix",
|
||||
prefix: "KB",
|
||||
affectedIds: ["KB-001"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user