Adds a reconcile script to recover leaked soft-deleted tasks, threads delete audit context through all callers, and records soft-delete audit events with archive column tracking across core/engine/cli/dashboard, with reliability backstop tests covering caller alignment. Fusion-Task-Id: FN-5175
119 lines
4.0 KiB
TypeScript
119 lines
4.0 KiB
TypeScript
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { Settings, Task, TaskStore } from "@fusion/core";
|
|
|
|
import { TriageProcessor } from "../triage.js";
|
|
|
|
function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
|
return {
|
|
listTasks: vi.fn().mockResolvedValue([]),
|
|
getTask: vi.fn(),
|
|
getSettings: vi.fn().mockResolvedValue({ requirePlanApproval: false } as Settings),
|
|
parseDependenciesFromPrompt: vi.fn().mockResolvedValue([]),
|
|
parseStepsFromPrompt: vi.fn().mockResolvedValue([]),
|
|
updateTask: vi.fn(),
|
|
moveTask: vi.fn(),
|
|
logEntry: vi.fn(),
|
|
deleteTask: vi.fn(),
|
|
on: vi.fn(),
|
|
off: vi.fn(),
|
|
...overrides,
|
|
} as unknown as TaskStore;
|
|
}
|
|
|
|
function createTask(overrides: Partial<Task> = {}): Task {
|
|
return {
|
|
id: "FN-001",
|
|
title: "Task",
|
|
description: "desc",
|
|
column: "triage",
|
|
status: "planning",
|
|
dependencies: [],
|
|
steps: [],
|
|
currentStep: 0,
|
|
log: [{ timestamp: new Date().toISOString(), action: "Spec review: APPROVE" }],
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("triage finalize duplicate lineage", () => {
|
|
let rootDir = "";
|
|
|
|
beforeEach(async () => {
|
|
rootDir = await mkdtemp(join(tmpdir(), "fusion-triage-dup-"));
|
|
await mkdir(join(rootDir, ".fusion", "tasks", "FN-001"), { recursive: true });
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(rootDir, { recursive: true, force: true });
|
|
});
|
|
|
|
async function runRecovery(task: Task, prompt: string, store: TaskStore): Promise<void> {
|
|
await writeFile(join(rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), prompt);
|
|
const processor = new TriageProcessor(store, rootDir);
|
|
await processor.recoverApprovedTask(task);
|
|
}
|
|
|
|
it("captures title-only duplicate references", async () => {
|
|
const store = createMockStore();
|
|
await runRecovery(
|
|
createTask({ title: "Foo (duplicate of FN-4894)", description: "plain" }),
|
|
"# Task: FN-001 - Foo\n\nBody",
|
|
store,
|
|
);
|
|
|
|
expect(vi.mocked(store.updateTask).mock.calls[0]?.[1]).toEqual(
|
|
expect.objectContaining({ sourceMetadataPatch: { duplicateOfTaskIds: ["FN-4894"] } }),
|
|
);
|
|
});
|
|
|
|
it("dedupes references across title and description in order", async () => {
|
|
const store = createMockStore();
|
|
await runRecovery(
|
|
createTask({ title: "(duplicate of FN-4894)", description: "duplicates FN-4894, FN-4847" }),
|
|
"# Task: FN-001 - Foo\n\nBody",
|
|
store,
|
|
);
|
|
|
|
expect(vi.mocked(store.updateTask).mock.calls[0]?.[1]).toEqual(
|
|
expect.objectContaining({ sourceMetadataPatch: { duplicateOfTaskIds: ["FN-4894", "FN-4847"] } }),
|
|
);
|
|
});
|
|
|
|
it("filters self references", async () => {
|
|
const store = createMockStore();
|
|
await runRecovery(
|
|
createTask({ title: "(duplicate of FN-001)", description: "duplicate of FN-001" }),
|
|
"# Task: FN-001 - Foo\n\nBody",
|
|
store,
|
|
);
|
|
|
|
expect(vi.mocked(store.updateTask).mock.calls[0]?.[1]).not.toHaveProperty("sourceMetadataPatch");
|
|
});
|
|
|
|
it("is a no-op when no references are present", async () => {
|
|
const store = createMockStore();
|
|
await runRecovery(createTask({ title: "Normal title", description: "Normal desc" }), "# Task: FN-001 - Foo\n\nBody", store);
|
|
|
|
expect(vi.mocked(store.updateTask).mock.calls[0]?.[1]).not.toHaveProperty("sourceMetadataPatch");
|
|
});
|
|
|
|
it("preserves duplicate stub delete path", async () => {
|
|
const store = createMockStore();
|
|
await runRecovery(createTask(), "DUPLICATE: FN-4894\n", store);
|
|
|
|
expect(store.deleteTask).toHaveBeenCalledWith("FN-001", expect.objectContaining({
|
|
removeLineageReferences: true,
|
|
auditContext: expect.objectContaining({
|
|
agentId: "triage",
|
|
runId: expect.stringMatching(/^triage-delete-FN-001-/),
|
|
}),
|
|
}));
|
|
expect(store.updateTask).not.toHaveBeenCalled();
|
|
});
|
|
});
|