Files
kandev/apps/web/hooks/domains/session/use-queue.test.ts
T
2026-06-27 19:01:18 +01:00

141 lines
4.0 KiB
TypeScript

import { act, cleanup, renderHook, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { QueuedMessage } from "@/lib/state/slices/session/types";
const queueApiMock = vi.hoisted(() => {
class QueueEntryNotFoundError extends Error {}
return {
QueueEntryNotFoundError,
queueMessage: vi.fn(),
clearQueue: vi.fn(),
drainQueuedMessage: vi.fn(),
getQueueStatus: vi.fn(),
updateQueuedMessage: vi.fn(),
removeQueuedEntry: vi.fn(),
};
});
type MockQueueState = {
queue: {
bySessionId: Record<string, QueuedMessage[]>;
metaBySessionId: Record<string, { count: number; max: number }>;
isLoading: Record<string, boolean>;
};
connection: { status: string };
setQueueEntries: ReturnType<typeof vi.fn>;
removeQueueEntry: ReturnType<typeof vi.fn>;
setQueueLoading: ReturnType<typeof vi.fn>;
};
let mockState: MockQueueState;
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (state: MockQueueState) => unknown) => selector(mockState),
}));
vi.mock("@/lib/api/domains/queue-api", () => queueApiMock);
import { useQueue } from "./use-queue";
const SESSION_ID = "sess-1";
const TASK_ID = "task-1";
function entry(overrides: Partial<QueuedMessage> = {}): QueuedMessage {
return {
id: "q-1",
session_id: SESSION_ID,
task_id: TASK_ID,
content: "queued prompt",
plan_mode: false,
queued_at: "2026-06-27T00:00:00Z",
queued_by: "user",
...overrides,
};
}
function setDocumentVisibility(value: DocumentVisibilityState) {
Object.defineProperty(document, "visibilityState", {
configurable: true,
value,
});
}
function resetMockState() {
mockState = {
queue: {
bySessionId: {},
metaBySessionId: {},
isLoading: {},
},
connection: { status: "connected" },
setQueueEntries: vi.fn(),
removeQueueEntry: vi.fn(),
setQueueLoading: vi.fn(),
};
}
describe("useQueue", () => {
beforeEach(() => {
resetMockState();
setDocumentVisibility("visible");
queueApiMock.getQueueStatus.mockResolvedValue({ entries: [], count: 0, max: 10 });
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
it("refetches the queue snapshot when the WebSocket reconnects", async () => {
mockState.connection.status = "disconnected";
const { rerender } = renderHook(() => useQueue(SESSION_ID));
await act(async () => {});
expect(queueApiMock.getQueueStatus).not.toHaveBeenCalled();
mockState.connection.status = "connected";
rerender();
await waitFor(() => expect(queueApiMock.getQueueStatus).toHaveBeenCalledWith(SESSION_ID));
expect(mockState.setQueueEntries).toHaveBeenCalledWith(SESSION_ID, [], {
count: 0,
max: 10,
});
});
it("refetches a stale queue snapshot when a suspended tab becomes visible again", async () => {
mockState.queue.bySessionId[SESSION_ID] = [entry()];
mockState.queue.metaBySessionId[SESSION_ID] = { count: 1, max: 10 };
queueApiMock.getQueueStatus.mockResolvedValueOnce({
entries: [entry()],
count: 1,
max: 10,
});
renderHook(() => useQueue(SESSION_ID));
await waitFor(() => expect(queueApiMock.getQueueStatus).toHaveBeenCalledTimes(1));
queueApiMock.getQueueStatus.mockClear();
mockState.setQueueEntries.mockClear();
queueApiMock.getQueueStatus.mockResolvedValueOnce({ entries: [], count: 0, max: 10 });
document.dispatchEvent(new Event("visibilitychange"));
await waitFor(() => expect(queueApiMock.getQueueStatus).toHaveBeenCalledWith(SESSION_ID));
expect(mockState.setQueueEntries).toHaveBeenCalledWith(SESSION_ID, [], {
count: 0,
max: 10,
});
});
it("does not refetch on foreground visibility while disconnected", async () => {
mockState.connection.status = "disconnected";
renderHook(() => useQueue(SESSION_ID));
await act(async () => {});
document.dispatchEvent(new Event("visibilitychange"));
expect(queueApiMock.getQueueStatus).not.toHaveBeenCalled();
});
});