feat(FN-5124): add soft-delete QA boundary regression coverage
Adds a regression test for FN-5124 covering soft-delete QA boundary cases, targeting the engine reliability interaction suite. The single new test file exercises edge conditions in the soft-delete path to guard against future regressions. Fusion-Task-Id: FN-5124 Fusion-Task-Lineage: 7166e252-9c98-4f51-956a-1402dae6f402
This commit is contained in:
committed by
gsxdsm
parent
0cf65b4269
commit
15bb84f862
81
packages/core/src/__tests__/soft-delete-qa-FN-5124.test.ts
Normal file
81
packages/core/src/__tests__/soft-delete-qa-FN-5124.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { createTaskStoreTestHarness } from "./store-test-helpers.js";
|
||||
|
||||
describe("soft-delete QA boundary audit (FN-5124)", () => {
|
||||
const harness = createTaskStoreTestHarness();
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.beforeEach();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await harness.afterEach();
|
||||
});
|
||||
|
||||
it("re-delete is deterministic: second delete fails not-found and does not emit duplicate task:deleted", async () => {
|
||||
const store = harness.store();
|
||||
const task = await store.createTask({ column: "todo", description: "redelete target" });
|
||||
|
||||
const deletedEvents: string[] = [];
|
||||
store.on("task:deleted", (event) => deletedEvents.push(event.id));
|
||||
|
||||
await store.deleteTask(task.id);
|
||||
const firstDeletedAt = ((store as any).db.prepare("SELECT deletedAt FROM tasks WHERE id = ?").get(task.id) as { deletedAt: string | null })
|
||||
.deletedAt;
|
||||
|
||||
await expect(store.deleteTask(task.id)).rejects.toThrow(`Task ${task.id} not found`);
|
||||
|
||||
const secondDeletedAt = ((store as any).db.prepare("SELECT deletedAt FROM tasks WHERE id = ?").get(task.id) as { deletedAt: string | null })
|
||||
.deletedAt;
|
||||
|
||||
expect(firstDeletedAt).toBeTruthy();
|
||||
expect(secondDeletedAt).toBe(firstDeletedAt);
|
||||
expect(deletedEvents).toEqual([task.id]);
|
||||
});
|
||||
|
||||
it("preserves dependents by default and rewrites blockedBy when removeDependencyReferences is true", async () => {
|
||||
const store = harness.store();
|
||||
const parent = await store.createTask({ column: "todo", title: "parent", description: "parent task" });
|
||||
const dependent = await store.createTask({ column: "todo", title: "dependent", description: "dependent task" });
|
||||
|
||||
await store.updateTask(dependent.id, { dependencies: [parent.id] });
|
||||
|
||||
await expect(store.deleteTask(parent.id)).rejects.toThrow(/depend/i);
|
||||
|
||||
await store.deleteTask(parent.id, { removeDependencyReferences: true });
|
||||
|
||||
const dependentAfter = await store.getTask(dependent.id);
|
||||
expect(dependentAfter.dependencies).not.toContain(parent.id);
|
||||
expect((store as any).findLiveDependents(parent.id)).toEqual([]);
|
||||
});
|
||||
|
||||
it("archiving a soft-deleted task succeeds and hard-removes the row consistently", async () => {
|
||||
const store = harness.store();
|
||||
const doneTask = await store.createTask({ column: "done", title: "done task", description: "done task description" });
|
||||
|
||||
await store.deleteTask(doneTask.id);
|
||||
const archived = await store.archiveTask(doneTask.id);
|
||||
|
||||
const liveRow = (store as any).db.prepare("SELECT id, deletedAt FROM tasks WHERE id = ?").get(doneTask.id) as
|
||||
| { id: string; deletedAt: string | null }
|
||||
| undefined;
|
||||
|
||||
expect(archived.column).toBe("archived");
|
||||
expect(liveRow).toBeUndefined();
|
||||
expect((store as any).archiveDb.get(doneTask.id)?.id).toBe(doneTask.id);
|
||||
});
|
||||
|
||||
it.each(["todo", "in-progress", "in-review", "done", "triage"])(
|
||||
"keeps ID reservation after soft-delete (%s)",
|
||||
async (column) => {
|
||||
const store = harness.store();
|
||||
const task = await store.createTask({ column: column as any, title: `reserve-${column}`, description: `reserve ${column}` });
|
||||
|
||||
await store.deleteTask(task.id);
|
||||
|
||||
expect(() => (store as any).assertTaskIdAvailable(task.id)).toThrow(`Task ID already exists: ${task.id}`);
|
||||
expect((store as any).taskIdExistsAnywhere(task.id)).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -677,6 +677,20 @@ test("createIsolatedHomeEnv: returns temp HOME/USERPROFILE pair without mutating
|
||||
rmSync(isolatedHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
||||
test("createIsolatedHomeEnv: preserves a stable COREPACK_HOME outside the isolated HOME", () => {
|
||||
const baseEnv = {
|
||||
PATH: process.env.PATH || "",
|
||||
HOME: "/tmp/original-home",
|
||||
};
|
||||
const { env, isolatedHome } = createIsolatedHomeEnv(baseEnv);
|
||||
|
||||
assert.equal(env.HOME, isolatedHome);
|
||||
assert.equal(env.COREPACK_HOME, path.join(baseEnv.HOME, ".cache", "node", "corepack"));
|
||||
|
||||
rmSync(isolatedHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("cleanupIsolatedHomePath: removes existing isolated HOME directory", () => {
|
||||
const homePath = mkdtempSync(path.join(tmpdir(), "fusion-test-home-root-cleanup-"));
|
||||
assert.equal(path.basename(homePath).startsWith("fusion-test-home-root-cleanup-"), true);
|
||||
|
||||
@@ -766,10 +766,16 @@ export function createIsolatedHomeEnv(env = process.env) {
|
||||
knownIsolatedHomeBasenames.add(path.basename(rawIsolatedHome));
|
||||
knownIsolatedHomeBasenames.add(path.basename(isolatedHome));
|
||||
|
||||
const inheritedHome = env.HOME || env.USERPROFILE;
|
||||
const corepackHome = env.COREPACK_HOME || (inheritedHome
|
||||
? path.join(inheritedHome, ".cache", "node", "corepack")
|
||||
: undefined);
|
||||
|
||||
const nextEnv = {
|
||||
...env,
|
||||
HOME: isolatedHome,
|
||||
USERPROFILE: isolatedHome,
|
||||
...(corepackHome ? { COREPACK_HOME: corepackHome } : {}),
|
||||
};
|
||||
|
||||
if (process.platform === "win32") {
|
||||
|
||||
Reference in New Issue
Block a user