import { describe, expect, test } from "bun:test"; import { applyTurnEvent, createAskFabroAdapter, } from "./ask-fabro-runtime"; import type { SessionStreamEvent } from "./session-stream"; function event(name: string, properties: Record): SessionStreamEvent { return { seq: 0, event: { event: name, properties }, } as unknown as SessionStreamEvent; } function flattenedEvent( name: string, properties: Record, ): SessionStreamEvent { return { seq: 0, id: "evt_1", ts: "2026-05-22T16:25:34.940200Z", run_id: "run_1", event: name, properties, } as unknown as SessionStreamEvent; } describe("applyTurnEvent", () => { test("appends assistant deltas from flattened SSE event envelopes", () => { const acc = { activeTextIndex: null, parts: [], toolCallIndex: new Map(), } as Parameters[0]; expect( applyTurnEvent( acc, flattenedEvent("run.session.assistant_delta", { delta: "Hello" }), ), ).toBe(true); expect(acc.parts).toEqual([{ type: "text", text: "Hello" }]); }); test("appends assistant deltas into a single streaming text part", () => { const acc = { activeTextIndex: null, parts: [], toolCallIndex: new Map(), } as Parameters[0]; expect( applyTurnEvent(acc, event("run.session.assistant_delta", { delta: "Hel" })), ).toBe(true); expect( applyTurnEvent(acc, event("run.session.assistant_delta", { delta: "lo" })), ).toBe(true); expect(acc.parts).toEqual([{ type: "text", text: "Hello" }]); }); test("inserts a tool-call part and later attaches its result", () => { const acc = { activeTextIndex: null, parts: [], toolCallIndex: new Map(), } as Parameters[0]; expect( applyTurnEvent( acc, event("run.session.tool_call.started", { tool_call_id: "tc_1", tool_name: "fabro_run_events", arguments: { run_id: "r" }, }), ), ).toBe(true); expect(acc.parts).toHaveLength(1); const callPart = acc.parts[0]; expect(callPart?.type).toBe("tool-call"); if (callPart?.type !== "tool-call") throw new Error("expected tool-call"); expect(callPart.toolName).toBe("fabro_run_events"); expect(callPart.toolCallId).toBe("tc_1"); expect(callPart.args).toEqual({ run_id: "r" }); expect( applyTurnEvent( acc, event("run.session.tool_call.completed", { tool_call_id: "tc_1", tool_name: "fabro_run_events", output: { events: [] }, is_error: false, }), ), ).toBe(true); const completed = acc.parts[0]; expect(completed?.type).toBe("tool-call"); if (completed?.type !== "tool-call") throw new Error("expected tool-call"); expect(completed.result).toEqual({ events: [] }); }); test("a text segment after a tool call starts a fresh text part", () => { const acc = { activeTextIndex: null, parts: [], toolCallIndex: new Map(), } as Parameters[0]; applyTurnEvent(acc, event("run.session.assistant_delta", { delta: "Intro" })); applyTurnEvent(acc, event("run.session.assistant_message", { text: "Intro" })); applyTurnEvent( acc, event("run.session.tool_call.started", { tool_call_id: "tc_a", tool_name: "fabro_run_events", arguments: {}, }), ); applyTurnEvent(acc, event("run.session.assistant_delta", { delta: "After" })); expect(acc.parts).toHaveLength(3); expect(acc.parts[0]).toMatchObject({ type: "text", text: "Intro" }); expect(acc.parts[1]?.type).toBe("tool-call"); expect(acc.parts[2]).toMatchObject({ type: "text", text: "After" }); }); }); describe("createAskFabroAdapter", () => { function ramSessionStore(initial: Record = {}) { const store: Record = { ...initial }; return { store, persisted: { read: (runId: string) => store[runId] ?? null, write: (runId: string, sessionId: string) => { store[runId] = sessionId; }, clear: (runId: string) => { delete store[runId]; }, }, }; } type StreamArgs = Parameters< NonNullable[0]["streamSessionTurnImpl"]> >[0]; function userMessages(text: string) { return [ { role: "user", content: [{ type: "text", text }], }, ]; } type RunArgs = Parameters["run"]>[0]; function fakeRunArgs( abortSignal: AbortSignal, messages: ReturnType, ): RunArgs { return { messages, abortSignal, runConfig: {}, context: { tools: [] } as unknown as RunArgs["context"], unstable_getMessage: () => ({}) as never, } as RunArgs; } test("creates a session lazily on the first turn and persists its id", async () => { let createCount = 0; let lastCreateBody: { title?: string; model?: string } | null = null; const { store, persisted } = ramSessionStore(); const adapter = createAskFabroAdapter({ runId: "r_1", defaultModel: "claude-haiku-4-5", persistedSession: persisted, createSession: async (_runId, body) => { createCount += 1; lastCreateBody = body; return { id: "ses_new" }; }, streamSessionTurnImpl: async (args: StreamArgs) => { args.onEvent( event("run.session.assistant_delta", { delta: "Hello" }), ); return { turnId: "turn_1" }; }, }); const ctl = new AbortController(); const result = adapter.run(fakeRunArgs(ctl.signal, userMessages("Say hi"))); if (!(Symbol.asyncIterator in result)) { throw new Error("expected async iterator"); } for await (const _ of result) { // drain } expect(createCount).toBe(1); expect(lastCreateBody).toEqual({ title: "Ask Fabro", model: "claude-haiku-4-5" }); expect(store["r_1"]).toBe("ses_new"); }); test("reuses a cached session id across runs (no second createSession call)", async () => { let createCount = 0; const { persisted } = ramSessionStore({ r_2: "ses_cached" }); const submittedSessionIds: string[] = []; const adapter = createAskFabroAdapter({ runId: "r_2", persistedSession: persisted, createSession: async () => { createCount += 1; return { id: "ses_should_not_be_called" }; }, streamSessionTurnImpl: async (args: StreamArgs) => { submittedSessionIds.push(args.sessionId); return { turnId: "turn_1" }; }, }); const ctl = new AbortController(); const result = adapter.run(fakeRunArgs(ctl.signal, userMessages("hi"))); if (!(Symbol.asyncIterator in result)) { throw new Error("expected async iterator"); } for await (const _ of result) { // drain } expect(createCount).toBe(0); expect(submittedSessionIds).toEqual(["ses_cached"]); }); });