test(FN-5105): expand soft-delete behavioral coverage

Fusion-Task-Id: FN-5105
Fusion-Task-Lineage: a876d5eb-d828-415e-b6f0-07bbc8713417
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 04:48:12 -07:00
committed by gsxdsm
parent 7c9209bba1
commit e4178718ef

View File

@@ -1,8 +1,9 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { createTaskStoreTestHarness } from "./store-test-helpers.js";
import { createDistributedTaskIdAllocator, reconcileTaskIdState } from "../distributed-task-id.js";
describe("TaskStore soft delete", () => {
const harness = createTaskStoreTestHarness();
@@ -32,7 +33,7 @@ describe("TaskStore soft delete", () => {
expect(deletedEvents).toContain(task.id);
});
it("excludes soft-deleted tasks from live readers and FTS search", async () => {
it("excludes soft-deleted tasks from live readers, list filters, modified feeds, and FTS search", async () => {
const store = harness.store();
const task = await store.createTask({ column: "todo", title: "Soft delete me", description: "keyword-needle description" });
@@ -43,9 +44,17 @@ describe("TaskStore soft delete", () => {
const listed = await store.listTasks();
expect(listed.map((entry) => entry.id)).not.toContain(task.id);
expect((await store.listTasks({ column: "todo" })).map((entry) => entry.id)).not.toContain(task.id);
expect((await store.listTasks({ includeArchived: true })).map((entry) => entry.id)).not.toContain(task.id);
expect((await store.listTasks({ column: "archived", includeArchived: true })).map((entry) => entry.id)).not.toContain(task.id);
const modified = await store.listTasksModifiedSince("1970-01-01T00:00:00.000Z", 100, { includeArchived: true });
expect(modified.tasks.map((entry) => entry.id)).not.toContain(task.id);
const after = await store.searchTasks("keyword-needle");
expect(after.map((entry) => entry.id)).not.toContain(task.id);
expect((await store.searchTasks(task.id)).map((entry) => entry.id)).not.toContain(task.id);
expect((await store.searchTasks("Soft delete me")).map((entry) => entry.id)).not.toContain(task.id);
});
it("allows deleting parent after dependent is soft-deleted", async () => {
@@ -57,4 +66,66 @@ describe("TaskStore soft delete", () => {
await store.deleteTask(dependent.id);
await expect(store.deleteTask(parent.id)).resolves.toMatchObject({ id: parent.id });
});
it("emits task:deleted exactly once from watcher polling after soft delete", async () => {
const store = harness.store();
const task = await store.createTask({ description: "watcher delete task" });
const deletedEvents: string[] = [];
store.on("task:deleted", (event) => deletedEvents.push(event.id));
await store.listTasks();
await store.deleteTask(task.id);
await (store as any).checkForChanges();
const afterFirstPoll = deletedEvents.length;
await (store as any).checkForChanges();
expect(afterFirstPoll).toBeGreaterThanOrEqual(1);
expect(deletedEvents.length).toBe(afterFirstPoll);
});
it("keeps soft-deleted ids reserved and non-archived", async () => {
const store = harness.store();
const task = await store.createTask({ description: "reserved id task" });
await store.deleteTask(task.id);
expect(() => (store as any).assertTaskIdAvailable(task.id)).toThrow();
expect((store as any).taskIdExistsAnywhere(task.id)).toBe(true);
expect((store as any).isTaskArchived(task.id)).toBe(false);
const prefix = task.id.split("-")[0];
reconcileTaskIdState((store as any).db);
const allocator = createDistributedTaskIdAllocator((store as any).db);
const state = await allocator.getDistributedTaskIdState({ prefix });
expect(state.nextSequence).toBeGreaterThan(1);
});
it("archiveTask still hard-deletes from active tasks table", async () => {
const store = harness.store();
const doneTask = await store.createTask({ column: "done", description: "archive me" });
await store.archiveTask(doneTask.id);
const row = (store as any).db
.prepare('SELECT id, deletedAt FROM tasks WHERE id = ?')
.get(doneTask.id) as { id: string; deletedAt: string | null } | undefined;
expect(row).toBeUndefined();
expect((store as any).archiveDb.get(doneTask.id)?.id).toBe(doneTask.id);
});
it("unlinks mission feature task references when task is soft-deleted", async () => {
const store = harness.store();
const unlinkFeatureFromTask = vi.fn();
(store as any).missionStore = {
getFeatureByTaskId: () => ({ id: "F-001" }),
unlinkFeatureFromTask,
};
const task = await store.createTask({ description: "linked task" });
await store.deleteTask(task.id);
expect(unlinkFeatureFromTask).toHaveBeenCalledWith("F-001");
});
});