Spaces:
Sleeping
Sleeping
| /** | |
| * 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"); | |
| }); | |
| }); | |