86 lines
2.8 KiB
TypeScript
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);
|
|
});
|