nandi/oripublic Fork 0
f5c963af3c1597c0274ce4a99705dbd92a7d1cba
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

ws.test.ts · 135 lines · 3.1 KBTypeScript Blame HistoryRaw
✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS 2434cc7 k33g 2d ago1import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2import { connectSocket } from "./ws";
3import type { ChatEvent } from "./reducer";
4
5/** A minimal scriptable WebSocket double. */
6class FakeWebSocket {
7 static OPEN = 1;
8 static instances: FakeWebSocket[] = [];
9
10 url: string;
11 readyState = 0;
12 sent: string[] = [];
13 onopen: (() => void) | null = null;
14 onmessage: ((event: { data: string }) => void) | null = null;
15 onclose: (() => void) | null = null;
16
17 constructor(url: string) {
18 this.url = url;
19 FakeWebSocket.instances.push(this);
20 }
21
22 send(data: string) {
23 this.sent.push(data);
24 }
25
26 close() {
27 this.readyState = 3;
28 this.onclose?.();
29 }
30
31 /* test helpers */
32 serverOpens() {
33 this.readyState = FakeWebSocket.OPEN;
34 this.onopen?.();
35 }
36
37 serverSends(payload: unknown) {
38 this.onmessage?.({ data: JSON.stringify(payload) });
39 }
40
41 serverDrops() {
42 this.readyState = 3;
43 this.onclose?.();
44 }
45}
46
47describe("connectSocket", () => {
48 let events: ChatEvent[];
49 const dispatch = (event: ChatEvent) => events.push(event);
50
51 beforeEach(() => {
52 events = [];
53 FakeWebSocket.instances = [];
54 vi.useFakeTimers();
55 });
56
57 afterEach(() => {
58 vi.useRealTimers();
59 });
60
61 const connect = () =>
62 connectSocket({
63 url: "ws://test/ws",
64 dispatch,
65 webSocketImpl: FakeWebSocket as unknown as typeof WebSocket,
66 initialRetryMs: 100,
67 });
68
69 it("dispatches parsed server messages", () => {
70 connect();
71 const ws = FakeWebSocket.instances[0];
72 ws.serverOpens();
73 ws.serverSends({ type: "hello", sessionId: "s1" });
74
75 expect(events).toContainEqual({
76 type: "server",
77 message: { type: "hello", sessionId: "s1" },
78 });
79 });
80
81 it("sends only while the socket is open", () => {
82 const socket = connect();
83 const ws = FakeWebSocket.instances[0];
84
85 socket.send({ type: "prompt", text: "too early" });
86 expect(ws.sent).toHaveLength(0);
87
88 ws.serverOpens();
89 socket.send({ type: "prompt", text: "hello" });
90 expect(ws.sent).toEqual([
91 JSON.stringify({ type: "prompt", text: "hello" }),
92 ]);
93 });
94
95 it("reconnects with backoff after a drop", () => {
96 connect();
97 FakeWebSocket.instances[0].serverOpens();
98 FakeWebSocket.instances[0].serverDrops();
99
100 expect(events).toContainEqual({ type: "connection", status: "offline" });
101 expect(FakeWebSocket.instances).toHaveLength(1);
102
103 vi.advanceTimersByTime(100);
104 expect(FakeWebSocket.instances).toHaveLength(2);
105
106 // second drop: the delay doubles
107 FakeWebSocket.instances[1].serverDrops();
108 vi.advanceTimersByTime(100);
109 expect(FakeWebSocket.instances).toHaveLength(2);
110 vi.advanceTimersByTime(100);
111 expect(FakeWebSocket.instances).toHaveLength(3);
112 });
113
114 it("does not reconnect after close()", () => {
115 const socket = connect();
116 FakeWebSocket.instances[0].serverOpens();
117 socket.close();
118
119 vi.advanceTimersByTime(10_000);
120 expect(FakeWebSocket.instances).toHaveLength(1);
121 });
122
123 it("survives malformed frames", () => {
124 connect();
125 const ws = FakeWebSocket.instances[0];
126 ws.serverOpens();
127 ws.onmessage?.({ data: "{not json" });
128 ws.serverSends({ type: "turn_started" });
129
130 expect(events).toContainEqual({
131 type: "server",
132 message: { type: "turn_started" },
133 });
134 });
135});