| |
|
|
| import { act } from "react"; |
| import { createRoot } from "react-dom/client"; |
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| import { ApiError } from "../../api/client"; |
| import { useLiveRunTranscripts } from "./useLiveRunTranscripts"; |
|
|
| const { useQueryMock, logMock, buildTranscriptMock } = vi.hoisted(() => ({ |
| useQueryMock: vi.fn(() => ({ data: { censorUsernameInLogs: false } })), |
| logMock: vi.fn(async () => ({ runId: "run-1", store: "memory", logRef: "log-1", content: "", nextOffset: 0 })), |
| buildTranscriptMock: vi.fn((chunks: unknown[]) => chunks), |
| })); |
|
|
| vi.mock("@tanstack/react-query", () => ({ |
| useQuery: useQueryMock, |
| })); |
|
|
| vi.mock("../../api/instanceSettings", () => ({ |
| instanceSettingsApi: { |
| getGeneral: vi.fn(), |
| }, |
| })); |
|
|
| vi.mock("../../api/heartbeats", () => ({ |
| heartbeatsApi: { |
| log: logMock, |
| }, |
| })); |
|
|
| vi.mock("../../adapters", () => ({ |
| buildTranscript: buildTranscriptMock, |
| getUIAdapter: () => null, |
| onAdapterChange: () => () => {}, |
| })); |
|
|
| class FakeWebSocket { |
| static readonly CONNECTING = 0; |
| static readonly OPEN = 1; |
| static readonly CLOSING = 2; |
| static readonly CLOSED = 3; |
| static instances: FakeWebSocket[] = []; |
|
|
| readonly url: string; |
| readyState = FakeWebSocket.CONNECTING; |
| onopen: ((event: Event) => void) | null = null; |
| onmessage: ((event: MessageEvent) => void) | null = null; |
| onerror: ((event: Event) => void) | null = null; |
| onclose: ((event: CloseEvent) => void) | null = null; |
| closeCalls: Array<{ code?: number; reason?: string }> = []; |
|
|
| constructor(url: string) { |
| this.url = url; |
| FakeWebSocket.instances.push(this); |
| } |
|
|
| close(code?: number, reason?: string) { |
| this.closeCalls.push({ code, reason }); |
| this.readyState = FakeWebSocket.CLOSING; |
| } |
|
|
| triggerOpen() { |
| this.readyState = FakeWebSocket.OPEN; |
| this.onopen?.(new Event("open")); |
| } |
| } |
|
|
| |
| (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; |
|
|
| describe("useLiveRunTranscripts", () => { |
| const OriginalWebSocket = globalThis.WebSocket; |
|
|
| beforeEach(() => { |
| FakeWebSocket.instances = []; |
| useQueryMock.mockClear(); |
| logMock.mockReset(); |
| logMock.mockImplementation(async () => ({ runId: "run-1", store: "memory", logRef: "log-1", content: "", nextOffset: 0 })); |
| buildTranscriptMock.mockClear(); |
| globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket; |
| }); |
|
|
| afterEach(() => { |
| globalThis.WebSocket = OriginalWebSocket; |
| }); |
|
|
| it("waits for a connecting socket to open before closing it during cleanup", async () => { |
| function Harness() { |
| useLiveRunTranscripts({ |
| companyId: "company-1", |
| runs: [{ id: "run-1", status: "running", adapterType: "codex_local" }], |
| }); |
| return null; |
| } |
|
|
| const container = document.createElement("div"); |
| document.body.appendChild(container); |
| const root = createRoot(container); |
|
|
| await act(async () => { |
| root.render(<Harness />); |
| await Promise.resolve(); |
| }); |
|
|
| expect(FakeWebSocket.instances).toHaveLength(1); |
| const socket = FakeWebSocket.instances[0]; |
| expect(socket.closeCalls).toHaveLength(0); |
|
|
| act(() => { |
| root.unmount(); |
| }); |
|
|
| expect(socket.closeCalls).toHaveLength(0); |
|
|
| act(() => { |
| socket.triggerOpen(); |
| }); |
|
|
| expect(socket.closeCalls).toEqual([{ code: 1000, reason: "live_run_transcripts_unmount" }]); |
| container.remove(); |
| }); |
|
|
| it("treats stored run output as available before transcript chunks finish loading", async () => { |
| let latestHasOutput = false; |
|
|
| function Harness() { |
| const { hasOutputForRun } = useLiveRunTranscripts({ |
| companyId: "company-1", |
| runs: [{ id: "run-1", status: "succeeded", adapterType: "codex_local", hasStoredOutput: true }], |
| }); |
| latestHasOutput = hasOutputForRun("run-1"); |
| return null; |
| } |
|
|
| const container = document.createElement("div"); |
| document.body.appendChild(container); |
| const root = createRoot(container); |
|
|
| await act(async () => { |
| root.render(<Harness />); |
| await Promise.resolve(); |
| }); |
|
|
| expect(latestHasOutput).toBe(true); |
|
|
| act(() => { |
| root.unmount(); |
| }); |
| container.remove(); |
| }); |
|
|
| it("reports initial hydration until the first persisted-log read completes", async () => { |
| let latestIsInitialHydrating = false; |
| type RunLogResult = { runId: string; store: string; logRef: string; content: string; nextOffset: number }; |
| let resolveLog: ((value: RunLogResult | PromiseLike<RunLogResult>) => void) | null = null; |
| logMock.mockImplementationOnce( |
| () => |
| new Promise<RunLogResult>((resolve) => { |
| resolveLog = resolve; |
| }), |
| ); |
|
|
| function Harness() { |
| const { isInitialHydrating } = useLiveRunTranscripts({ |
| companyId: "company-1", |
| runs: [{ id: "run-1", status: "succeeded", adapterType: "codex_local" }], |
| }); |
| latestIsInitialHydrating = isInitialHydrating; |
| return null; |
| } |
|
|
| const container = document.createElement("div"); |
| document.body.appendChild(container); |
| const root = createRoot(container); |
|
|
| await act(async () => { |
| root.render(<Harness />); |
| await Promise.resolve(); |
| }); |
|
|
| expect(latestIsInitialHydrating).toBe(true); |
|
|
| await act(async () => { |
| resolveLog?.({ runId: "run-1", store: "memory", logRef: "log-1", content: "", nextOffset: 0 }); |
| await Promise.resolve(); |
| }); |
|
|
| expect(latestIsInitialHydrating).toBe(false); |
|
|
| act(() => { |
| root.unmount(); |
| }); |
| container.remove(); |
| }); |
|
|
| it("stops retrying terminal runs whose persisted log never existed", async () => { |
| logMock.mockReset(); |
| logMock.mockRejectedValue(new ApiError("Run log not found", 404, { error: "Run log not found" })); |
|
|
| function Harness() { |
| useLiveRunTranscripts({ |
| companyId: "company-1", |
| runs: [{ id: "run-404", status: "failed", adapterType: "codex_local" }], |
| }); |
| return null; |
| } |
|
|
| const container = document.createElement("div"); |
| document.body.appendChild(container); |
| const root = createRoot(container); |
|
|
| await act(async () => { |
| root.render(<Harness />); |
| await Promise.resolve(); |
| }); |
|
|
| expect(logMock).toHaveBeenCalledTimes(1); |
|
|
| await act(async () => { |
| root.render(<Harness />); |
| await Promise.resolve(); |
| }); |
|
|
| expect(logMock).toHaveBeenCalledTimes(1); |
|
|
| act(() => { |
| root.unmount(); |
| }); |
| container.remove(); |
| }); |
|
|
| it("can hydrate active runs without opening the live event socket", async () => { |
| function Harness() { |
| useLiveRunTranscripts({ |
| companyId: "company-1", |
| runs: [{ id: "run-1", status: "running", adapterType: "codex_local" }], |
| enableRealtimeUpdates: false, |
| logReadLimitBytes: 64_000, |
| }); |
| return null; |
| } |
|
|
| const container = document.createElement("div"); |
| document.body.appendChild(container); |
| const root = createRoot(container); |
|
|
| await act(async () => { |
| root.render(<Harness />); |
| await Promise.resolve(); |
| }); |
|
|
| expect(FakeWebSocket.instances).toHaveLength(0); |
| expect(logMock).toHaveBeenCalledWith("run-1", 0, 64_000); |
|
|
| act(() => { |
| root.unmount(); |
| }); |
| container.remove(); |
| }); |
|
|
| it("starts persisted-log hydration from the newest bytes when the visible window is truncated", async () => { |
| function Harness() { |
| useLiveRunTranscripts({ |
| companyId: "company-1", |
| runs: [{ id: "run-1", status: "running", adapterType: "codex_local", lastOutputBytes: 100_000 }], |
| enableRealtimeUpdates: false, |
| logReadLimitBytes: 64_000, |
| }); |
| return null; |
| } |
|
|
| const container = document.createElement("div"); |
| document.body.appendChild(container); |
| const root = createRoot(container); |
|
|
| await act(async () => { |
| root.render(<Harness />); |
| await Promise.resolve(); |
| }); |
|
|
| expect(logMock).toHaveBeenCalledWith("run-1", 36_000, 64_000); |
|
|
| act(() => { |
| root.unmount(); |
| }); |
| container.remove(); |
| }); |
|
|
| it("rebuilds only the transcript for the run that receives live output", async () => { |
| function Harness() { |
| useLiveRunTranscripts({ |
| companyId: "company-1", |
| runs: [ |
| { id: "run-1", status: "running", adapterType: "codex_local" }, |
| { id: "run-2", status: "running", adapterType: "codex_local" }, |
| ], |
| }); |
| return null; |
| } |
|
|
| const container = document.createElement("div"); |
| document.body.appendChild(container); |
| const root = createRoot(container); |
|
|
| await act(async () => { |
| root.render(<Harness />); |
| await Promise.resolve(); |
| await Promise.resolve(); |
| }); |
|
|
| expect(FakeWebSocket.instances).toHaveLength(1); |
| expect(buildTranscriptMock).toHaveBeenCalledTimes(2); |
| buildTranscriptMock.mockClear(); |
|
|
| await act(async () => { |
| FakeWebSocket.instances[0]!.onmessage?.( |
| new MessageEvent("message", { |
| data: JSON.stringify({ |
| companyId: "company-1", |
| type: "heartbeat.run.log", |
| createdAt: "2026-04-20T00:00:00.000Z", |
| payload: { |
| runId: "run-1", |
| ts: "2026-04-20T00:00:00.000Z", |
| stream: "stdout", |
| chunk: "hello from run 1\n", |
| }, |
| }), |
| }), |
| ); |
| await Promise.resolve(); |
| }); |
|
|
| expect(buildTranscriptMock).toHaveBeenCalledTimes(1); |
| expect(buildTranscriptMock).toHaveBeenCalledWith( |
| [{ ts: "2026-04-20T00:00:00.000Z", stream: "stdout", chunk: "hello from run 1\n" }], |
| null, |
| { censorUsernameInLogs: false }, |
| ); |
|
|
| act(() => { |
| root.unmount(); |
| }); |
| container.remove(); |
| }); |
| }); |
|
|