Files
fusion/packages/engine/src/__tests__/triage-soft-delete-abort.test.ts
Fusion (runfusion.ai) ab0a253004 feat(FN-5264): abort in-flight executor/merge/triage work on soft-delete (F
Implements in-flight abort for soft-deleted tasks across all three execution lanes: executor, merger, and triage now check for `deletedAt` before proceeding and emit `task:soft-delete-in-flight-abort` audits rather than continuing work on a deleted task. The 829-line addition is dominated by integra

Fusion-Task-Id: FN-5264

Fusion-Task-Lineage: 4ee8e63b-abf0-43e2-9130-39a05434d8f9
2026-05-20 01:16:06 -07:00

103 lines
3.5 KiB
TypeScript

import "./executor-test-helpers.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { TriageProcessor } from "../triage.js";
import { resetExecutorMocks } from "./executor-test-helpers.js";
type Listener = (...args: any[]) => void;
function createEventedStore() {
const listeners = new Map<string, Set<Listener>>();
return {
store: {
getSettings: vi.fn().mockResolvedValue({ pollIntervalMs: 60_000, maxConcurrent: 1, maxWorktrees: 1, autoMerge: true }),
listTasks: vi.fn().mockResolvedValue([]),
updateTask: vi.fn().mockResolvedValue(undefined),
on: vi.fn((event: string, listener: Listener) => {
const set = listeners.get(event) ?? new Set<Listener>();
set.add(listener);
listeners.set(event, set);
}),
off: vi.fn((event: string, listener: Listener) => {
listeners.get(event)?.delete(listener);
}),
} as any,
emit(event: string, ...args: any[]) {
for (const listener of listeners.get(event) ?? []) {
listener(...args);
}
},
};
}
describe("TriageProcessor soft-delete aborts", () => {
beforeEach(() => {
resetExecutorMocks();
vi.clearAllMocks();
});
it("aborts and disposes an active specify session on task:deleted", async () => {
const { store, emit } = createEventedStore();
const stuckTaskDetector = { untrackTask: vi.fn() };
const processor = new TriageProcessor(store, "/tmp/root", { stuckTaskDetector } as any);
const abort = vi.fn().mockResolvedValue(undefined);
const dispose = vi.fn();
processor.start();
(processor as any).activeSessions.set("FN-TEST-1", { abort, dispose });
emit("task:deleted", { id: "FN-TEST-1" });
await Promise.resolve();
expect(abort).toHaveBeenCalledTimes(1);
expect(dispose).toHaveBeenCalledTimes(1);
expect((processor as any).activeSessions.has("FN-TEST-1")).toBe(false);
expect((processor as any).pauseAborted.has("FN-TEST-1")).toBe(true);
expect(stuckTaskDetector.untrackTask).toHaveBeenCalledWith("FN-TEST-1");
processor.stop();
});
it("disposes reviewer subagents for a soft-deleted task", () => {
const { store, emit } = createEventedStore();
const processor = new TriageProcessor(store, "/tmp/root");
const dispose = vi.fn();
processor.start();
(processor as any).registerSubagentSession("FN-TEST-2", { dispose });
emit("task:deleted", { id: "FN-TEST-2" });
expect(dispose).toHaveBeenCalledTimes(1);
expect((processor as any).activeSubagentSessions.has("FN-TEST-2")).toBe(false);
processor.stop();
});
it("is a no-op for unknown soft-deleted ids", () => {
const { store, emit } = createEventedStore();
const processor = new TriageProcessor(store, "/tmp/root");
processor.start();
expect(() => emit("task:deleted", { id: "FN-UNKNOWN" })).not.toThrow();
processor.stop();
});
it("detaches the task:deleted listener on stop", () => {
const { store, emit } = createEventedStore();
const processor = new TriageProcessor(store, "/tmp/root");
const abort = vi.fn().mockResolvedValue(undefined);
const dispose = vi.fn();
processor.start();
(processor as any).activeSessions.set("FN-TEST-3", { abort, dispose });
processor.stop();
const abortCallsAfterStop = abort.mock.calls.length;
const disposeCallsAfterStop = dispose.mock.calls.length;
emit("task:deleted", { id: "FN-TEST-3" });
expect(abort).toHaveBeenCalledTimes(abortCallsAfterStop);
expect(dispose).toHaveBeenCalledTimes(disposeCallsAfterStop);
});
});