Added resume instrumentation to the `useAgentLogs` hook (`packages/dashboard/app/hooks/useAgentLogs.ts`) with test coverage and diagnostics documentation. The new test file is included in the dashboard test gate via `vitest.config.ts`. Fusion-Task-Id: FN-5469
151 lines
4.3 KiB
TypeScript
151 lines
4.3 KiB
TypeScript
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const recordResumeEvent = vi.fn();
|
|
const fetchAgentLogsWithMeta = vi.fn();
|
|
|
|
type SseHandlerSet = {
|
|
url: string;
|
|
onOpen?: () => void;
|
|
onReconnect?: () => void;
|
|
events?: Record<string, (event: { data: string }) => void>;
|
|
unsubscribe: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
const subscribeCalls: SseHandlerSet[] = [];
|
|
|
|
vi.mock("../../utils/resumeInstrumentation", () => ({
|
|
recordResumeEvent,
|
|
}));
|
|
|
|
vi.mock("../../api", () => ({
|
|
fetchAgentLogsWithMeta,
|
|
}));
|
|
|
|
vi.mock("../../sse-bus", () => ({
|
|
subscribeSse: (
|
|
url: string,
|
|
handlers: {
|
|
onOpen?: () => void;
|
|
onReconnect?: () => void;
|
|
events?: Record<string, (event: { data: string }) => void>;
|
|
},
|
|
) => {
|
|
const unsubscribe = vi.fn();
|
|
subscribeCalls.push({
|
|
url,
|
|
onOpen: handlers.onOpen,
|
|
onReconnect: handlers.onReconnect,
|
|
events: handlers.events,
|
|
unsubscribe,
|
|
});
|
|
return unsubscribe;
|
|
},
|
|
}));
|
|
|
|
describe("useAgentLogs resume instrumentation", () => {
|
|
beforeEach(() => {
|
|
subscribeCalls.length = 0;
|
|
recordResumeEvent.mockReset();
|
|
fetchAgentLogsWithMeta.mockReset().mockResolvedValue({ entries: [], hasMore: false, total: 0 });
|
|
});
|
|
|
|
it("emits sse-open resume event", async () => {
|
|
const { useAgentLogs } = await import("../useAgentLogs");
|
|
|
|
renderHook(() => useAgentLogs("FN-123", true, "proj-1"));
|
|
|
|
await waitFor(() => {
|
|
expect(subscribeCalls[0]?.onOpen).toBeTypeOf("function");
|
|
});
|
|
|
|
act(() => {
|
|
subscribeCalls[0]?.onOpen?.();
|
|
});
|
|
|
|
expect(recordResumeEvent).toHaveBeenCalledTimes(1);
|
|
expect(recordResumeEvent).toHaveBeenCalledWith(expect.objectContaining({
|
|
view: "useAgentLogs",
|
|
trigger: "sse-open",
|
|
projectId: "proj-1",
|
|
replayAttempted: false,
|
|
sseChannel: "/api/tasks/FN-123/logs/stream",
|
|
detail: { taskId: "FN-123" },
|
|
}));
|
|
});
|
|
|
|
it("emits sse-reconnect event and keeps agent:log handler reachable", async () => {
|
|
const { useAgentLogs } = await import("../useAgentLogs");
|
|
|
|
const { result } = renderHook(() => useAgentLogs("FN-123", true, "proj-1"));
|
|
|
|
await waitFor(() => {
|
|
expect(subscribeCalls[0]?.onReconnect).toBeTypeOf("function");
|
|
expect(subscribeCalls[0]?.events?.["agent:log"]).toBeTypeOf("function");
|
|
});
|
|
|
|
act(() => {
|
|
subscribeCalls[0]?.onReconnect?.();
|
|
});
|
|
|
|
expect(recordResumeEvent).toHaveBeenCalledWith(expect.objectContaining({
|
|
view: "useAgentLogs",
|
|
trigger: "sse-reconnect",
|
|
projectId: "proj-1",
|
|
replayAttempted: false,
|
|
sseChannel: "/api/tasks/FN-123/logs/stream",
|
|
detail: { taskId: "FN-123" },
|
|
}));
|
|
|
|
act(() => {
|
|
subscribeCalls[0]?.events?.["agent:log"]?.({
|
|
data: JSON.stringify({
|
|
timestamp: "2026-01-01T00:00:00Z",
|
|
taskId: "FN-123",
|
|
text: "live-log",
|
|
type: "text",
|
|
}),
|
|
});
|
|
});
|
|
|
|
expect(result.current.entries.at(-1)?.text).toBe("live-log");
|
|
});
|
|
|
|
it("emits project-context-change and tears down prior subscription", async () => {
|
|
const { useAgentLogs } = await import("../useAgentLogs");
|
|
|
|
const { rerender } = renderHook(
|
|
({ projectId }) => useAgentLogs("FN-123", true, projectId),
|
|
{ initialProps: { projectId: "proj-a" } },
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(subscribeCalls).toHaveLength(1);
|
|
});
|
|
|
|
rerender({ projectId: "proj-b" });
|
|
|
|
expect(subscribeCalls[0]?.unsubscribe).toHaveBeenCalledTimes(1);
|
|
expect(recordResumeEvent).toHaveBeenCalledWith(expect.objectContaining({
|
|
view: "useAgentLogs",
|
|
trigger: "project-context-change",
|
|
projectId: "proj-b",
|
|
replayAttempted: false,
|
|
reason: "context-version-bumped",
|
|
detail: { taskId: "FN-123" },
|
|
}));
|
|
});
|
|
|
|
it("does not emit sse events when disabled", async () => {
|
|
const { useAgentLogs } = await import("../useAgentLogs");
|
|
|
|
renderHook(() => useAgentLogs("FN-123", false, "proj-1"));
|
|
|
|
await Promise.resolve();
|
|
|
|
expect(subscribeCalls).toHaveLength(0);
|
|
expect(recordResumeEvent).not.toHaveBeenCalledWith(expect.objectContaining({ trigger: "sse-open" }));
|
|
expect(recordResumeEvent).not.toHaveBeenCalledWith(expect.objectContaining({ trigger: "sse-reconnect" }));
|
|
});
|
|
});
|