feat(server): stream replayable harness events

This commit is contained in:
Wenxin Duan
2026-09-07 16:43:14 +08:00
parent 5e28e74ca1
commit acc5e26b1c
9 changed files with 314 additions and 6 deletions
+26
View File
@@ -0,0 +1,26 @@
import { describe, expect, it, vi } from "vitest";
import { buildApp } from "../src/app.js";
describe("shutdown lifecycle", () => {
it("closes SSE before disposing all sessions", async () => {
const order: string[] = [];
const app = buildApp({
sessionRegistry: {
create: vi.fn(), get: vi.fn(), delete: vi.fn(),
disposeAll: () => { order.push("sessions"); return Promise.resolve(); },
},
skillCatalog: { list: () => [] },
modelStore: {} as never,
attachmentStore: {} as never,
approvalBroker: {} as never,
sseRegistry: {
add: vi.fn(), closeForSession: vi.fn(),
closeAll: () => { order.push("sse"); },
},
});
await app.close();
expect(order).toEqual(["sse", "sessions"]);
});
});
+84
View File
@@ -0,0 +1,84 @@
import { EventEmitter } from "node:events";
import { afterEach, describe, expect, it, vi } from "vitest";
import { InMemoryEventStore } from "@agent-studio/harness";
import * as sseModule from "../src/sse/sse-connection.js";
class FakeResponse extends EventEmitter {
chunks: string[] = [];
ended = false;
shouldBackpressure = false;
write(chunk: string): boolean {
this.chunks.push(chunk);
return !this.shouldBackpressure;
}
end(): void {
this.ended = true;
this.emit("close");
}
}
afterEach(() => vi.useRealTimers());
describe("SseConnection", () => {
it("replays after Last-Event-ID and gives it precedence over sequence", () => {
const store = new InMemoryEventStore();
const first = store.append("ses_1", { type: "session.started", payload: { cwd: "." } });
const second = store.append("ses_1", { type: "run.started", runId: "run_1", payload: { modelId: "demo" } });
const third = store.append("ses_1", { type: "assistant.delta", runId: "run_1", payload: { messageId: "msg_1", delta: "hi" } });
const Response = Reflect.get(sseModule, "SseConnection") as unknown as new (options: Record<string, unknown>) => { start(): void; close(): void };
expect(Response, "SseConnection must be exported").toBeDefined();
const response = new FakeResponse();
const connection = new Response({ sessionId: "ses_1", eventStore: store, response, cursor: { eventId: second.eventId, sequence: first.sequence } });
connection.start();
const output = response.chunks.join("");
expect(output).toContain(`id: ${third.eventId}`);
expect(output).not.toContain(`id: ${first.eventId}`);
connection.close();
});
it("emits reset for a retained-history gap and streams live events", () => {
const store = new InMemoryEventStore(1);
store.append("ses_1", { type: "session.started", payload: { cwd: "." } });
store.append("ses_1", { type: "run.started", runId: "run_1", payload: { modelId: "demo" } });
const response = new FakeResponse();
const Response = Reflect.get(sseModule, "SseConnection") as unknown as new (options: Record<string, unknown>) => { start(): void; close(): void };
const connection = new Response({ sessionId: "ses_1", eventStore: store, response, cursor: { sequence: 0 } });
connection.start();
store.append("ses_1", { type: "assistant.delta", runId: "run_1", payload: { messageId: "msg_1", delta: "live" } });
const output = response.chunks.join("");
expect(output).toContain("event: stream.reset");
expect(output).toContain('"delta":"live"');
connection.close();
});
it("clears its subscriber and heartbeat when closed", () => {
vi.useFakeTimers();
const store = new InMemoryEventStore() as InMemoryEventStore & { listenerCount(sessionId: string): number };
const response = new FakeResponse();
const Response = Reflect.get(sseModule, "SseConnection") as unknown as new (options: Record<string, unknown>) => { start(): void; close(): void };
const connection = new Response({ sessionId: "ses_1", eventStore: store, response, heartbeatMs: 15_000 });
connection.start();
expect(store.listenerCount("ses_1")).toBe(1);
connection.close();
expect(store.listenerCount("ses_1")).toBe(0);
expect(vi.getTimerCount()).toBe(0);
});
it("closes a persistently backpressured client at the configured bound", () => {
const store = new InMemoryEventStore();
const response = new FakeResponse();
response.shouldBackpressure = true;
const Response = Reflect.get(sseModule, "SseConnection") as unknown as new (options: Record<string, unknown>) => { start(): void };
const connection = new Response({ sessionId: "ses_1", eventStore: store, response, maxQueuedBytes: 20 });
connection.start();
store.append("ses_1", { type: "assistant.delta", runId: "run_1", payload: { messageId: "msg_1", delta: "a long live chunk" } });
expect(response.ended).toBe(true);
});
});