Files
fusion/packages/engine/src/__tests__/executor-soft-delete-guard.test.ts
2026-05-20 02:47:34 -07:00

109 lines
3.8 KiB
TypeScript

import "./executor-test-helpers.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Task } from "@fusion/core";
import { TaskExecutor } from "../executor.js";
import { executingTaskLock } from "../active-session-registry.js";
import { executorLog } from "../logger.js";
import * as childProcess from "node:child_process";
import { resetExecutorMocks } from "./executor-test-helpers.js";
function makeTask(overrides: Partial<Task> & Pick<Task, "id">): Task {
return {
id: overrides.id,
title: overrides.title ?? null,
description: overrides.description ?? "desc",
status: overrides.status ?? "open",
column: overrides.column ?? "in-progress",
createdAt: overrides.createdAt ?? "2026-01-01T00:00:00.000Z",
updatedAt: overrides.updatedAt ?? "2026-01-01T00:00:00.000Z",
dependencies: overrides.dependencies ?? [],
comments: overrides.comments ?? [],
steps: overrides.steps ?? [],
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;
}
function createStore(overrides?: { tasks?: Task[] }) {
const listeners = new Map<string, ((payload: unknown) => void)[]>();
const store = {
on: vi.fn((event: string, listener: (payload: unknown) => void) => {
const existing = listeners.get(event) ?? [];
existing.push(listener);
listeners.set(event, existing);
}),
off: vi.fn(),
getSettings: vi.fn().mockResolvedValue({ globalPause: false, enginePaused: false }),
listTasks: vi.fn().mockResolvedValue(overrides?.tasks ?? []),
} as any;
return store;
}
describe("TaskExecutor soft-delete guards", () => {
beforeEach(() => {
resetExecutorMocks();
});
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",
checkedOutBy: "agent-1",
checkoutLeaseEpoch: 2,
deletedAt: "2026-01-02T00:00:00.000Z",
});
await executor.execute(task);
expect(execSyncSpy).not.toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith("FN-5137: refusing execute — task is soft-deleted");
expect(executingTaskLock.tryClaim(task.id)).toBe(true);
executingTaskLock.release(task.id);
});
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",
});
const store = createStore({ tasks: [deletedTask] });
const executor = new TaskExecutor(store, "/tmp/test");
const executeSpy = vi.spyOn(executor, "execute");
await executor.resumeOrphaned();
expect(executeSpy).not.toHaveBeenCalled();
});
});