import { describe, test, expect } from "bun:test";
import { get } from "svelte/store";
import { parseServerMsgV2 } from "./protocol-v2";
import { StreamingClientV2, WINDOW_LIMIT } from "./streaming-v2";
// Mock WebSocket: this suite only needs the socket the client writes its requests to.
class MockWebSocket {
public static readonly OPEN = 1;
public static instances: MockWebSocket[] = [];
public sent: string[] = [];
public readyState: number = MockWebSocket.OPEN;
public url: string;
public onopen: (() => void) | null = null;
public onmessage: ((event: { data: string }) => void) | null = null;
public onerror: (() => void) | null = null;
public onclose: (() => void) | null = null;
constructor(url: string) {
this.url = url;
MockWebSocket.instances.push(this);
}
send(data: string) {
this.sent.push(data);
}
close() {}
}
(globalThis as any).WebSocket = MockWebSocket;
/**
* The same frames the daemon's own suite pins (`klbr-ipc/src/v2.rs`, `window_frames`), replayed through
* the parser this console uses. Two implementations of one contract drift in silence otherwise: a renamed
* field here is an empty session there, with nothing red on either side.
*/
describe("window frames shared with the daemon", () => {
const frames = {
started: '{"type":"sync_started","session_id":"default","from_seq":26,"to_seq":31,"earliest_seq":1,"fold_seq":25}',
completedNoPage: '{"type":"sync_completed","session_id":"default","latest_seq":31}',
completedWithFloor: '{"type":"sync_completed","session_id":"default","latest_seq":31,"earliest_seq":1}',
event: '{"type":"event","record":{"seq":26,"session_id":"default","lane":"input","payload":{"source":"operator","text":"turn 361"},"created_ms":1010}}',
};
test("the daemon's range, floor and fold are read as the console reads them", () => {
const started = parseServerMsgV2(frames.started);
expect(started.type).toBe("sync_started");
if (started.type !== "sync_started") throw new Error("unreachable");
expect(started.from_seq).toBe(26);
expect(started.to_seq).toBe(31);
expect(started.earliest_seq).toBe(1);
expect(started.fold_seq).toBe(25);
const bare = parseServerMsgV2(frames.completedNoPage);
if (bare.type !== "sync_completed") throw new Error("unreachable");
expect(bare.earliest_seq).toBeUndefined();
const floored = parseServerMsgV2(frames.completedWithFloor);
if (floored.type !== "sync_completed") throw new Error("unreachable");
expect(floored.earliest_seq).toBe(1);
});
test("a client fed those frames holds a window it can page and knows where it starts", () => {
const sessionId = "daemon-frames-" + Math.random().toString(36).slice(2, 7);
const client = new StreamingClientV2("ws://test", sessionId);
client.handleServerMsg({ type: "sync_started", session_id: sessionId, from_seq: 26, to_seq: 31, earliest_seq: 1, fold_seq: 25 });
client.handleServerMsg(parseServerMsgV2(frames.event));
client.handleServerMsg(parseServerMsgV2(frames.completedWithFloor));
const conn = get(client.connectionStore);
// The floor is under the window, so there is history to fetch; the fold is above nothing the console
// holds, so the window it has does start at the session's fold.
expect(conn.hasEarlier).toBe(true);
expect(conn.foldSeq).toBe(25);
// The replay's range ends at the newest record, so the live cursor does too: everything the daemon
// announced is what the console was sent.
expect(conn.lastSeenSeq).toBe(31);
expect(get(client.timelineStore).map((item) => item.seq)).toEqual([26]);
});
test("the console's own requests are the shapes the daemon parses", () => {
const sessionId = "daemon-requests-" + Math.random().toString(36).slice(2, 7);
const client = new StreamingClientV2("ws://test", sessionId, "daemon-requests");
client.connect();
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1];
socket.readyState = MockWebSocket.OPEN;
socket.onopen?.();
expect(JSON.parse(socket.sent[0] ?? "{}")).toEqual({ type: "subscribe", session_id: sessionId, after_seq: 0, limit: WINDOW_LIMIT });
});
});