Adds a title-id drift detection and reconciliation system to prevent duplicate tasks when task titles change after initial creation. Core adds the `task-title-id-drift.ts` module with `markTitleIdStable` and `resolveTitleIdDrift` logic, wired through `db.ts`, `store.ts`, and `archive-db.ts`, with co Fusion-Task-Id: FN-4898
88 lines
2.8 KiB
TypeScript
88 lines
2.8 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import { hasGit, makeReliabilityFixture } from "./_helpers.js";
|
|
|
|
const describeIfGit = hasGit ? describe : describe.skip;
|
|
|
|
describeIfGit("reliability interactions: self-defeating dep reconciliation", () => {
|
|
const fixtures: Array<Awaited<ReturnType<typeof makeReliabilityFixture>>> = [];
|
|
|
|
afterEach(async () => {
|
|
while (fixtures.length) await fixtures.pop()!.cleanup();
|
|
});
|
|
|
|
it("reconciles pre-existing offender in todo and records audit + task log", async () => {
|
|
const fx = await makeReliabilityFixture({
|
|
taskId: "FN-4891-A",
|
|
task: {
|
|
column: "todo",
|
|
title: "safe title",
|
|
dependencies: ["FN-100", "FN-200"],
|
|
} as any,
|
|
});
|
|
fixtures.push(fx);
|
|
|
|
fx.store.getDatabase().prepare("UPDATE tasks SET title = ? WHERE id = ?").run("Finalize FN-100: close loop", fx.task.id);
|
|
|
|
const recovered = await fx.manager.reconcileSelfDefeatingDependencies();
|
|
expect(recovered).toBe(1);
|
|
|
|
const updated = await fx.store.getTask(fx.task.id);
|
|
expect(updated?.dependencies).toEqual(["FN-200"]);
|
|
expect(
|
|
updated?.log.some((entry) => JSON.stringify(entry).includes("Auto-reconciled self-defeating dependency")),
|
|
).toBe(true);
|
|
|
|
const events = fx.store.getRunAuditEvents({
|
|
taskId: fx.task.id,
|
|
domain: "database",
|
|
mutationType: "task:auto-reconciled-self-defeating-dep",
|
|
});
|
|
expect(events).toHaveLength(1);
|
|
expect(events[0]?.target).toBe(fx.task.id);
|
|
expect(events[0]?.metadata).toMatchObject({
|
|
matchedVerb: "finalize",
|
|
operandTaskId: "FN-100",
|
|
originalDependencies: ["FN-100", "FN-200"],
|
|
nextDependencies: ["FN-200"],
|
|
});
|
|
});
|
|
|
|
it("does not reconcile non-operational test title", async () => {
|
|
const fx = await makeReliabilityFixture({
|
|
taskId: "FN-4891-B",
|
|
task: {
|
|
column: "todo",
|
|
title: "Test FN-100",
|
|
dependencies: ["FN-100"],
|
|
} as any,
|
|
});
|
|
fixtures.push(fx);
|
|
|
|
const recovered = await fx.manager.reconcileSelfDefeatingDependencies();
|
|
expect(recovered).toBe(0);
|
|
|
|
const updated = await fx.store.getTask(fx.task.id);
|
|
expect(updated?.dependencies).toEqual(["FN-100"]);
|
|
});
|
|
|
|
it("does not touch in-progress offenders", async () => {
|
|
const fx = await makeReliabilityFixture({
|
|
taskId: "FN-4891-C",
|
|
task: {
|
|
column: "in-progress",
|
|
title: "safe title",
|
|
dependencies: ["FN-100"],
|
|
} as any,
|
|
});
|
|
fixtures.push(fx);
|
|
|
|
fx.store.getDatabase().prepare("UPDATE tasks SET title = ? WHERE id = ?").run("Finalize FN-100", fx.task.id);
|
|
|
|
const recovered = await fx.manager.reconcileSelfDefeatingDependencies();
|
|
expect(recovered).toBe(0);
|
|
|
|
const updated = await fx.store.getTask(fx.task.id);
|
|
expect(updated?.dependencies).toEqual(["FN-100"]);
|
|
});
|
|
});
|