/** * Integration tests for the SSE endpoint (FD-1 / FD-2): * - Subscribing immediately delivers the current task_state snapshot (first * frame, ahead of any pending-approval replay); * - A Last-Event-ID with a mismatched/unknown epoch → sends resync_required * first; a buffer hit → replays events after it. */ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { approvalDecision, assistantText, toolCall, userText } from "@prismshadow/penguin-core"; import type { ApproveFn, OmniMessage } from "@prismshadow/penguin-core"; import type { SessionRow } from "../src/db/repos/sessions.js"; import type { RuntimeSession } from "../src/runtime/session-manager.js"; import { apiClient, createTestApp, provisionUser, waitFor } from "./helpers.js"; import type { TestApp } from "./helpers.js"; const SID = "session-2026-07-06-10-00-00-aabb0001"; interface SseFrame { event?: string; id?: string; data: string; } /** Reads the first `count` frames of an SSE response (skipping heartbeat comment lines), then cancels the stream. */ async function readSseFrames(res: Response, count: number, timeoutMs = 3000): Promise { expect(res.status).toBe(200); const reader = res.body!.getReader(); const decoder = new TextDecoder(); const frames: SseFrame[] = []; let buf = ""; const deadline = Date.now() + timeoutMs; try { while (frames.length < count) { if (Date.now() > deadline) throw new Error(`SSE read timed out (${frames.length} frames so far)`); const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); let idx: number; while ((idx = buf.indexOf("\n\n")) !== -1) { const raw = buf.slice(0, idx); buf = buf.slice(idx + 2); if (raw.startsWith(":") || raw.trim() === "") continue; // heartbeat/empty frame const frame: SseFrame = { data: "" }; for (const line of raw.split("\n")) { if (line.startsWith("event:")) frame.event = line.slice(6).trim(); else if (line.startsWith("id:")) frame.id = line.slice(3).trim(); else if (line.startsWith("data:")) frame.data += line.slice(5).trim(); } frames.push(frame); } } } finally { await reader.cancel().catch(() => {}); } return frames; } /** Fake Session that requests one approval (for the running state and approval-replay scenarios). */ function approvalFakeSession(sessionId: string): RuntimeSession { return { sessionId, toolPermission: () => "rw", generateTitle: async () => ({ title: null, usage: null }), compactability: () => "ok" as const, steer: () => false, skipReconnectWait: () => false, async *run(_input: OmniMessage[], opts: { approve: ApproveFn; signal: AbortSignal }) { const tc = toolCall({ name: "exec_command", arguments: "{}", toolCallId: "tc-sse" }); yield tc; const decision = await opts.approve(tc); yield approvalDecision(decision, "tc-sse"); yield assistantText("done"); }, async *compact() {}, }; } describe("sse-stream", () => { let t: TestApp; let cookie: string; let row: SessionRow; beforeEach(async () => { t = await createTestApp(); ({ cookie } = await provisionUser(t.app, "streamer")); row = { sessionId: SID, // streamer's own initial Project (default_project belongs to admin; others // get 404 via the index lookup). projectId: "streamer-default_project", agentId: "default_agent", modelId: "m1", provider: "custom", workspace: "/tmp/w", approvalMode: "always-ask", title: null, createdAt: new Date().toISOString(), }; t.deps.sessionsRepo.insert(row); }); afterEach(async () => { await t.cleanup(); }); const getStream = (headers: Record = {}) => t.app.request(`/api/sessions/${SID}/stream`, { headers: { cookie, ...headers } }); it("FD-1: a new subscription's first frame is always the task_state snapshot (idle)", async () => { const frames = await readSseFrames(await getStream(), 1); expect(frames[0]!.event).toBe("server_event"); expect(JSON.parse(frames[0]!.data)).toEqual({ type: "task_state", state: "idle", queued: 0 }); expect(frames[0]!.id).toMatch(/^[0-9a-f]{8}-\d+$/); // FD-2: opaque string id }); it("FD-1: subscribing while running receives task_state: running, then pending approvals are replayed", async () => { t.deps.manager.adopt(row, approvalFakeSession(SID)); await t.deps.manager.startTask(SID, [userText("go")]); await waitFor(() => t.deps.manager.pendingApprovalCount(SID) === 1); const frames = await readSseFrames(await getStream(), 2); expect(JSON.parse(frames[0]!.data)).toEqual({ type: "task_state", state: "running", queued: 0, }); const approval = JSON.parse(frames[1]!.data) as { type: string; toolCall: { payload: { tool_call_id: string } }; }; expect(approval.type).toBe("approval_request"); expect(approval.toolCall.payload.tool_call_id).toBe("tc-sse"); t.deps.manager.abortTask(SID); await waitFor(() => t.deps.manager.statusOf(SID) === "idle"); }); it("FD-2: mismatched Last-Event-ID epoch → resync_required first, then the task_state snapshot", async () => { t.deps.channels.get(SID).publish(userText("old event")); const frames = await readSseFrames( await getStream({ "Last-Event-ID": "deadbeef-1" }), // guaranteed to differ from the current channel epoch 2, ); expect(JSON.parse(frames[0]!.data)).toEqual({ type: "resync_required" }); expect(JSON.parse(frames[1]!.data)).toEqual({ type: "task_state", state: "idle", queued: 0 }); }); it("FD-2: same-epoch Last-Event-ID hitting the buffer → replays later events, then the task_state snapshot", async () => { const channel = t.deps.channels.get(SID); const first = channel.publish(userText("m1")); channel.publish(userText("m2")); const frames = await readSseFrames(await getStream({ "Last-Event-ID": first.id }), 2); expect(frames[0]!.event).toBeUndefined(); // replayed OmniMessage expect((JSON.parse(frames[0]!.data) as { payload: { text: string } }).payload.text).toBe("m2"); expect(JSON.parse(frames[1]!.data)).toEqual({ type: "task_state", state: "idle", queued: 0 }); }); });