feat(FN-5195): merge fusion/fn-5195
This commit is contained in:
@@ -37,6 +37,24 @@ describe("AutoClaimSnapshotManager soft-delete guards", () => {
|
||||
expect(snapshot.tasks.map((task) => task.id)).toEqual([live.id]);
|
||||
});
|
||||
|
||||
it("omits a checked-out task once it is soft-deleted", async () => {
|
||||
const live = await store.createTask({ title: "Live", description: "live" });
|
||||
const checkedOutDeleted = await store.createTask({ title: "Checked out", description: "checked out" });
|
||||
await store.moveTask(live.id, "todo");
|
||||
await store.moveTask(checkedOutDeleted.id, "todo");
|
||||
await store.updateTask(checkedOutDeleted.id, {
|
||||
checkedOutBy: "agent-1",
|
||||
checkoutLeaseEpoch: 1,
|
||||
checkoutNodeId: "node-1",
|
||||
});
|
||||
await store.deleteTask(checkedOutDeleted.id);
|
||||
|
||||
const manager = new AutoClaimSnapshotManager({ taskStore: store });
|
||||
const snapshot = await manager.getSnapshot();
|
||||
|
||||
expect(snapshot.tasks.map((task) => task.id)).toEqual([live.id]);
|
||||
});
|
||||
|
||||
it("drops deleted ids after cache invalidation and rebuild", async () => {
|
||||
let includeDeletedCandidate = true;
|
||||
const listTasks = vi.fn(async () => ([
|
||||
@@ -85,7 +103,7 @@ describe("AutoClaimSnapshotManager soft-delete guards", () => {
|
||||
expect(afterDelete.tasks.map((task) => task.id)).toEqual(["FN-001"]);
|
||||
});
|
||||
|
||||
it("defense-in-depth filters deleted candidates from synthetic listTasks results", async () => {
|
||||
it("defense-in-depth filters checked-out soft-deleted candidates from synthetic listTasks results", async () => {
|
||||
const listTasks = vi.fn(async () => ([
|
||||
{
|
||||
id: "FN-live",
|
||||
@@ -100,6 +118,7 @@ describe("AutoClaimSnapshotManager soft-delete guards", () => {
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
checkedOutBy: null,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
@@ -115,6 +134,7 @@ describe("AutoClaimSnapshotManager soft-delete guards", () => {
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
checkedOutBy: "agent-1",
|
||||
deletedAt: "2026-01-02T00:00:00.000Z",
|
||||
},
|
||||
] as unknown as Task[]));
|
||||
|
||||
@@ -22,6 +22,8 @@ function makeTask(overrides: Partial<Task> & Pick<Task, "id">): Task {
|
||||
currentStep: overrides.currentStep ?? 0,
|
||||
log: overrides.log ?? [],
|
||||
assignedAgentId: overrides.assignedAgentId,
|
||||
checkedOutBy: overrides.checkedOutBy,
|
||||
checkoutLeaseEpoch: overrides.checkoutLeaseEpoch,
|
||||
paused: overrides.paused,
|
||||
deletedAt: overrides.deletedAt,
|
||||
} as unknown as Task;
|
||||
@@ -47,13 +49,18 @@ describe("TaskExecutor soft-delete guards", () => {
|
||||
resetExecutorMocks();
|
||||
});
|
||||
|
||||
it("refuses to execute soft-deleted tasks and releases execution lock", async () => {
|
||||
it("refuses to execute soft-deleted tasks even when checked out and releases execution lock", async () => {
|
||||
const store = createStore();
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
const warnSpy = vi.spyOn(executorLog, "warn");
|
||||
const execSyncSpy = vi.spyOn(childProcess, "execSync");
|
||||
|
||||
const task = makeTask({ id: "FN-5137", deletedAt: "2026-01-02T00:00:00.000Z" });
|
||||
const task = makeTask({
|
||||
id: "FN-5137",
|
||||
checkedOutBy: "agent-1",
|
||||
checkoutLeaseEpoch: 2,
|
||||
deletedAt: "2026-01-02T00:00:00.000Z",
|
||||
});
|
||||
await executor.execute(task);
|
||||
|
||||
expect(execSyncSpy).not.toHaveBeenCalled();
|
||||
@@ -62,10 +69,31 @@ describe("TaskExecutor soft-delete guards", () => {
|
||||
executingTaskLock.release(task.id);
|
||||
});
|
||||
|
||||
it("resumeOrphaned skips in-progress tasks that are soft-deleted", async () => {
|
||||
it("resumeTaskForAgent skips checked-out soft-deleted tasks", async () => {
|
||||
const deletedTask = makeTask({
|
||||
id: "FN-assigned-deleted",
|
||||
column: "in-progress",
|
||||
assignedAgentId: "agent-1",
|
||||
checkedOutBy: "agent-1",
|
||||
checkoutLeaseEpoch: 3,
|
||||
paused: false,
|
||||
deletedAt: "2026-01-03T00:00:00.000Z",
|
||||
});
|
||||
const store = createStore({ tasks: [deletedTask] });
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
const executeSpy = vi.spyOn(executor, "execute");
|
||||
|
||||
await executor.resumeTaskForAgent("agent-1");
|
||||
|
||||
expect(executeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resumeOrphaned skips checked-out soft-deleted in-progress tasks", async () => {
|
||||
const deletedTask = makeTask({
|
||||
id: "FN-deleted",
|
||||
column: "in-progress",
|
||||
checkedOutBy: "agent-1",
|
||||
checkoutLeaseEpoch: 4,
|
||||
paused: false,
|
||||
deletedAt: "2026-01-03T00:00:00.000Z",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user