feat(FN-1145): harden dashboard SSE streams with replay and reconnect
- Add a shared SSE event buffer utility and wire it into planning, subtask, mission interview, and task stream routes - Support Last-Event-ID replay semantics and robust event serialization so clients can recover missed stream events - Add a resilient client-side reconnect wrapper and surface reconnecting state in planning, subtask breakdown, and mission interview modals - Expand test coverage across API routes, SSE buffering behavior, reconnect handling, and mission/planning stream flows
This commit is contained in:
73
packages/dashboard/src/subtask-breakdown.test.ts
Normal file
73
packages/dashboard/src/subtask-breakdown.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
__resetSubtaskBreakdownState,
|
||||
subtaskStreamManager,
|
||||
} from "./subtask-breakdown.js";
|
||||
|
||||
describe("subtask-breakdown stream buffering", () => {
|
||||
beforeEach(() => {
|
||||
__resetSubtaskBreakdownState();
|
||||
});
|
||||
|
||||
it("buffers broadcast events and forwards ids to subscribers", () => {
|
||||
const sessionId = "subtask-session-1";
|
||||
const callback = vi.fn();
|
||||
|
||||
const unsubscribe = subtaskStreamManager.subscribe(sessionId, callback);
|
||||
|
||||
const firstId = subtaskStreamManager.broadcast(sessionId, {
|
||||
type: "thinking",
|
||||
data: "delta-1",
|
||||
});
|
||||
const secondId = subtaskStreamManager.broadcast(sessionId, {
|
||||
type: "subtasks",
|
||||
data: [
|
||||
{
|
||||
id: "subtask-1",
|
||||
title: "Title",
|
||||
description: "Description",
|
||||
suggestedSize: "S",
|
||||
dependsOn: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(firstId).toBe(1);
|
||||
expect(secondId).toBe(2);
|
||||
expect(callback).toHaveBeenNthCalledWith(1, { type: "thinking", data: "delta-1" }, 1);
|
||||
expect(callback).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: "subtasks" }), 2);
|
||||
|
||||
const buffered = subtaskStreamManager.getBufferedEvents(sessionId, 1);
|
||||
expect(buffered).toHaveLength(1);
|
||||
expect(buffered[0]).toMatchObject({ id: 2, event: "subtasks" });
|
||||
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it("buffers complete events without subscribers", () => {
|
||||
const sessionId = "subtask-session-2";
|
||||
|
||||
const eventId = subtaskStreamManager.broadcast(sessionId, { type: "complete" });
|
||||
|
||||
expect(eventId).toBe(1);
|
||||
expect(subtaskStreamManager.getBufferedEvents(sessionId, 0)).toEqual([
|
||||
{ id: 1, event: "complete", data: "{}" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("clears subscriptions and buffered events on cleanupSession", () => {
|
||||
const sessionId = "subtask-session-3";
|
||||
const callback = vi.fn();
|
||||
|
||||
subtaskStreamManager.subscribe(sessionId, callback);
|
||||
subtaskStreamManager.broadcast(sessionId, { type: "thinking", data: "delta" });
|
||||
|
||||
expect(subtaskStreamManager.getBufferedEvents(sessionId, 0)).toHaveLength(1);
|
||||
|
||||
subtaskStreamManager.cleanupSession(sessionId);
|
||||
|
||||
expect(subtaskStreamManager.getBufferedEvents(sessionId, 0)).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user