Files
ThothII/frontend/src/stream/useSessionStream.test.tsx
T

86 lines
2.8 KiB
TypeScript

import { renderHook } from "@testing-library/react";
import { act } from "react";
import { FakeEventSource } from "../test/fakeEventSource";
import { useSessionStream } from "./useSessionStream";
import { useSessionStore } from "../store/sessionStore";
beforeEach(() => {
FakeEventSource.instances = [];
(globalThis as any).EventSource = FakeEventSource;
useSessionStore.getState().resetSession();
});
test("opens an EventSource and feeds NAMED events to the store", () => {
renderHook(() => useSessionStream("s1"));
const es = FakeEventSource.instances[0];
expect(es.url).toBe("http://localhost:8787/sessions/s1/events");
// Backend sends `event: ui_request` (named) — drive the addEventListener path
// that production relies on, not the unnamed onmessage fallback.
act(() =>
es.emitNamed("ui_request", { type: "ui_request", ui_request: { id: "u1", widget: "select" } })
);
expect(useSessionStore.getState().pendingWidget?.id).toBe("u1");
});
test("feeds named activity_delta events to the thinking activity timeline", () => {
renderHook(() => useSessionStream("s1"));
const es = FakeEventSource.instances[0];
act(() =>
es.emitNamed("activity_delta", { type: "activity_delta", text: "Analizzo le ambiguità" })
);
expect(useSessionStore.getState().activityLog.at(-1)).toMatchObject({
kind: "thinking",
text: "Analizzo le ambiguità",
});
});
test("feeds named activity_event events to the tool activity timeline", () => {
renderHook(() => useSessionStream("s1"));
const es = FakeEventSource.instances[0];
act(() =>
es.emitNamed("activity_event", {
type: "activity_event",
activity: { kind: "tool", toolCallId: "t1", toolName: "bash", status: "running" },
})
);
expect(useSessionStore.getState().activityLog).toContainEqual({
kind: "tool",
phase: null,
text: "bash",
toolCallId: "t1",
status: "running",
});
});
test("also feeds UNNAMED (default message) events to the store", () => {
renderHook(() => useSessionStream("s1"));
const es = FakeEventSource.instances[0];
act(() => es.emit({ type: "text_delta", text: "hello" }));
expect(useSessionStore.getState().transcript.at(-1)?.text).toBe("hello");
});
test("closes the stream on unmount", () => {
const { unmount } = renderHook(() => useSessionStream("s1"));
const es = FakeEventSource.instances[0];
unmount();
expect(es.closed).toBe(true);
});
test("reconnects the same session when the generation changes", () => {
const { rerender } = renderHook(
({ generation }) => useSessionStream("s1", generation),
{ initialProps: { generation: 0 } },
);
const first = FakeEventSource.instances[0];
rerender({ generation: 1 });
expect(first.closed).toBe(true);
expect(FakeEventSource.instances).toHaveLength(2);
expect(FakeEventSource.instances[1].url).toBe(first.url);
});