86 lines
2.9 KiB
TypeScript
86 lines
2.9 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import * as harness from "../src/index.js";
|
|
|
|
type Store = {
|
|
append(sessionId: string, input: Record<string, unknown>): { eventId: string; sequence: number; type: string };
|
|
listAfter(sessionId: string, cursor?: { eventId?: string; sequence?: number }): {
|
|
events: Array<{ eventId: string; sequence: number; type: string }>;
|
|
gap: boolean;
|
|
oldestSequence?: number;
|
|
latestSequence?: number;
|
|
};
|
|
subscribe(sessionId: string, listener: (event: unknown) => void): () => void;
|
|
};
|
|
|
|
function createStore(limit = 1000): Store {
|
|
const Constructor = Reflect.get(harness, "InMemoryEventStore") as (new (limit?: number) => Store) | undefined;
|
|
expect(Constructor, "InMemoryEventStore must be exported").toBeDefined();
|
|
return new Constructor!(limit);
|
|
}
|
|
|
|
const started = {
|
|
type: "session.started",
|
|
payload: { cwd: "workspace/sessions/ses_a" },
|
|
};
|
|
const runStarted = {
|
|
type: "run.started",
|
|
runId: "run_1",
|
|
payload: { modelId: "fake-model" },
|
|
};
|
|
const runCompleted = {
|
|
type: "run.completed",
|
|
runId: "run_1",
|
|
payload: { messageId: "msg_1" },
|
|
};
|
|
|
|
describe("InMemoryEventStore", () => {
|
|
it("allocates monotonic sequence independently per Session", () => {
|
|
const store = createStore();
|
|
expect(store.append("ses_a", started).sequence).toBe(1);
|
|
expect(store.append("ses_a", runStarted).sequence).toBe(2);
|
|
expect(store.append("ses_b", started).sequence).toBe(1);
|
|
});
|
|
|
|
it("bounds retained history and reports an old cursor gap", () => {
|
|
const store = createStore(2);
|
|
store.append("ses_a", started);
|
|
store.append("ses_a", runStarted);
|
|
store.append("ses_a", runCompleted);
|
|
|
|
const replay = store.listAfter("ses_a", { sequence: 0 });
|
|
expect(replay.events.map((event) => event.sequence)).toEqual([2, 3]);
|
|
expect(replay).toMatchObject({ gap: true, oldestSequence: 2, latestSequence: 3 });
|
|
});
|
|
|
|
it("uses eventId before sequence when both cursors are present", () => {
|
|
const store = createStore();
|
|
const first = store.append("ses_a", started);
|
|
const second = store.append("ses_a", runStarted);
|
|
|
|
expect(store.listAfter("ses_a", { eventId: first.eventId, sequence: 99 }).events)
|
|
.toEqual([second]);
|
|
});
|
|
|
|
it("notifies active subscribers after append and supports idempotent unsubscribe", () => {
|
|
const store = createStore();
|
|
const listener = vi.fn();
|
|
const unsubscribe = store.subscribe("ses_a", listener);
|
|
|
|
const event = store.append("ses_a", started);
|
|
expect(listener).toHaveBeenCalledWith(event);
|
|
|
|
unsubscribe();
|
|
unsubscribe();
|
|
store.append("ses_a", runStarted);
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("rejects event payloads that do not match their type", () => {
|
|
const store = createStore();
|
|
expect(() => store.append("ses_a", {
|
|
type: "assistant.delta",
|
|
payload: { approvalId: "ap_1" },
|
|
})).toThrow();
|
|
});
|
|
});
|