AUDIT / src /lib /__tests__ /agentSSEDispatch.test.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
6.08 kB
/**
* agentSSEDispatch.test.ts — MX16-ABORT regression suite
*
* Verifica abort_reason enum "user_stop" vs "system":
* R1: user_stop → toast.success "✓ Task fermato" (non toast.info)
* R2: user_stop → eventRuntime.emit NON chiamato
* R3: system abort → toast.info + eventRuntime.emit chiamato
* R4: abort_reason assente → default user_stop (no eventRuntime)
* R5: store.updateStatus("CANCELLED") chiamato sempre
*
* Pattern mock: vi.mock() + vi.stubGlobal — identico a backendSchedulerHealth.test.ts
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
// ─── Mock dipendenze PRIMA degli import ──────────────────────────────────────
vi.mock("sonner", () => ({
toast: Object.assign(vi.fn(), {
success: vi.fn(),
info: vi.fn(),
error: vi.fn(),
}),
}));
vi.mock("@/core/events", () => ({
eventRuntime: { emit: vi.fn() },
}));
vi.mock("@/lib/streamManager", () => ({
streamManager: { cancel: vi.fn(), init: vi.fn() },
}));
vi.mock("@/lib/checkpointManager", () => ({
onTaskEnd: vi.fn(),
onStepDone: vi.fn(),
}));
vi.mock("@/lib/buildFailureHandler", () => ({
handleBuildFailure: vi.fn(),
}));
vi.mock("@/lib/sseActionInferer", () => ({
inferActionType: vi.fn(() => "tool_call"),
inferActionLabel: vi.fn(() => "Tool"),
inferActionDetail: vi.fn(() => undefined),
}));
vi.mock("@/lib/agentTelegram", () => ({
tgNotify: { taskStart: vi.fn(), taskComplete: vi.fn(), taskError: vi.fn() },
}));
// agentSSEState: Maps vuote — _abGoal sarà "" → no auto-retry setTimeout
vi.mock("@/lib/agentSSEState", () => ({
_taskParams: new Map(),
_taskStarted: new Map(),
_connectAt: new Map(),
_thinkingTimers: new Map(),
_taskTimers: new Map(),
_streamingBuffer: new Map(),
_bufferTimestamps: new Map(),
}));
// useTaskStore: mock singleton per le chiamate interne a handleEvent
vi.mock("@/store/taskStore", () => ({
useTaskStore: {
getState: vi.fn(() => ({
tasks: [],
updateStatus: vi.fn(),
addLog: vi.fn(),
addAction: vi.fn(),
updateStep: vi.fn(),
})),
},
}));
// ─── Import dopo i mock ──────────────────────────────────────────────────────
import { handleEvent } from "@/lib/agentSSEDispatch";
import { eventRuntime } from "@/core/events";
import { toast } from "sonner";
import type { SSEEvent } from "@/lib/agentSSETypes";
// ─── Helpers ─────────────────────────────────────────────────────────────────
function makeStore(extra: Record<string, unknown> = {}) {
return {
updateStatus: vi.fn(),
addLog: vi.fn(),
addAction: vi.fn(),
updateStep: vi.fn(),
tasks: [],
...extra,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
}
/** Costruisce un SSEEvent task_aborted con abort_reason opzionale */
function abortEv(abort_reason?: string): SSEEvent {
return {
event: "task_aborted",
taskId: TASK_ID,
...(abort_reason !== undefined && { abort_reason }),
} as unknown as SSEEvent;
}
const TASK_ID = "test-task-mx16";
const _toast = toast as unknown as Record<string, ReturnType<typeof vi.fn>>;
const _er = eventRuntime as unknown as { emit: ReturnType<typeof vi.fn> };
// ─── Suite ────────────────────────────────────────────────────────────────────
describe("agentSSEDispatch — task_aborted MX16-ABORT", () => {
beforeEach(() => {
vi.clearAllMocks();
// reset window.dispatchEvent per verificare no auto-retry
vi.stubGlobal("dispatchEvent", vi.fn());
});
// R1: user_stop → toast.success (non toast.info)
it("R1: user_stop → toast.success '✓ Task fermato'", () => {
handleEvent(abortEv("user_stop"), TASK_ID, makeStore());
expect(_toast.success).toHaveBeenCalledWith(
"✓ Task fermato",
expect.objectContaining({ id: "task-aborted-toast" }),
);
expect(_toast.info).not.toHaveBeenCalled();
});
// R2: user_stop → eventRuntime.emit NON chiamato (stop intenzionale ≠ errore)
it("R2: user_stop → eventRuntime.emit NON chiamato", () => {
handleEvent(abortEv("user_stop"), TASK_ID, makeStore());
expect(_er.emit).not.toHaveBeenCalled();
});
// R3: abort_reason:"system" → toast.info + eventRuntime.emit({kind:"task_error"})
it("R3: system → toast.info + eventRuntime task_error emesso", () => {
handleEvent(abortEv("system"), TASK_ID, makeStore());
expect(_toast.info).toHaveBeenCalledWith(
"Task interrotto — riavvio automatico in corso…",
expect.objectContaining({ id: "task-aborted-toast" }),
);
expect(_er.emit).toHaveBeenCalledWith(
expect.objectContaining({ kind: "task_error" }),
);
expect(_toast.success).not.toHaveBeenCalled();
});
// R4: abort_reason assente → default user_stop (no eventRuntime, toast.success)
it("R4: abort_reason assente → default user_stop (no eventRuntime)", () => {
handleEvent(abortEv(undefined), TASK_ID, makeStore());
expect(_er.emit).not.toHaveBeenCalled();
expect(_toast.success).toHaveBeenCalled();
});
// R5: store.updateStatus("CANCELLED") chiamato in entrambi i casi
it("R5: updateStatus CANCELLED — user_stop", () => {
const store = makeStore();
handleEvent(abortEv("user_stop"), TASK_ID, store);
expect(store.updateStatus).toHaveBeenCalledWith(TASK_ID, "CANCELLED");
});
it("R5b: updateStatus CANCELLED — system", () => {
const store = makeStore();
handleEvent(abortEv("system"), TASK_ID, store);
expect(store.updateStatus).toHaveBeenCalledWith(TASK_ID, "CANCELLED");
});
});