feat(FN-5195): merge fusion/fn-5195

This commit is contained in:
gsxdsm
2026-05-20 02:47:34 -07:00
parent c9fd41ef26
commit b1c0a91752
4 changed files with 136 additions and 6 deletions

View File

@@ -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[]));

View File

@@ -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",
});