diff --git a/apps/fabro-web/app/lib/ask-fabro-runtime.test.ts b/apps/fabro-web/app/lib/ask-fabro-runtime.test.ts index 9b4f07788..fdb360045 100644 --- a/apps/fabro-web/app/lib/ask-fabro-runtime.test.ts +++ b/apps/fabro-web/app/lib/ask-fabro-runtime.test.ts @@ -13,7 +13,38 @@ function event(name: string, properties: Record): SessionStream } 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, diff --git a/apps/fabro-web/app/lib/ask-fabro-runtime.ts b/apps/fabro-web/app/lib/ask-fabro-runtime.ts index dce3d5795..17623ea79 100644 --- a/apps/fabro-web/app/lib/ask-fabro-runtime.ts +++ b/apps/fabro-web/app/lib/ask-fabro-runtime.ts @@ -100,17 +100,34 @@ interface NestedRunEvent { properties?: Record; } +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function eventPayload(envelope: SessionStreamEvent): { + eventName: string; + props: Record; +} { + const raw = envelope as unknown as Record; + if (typeof raw.event === "string") { + return { + eventName: raw.event, + props: isRecord(raw.properties) ? raw.properties : {}, + }; + } + + const nested = isRecord(raw.event) ? (raw.event as NestedRunEvent) : {}; + return { + eventName: nested.event ?? "", + props: isRecord(nested.properties) ? nested.properties : {}, + }; +} + export function applyTurnEvent( acc: TurnAccumulator, envelope: SessionStreamEvent, ): boolean { - // The on-wire SSE envelope is `{ seq, event: { event: "...", properties } }`, - // but the generated OpenAPI `EventEnvelope` type flattens the inner event - // fields. Cast through `unknown` to read the nested runtime shape that the - // server actually emits (matches `session-stream.test.ts`). - const nested = (envelope as unknown as { event?: NestedRunEvent }).event ?? {}; - const eventName = nested.event ?? ""; - const props: Record = nested.properties ?? {}; + const { eventName, props } = eventPayload(envelope); if (eventName === "run.session.assistant_delta") { const delta = typeof props.delta === "string" ? props.delta : ""; diff --git a/apps/fabro-web/app/lib/session-stream.test.ts b/apps/fabro-web/app/lib/session-stream.test.ts index 15b88a8db..168da8d3a 100644 --- a/apps/fabro-web/app/lib/session-stream.test.ts +++ b/apps/fabro-web/app/lib/session-stream.test.ts @@ -41,7 +41,7 @@ describe("session stream helpers", () => { streamResponse( [ "id: 3\nevent: run.session.turn.started\n", - 'data: {"seq":3,"event":{"event":"run.session.turn.started","properties":{"turn_id":"turn_1"}}}\n\n', + 'data: {"seq":3,"id":"evt_1","ts":"2026-05-22T16:25:34.940200Z","run_id":"run_1","event":"run.session.turn.started","properties":{"turn_id":"turn_1"}}\n\n', ], 200, { "x-fabro-turn-id": "turn_1" }, @@ -65,7 +65,7 @@ describe("session stream helpers", () => { }); expect(events).toHaveLength(1); expect(events[0]?.seq).toBe(3); - expect(events[0]?.event.event).toBe("run.session.turn.started"); + expect(events[0]?.event).toBe("run.session.turn.started"); }); test("attaches to session events from a run sequence", async () => { @@ -73,7 +73,7 @@ describe("session stream helpers", () => { const fetchMock = mock(() => Promise.resolve( streamResponse([ - 'data: {"seq":7,"event":{"event":"run.session.assistant_message","properties":{}}}\n\n', + 'data: {"seq":7,"id":"evt_2","ts":"2026-05-22T16:25:34.940200Z","run_id":"run_1","event":"run.session.assistant_message","properties":{}}\n\n', ]), ), ); @@ -96,7 +96,7 @@ describe("session stream helpers", () => { const fetchMock = mock(() => Promise.resolve( streamResponse([ - 'data: {"seq":8,"event":{"event":"run.session.assistant_message","properties":{}}}\r\n\r\n', + 'data: {"seq":8,"id":"evt_3","ts":"2026-05-22T16:25:34.940200Z","run_id":"run_1","event":"run.session.assistant_message","properties":{}}\r\n\r\n', ]), ), );