1import { describe, it, expect } from "vitest";
2import {
3 decodeClientFrame,
4 encodeServerFrame,
5 encodeBinaryUpdate,
6 encodeBinaryAwareness,
7 StrikeTracker,
8 WS_CLOSE,
9 MAX_FRAME_BYTES,
10 type DecodedFrame,
11} from "../src/protocol.js";
12import { BINARY_KIND, type ServerFrame } from "@factory/shared/realtime/frames";
13
14function text(o: unknown): string {
15 return JSON.stringify(o);
16}
17
18describe("decodeClientFrame — text frames", () => {
19 it("accepts a sync.request (full sync — no stateVector)", () => {
20 const r = decodeClientFrame(text({ type: "sync.request" }));
21 expect(r).toEqual({ ok: true, frame: { kind: "text", frame: { type: "sync.request" } } });
22 });
23
24 it("accepts a sync.request with base64 stateVector + lastSeq (resume paths)", () => {
25 const r = decodeClientFrame(text({ type: "sync.request", stateVector: "AAAA", lastSeq: 42 }));
26 expect(r).toMatchObject({
27 ok: true,
28 frame: { kind: "text", frame: { type: "sync.request", stateVector: "AAAA", lastSeq: 42 } },
29 });
30 });
31
32 it("accepts a ping with ts (and optional lastSeq)", () => {
33 const r = decodeClientFrame(text({ type: "ping", ts: 1700000000000 }));
34 expect(r).toMatchObject({
35 ok: true,
36 frame: { kind: "text", frame: { type: "ping", ts: 1700000000000 } },
37 });
38 const r2 = decodeClientFrame(text({ type: "ping", ts: 1, lastSeq: 7 }));
39 expect(r2).toMatchObject({ ok: true, frame: { kind: "text", frame: { lastSeq: 7 } } });
40 });
41
42 it("accepts an outbox.pending with a non-negative integer count", () => {
43 const r = decodeClientFrame(text({ type: "outbox.pending", count: 3 }));
44 expect(r).toMatchObject({
45 ok: true,
46 frame: { kind: "text", frame: { type: "outbox.pending", count: 3 } },
47 });
48 });
49
50 it("accepts a subscribe for a page channel (control room, TASK-0092 6b)", () => {
51 const r = decodeClientFrame(text({ type: "subscribe", channel: "page:pg_1" }));
52 expect(r).toEqual({
53 ok: true,
54 frame: { kind: "text", frame: { type: "subscribe", channel: "page:pg_1" } },
55 });
56 });
57
58 it("accepts a subscribe for a workspace channel (accepted, no frames ride it yet)", () => {
59 const r = decodeClientFrame(text({ type: "subscribe", channel: "workspace:wsp_1" }));
60 expect(r).toMatchObject({
61 ok: true,
62 frame: { kind: "text", frame: { type: "subscribe", channel: "workspace:wsp_1" } },
63 });
64 });
65
66 it("accepts an unsubscribe for a page channel", () => {
67 const r = decodeClientFrame(text({ type: "unsubscribe", channel: "page:pg_1" }));
68 expect(r).toMatchObject({
69 ok: true,
70 frame: { kind: "text", frame: { type: "unsubscribe", channel: "page:pg_1" } },
71 });
72 });
73
74 it("rejects a subscribe with an unknown channel kind as invalid_frame (not unknown_frame)", () => {
75
76 expect(decodeClientFrame(text({ type: "subscribe", channel: "bogus:pg_1" }))).toEqual({
77 ok: false,
78 code: "invalid_frame",
79 });
80 });
81
82 it("rejects a subscribe with an empty id as invalid_frame", () => {
83 expect(decodeClientFrame(text({ type: "subscribe", channel: "page:" }))).toEqual({
84 ok: false,
85 code: "invalid_frame",
86 });
87 });
88
89 it("accepts a presence frame (cursor move, TASK-0093) and a presence-leave", () => {
90 const r = decodeClientFrame(text({ type: "presence", cursor: { blockId: "b3", offset: 4 } }));
91 expect(r).toEqual({
92 ok: true,
93 frame: { kind: "text", frame: { type: "presence", cursor: { blockId: "b3", offset: 4 } } },
94 });
95 expect(decodeClientFrame(text({ type: "presence-leave" }))).toEqual({
96 ok: true,
97 frame: { kind: "text", frame: { type: "presence-leave" } },
98 });
99 });
100
101 it("rejects a presence frame with a bad payload as invalid_frame (not unknown_frame)", () => {
102
103
104
105 expect(
106 decodeClientFrame(text({ type: "presence", cursor: { blockId: "", offset: 4 } })),
107 ).toEqual({ ok: false, code: "invalid_frame" });
108 });
109
110 it("strips a client-supplied identity from a presence frame (Zod non-strict: unknown keys are dropped)", () => {
111
112
113
114
115 const r = decodeClientFrame(text({ type: "presence", user: { id: "u-evil" }, cursor: null }));
116 expect(r.ok).toBe(true);
117 if (!r.ok) return;
118 expect(r.frame.frame).toEqual({ type: "presence", cursor: null });
119 });
120
121 it("rejects non-JSON input as invalid_json", () => {
122 expect(decodeClientFrame("this is not json")).toEqual({ ok: false, code: "invalid_json" });
123 });
124
125 it("rejects a wholly-unknown type as unknown_frame", () => {
126 expect(decodeClientFrame(text({ type: "bogus", docId: "d1" }))).toEqual({
127 ok: false,
128 code: "unknown_frame",
129 });
130 });
131
132 it("rejects a known type with a bad payload as invalid_frame", () => {
133
134 expect(decodeClientFrame(text({ type: "ping" }))).toEqual({ ok: false, code: "invalid_frame" });
135 });
136
137 it("rejects a sync.request with a negative lastSeq as invalid_frame", () => {
138 expect(decodeClientFrame(text({ type: "sync.request", lastSeq: -1 }))).toEqual({
139 ok: false,
140 code: "invalid_frame",
141 });
142 });
143
144 it("rejects an outbox.pending with a non-integer count as invalid_frame", () => {
145 expect(decodeClientFrame(text({ type: "outbox.pending", count: 1.5 }))).toEqual({
146 ok: false,
147 code: "invalid_frame",
148 });
149 });
150});
151
152describe("decodeClientFrame — binary frames", () => {
153 it("accepts a 0x01 doc update (payload = the raw Yjs bytes after the prefix)", () => {
154 const bytes = new Uint8Array([0x01, 0x00, 0x49, 0x7c]);
155 const r = decodeClientFrame(bytes) as { ok: true; frame: DecodedFrame };
156 expect(r.ok).toBe(true);
157 if (r.frame.kind === "binary") {
158 expect(r.frame.binary).toBe("update");
159 expect(Array.from(r.frame.bytes)).toEqual([0x00, 0x49, 0x7c]);
160 } else {
161 throw new Error("expected a binary frame");
162 }
163 });
164
165 it("accepts a 0x02 awareness update", () => {
166 const r = decodeClientFrame(new Uint8Array([0x02, 0x01]));
167 expect(r).toMatchObject({ ok: true });
168 });
169
170 it("rejects an empty binary frame as bad_binary_kind", () => {
171 expect(decodeClientFrame(new Uint8Array([]))).toEqual({ ok: false, code: "bad_binary_kind" });
172 });
173
174 it("rejects an unknown binary kind (0x7f) as bad_binary_kind", () => {
175 expect(decodeClientFrame(new Uint8Array([0x7f, 0x01]))).toEqual({
176 ok: false,
177 code: "bad_binary_kind",
178 });
179 });
180});
181
182describe("encodeServerFrame / encodeBinaryUpdate / encodeBinaryAwareness", () => {
183 it("serializes a server frame to JSON", () => {
184 const frame: ServerFrame = { type: "error", code: "forbidden", message: "no edit permission" };
185 expect(JSON.parse(encodeServerFrame(frame)) as ServerFrame).toEqual(frame);
186 });
187
188 it("prefixes a binary update with 0x01", () => {
189 const payload = new Uint8Array([0xaa, 0xbb]);
190 const out = encodeBinaryUpdate(payload);
191 expect(out[0]).toBe(BINARY_KIND.Update);
192 expect(Array.from(out.subarray(1))).toEqual([0xaa, 0xbb]);
193 });
194
195 it("prefixes a binary awareness update with 0x02", () => {
196 const out = encodeBinaryAwareness(new Uint8Array([0x01]));
197 expect(out[0]).toBe(BINARY_KIND.Awareness);
198 expect(out.byteLength).toBe(2);
199 });
200});
201
202describe("StrikeTracker (the 10-strike policy, R3)", () => {
203 function clock() {
204 let t = 0;
205 return { now: () => t, advance: (ms: number) => (t += ms) };
206 }
207
208 it("violates exactly at the limit within the window", () => {
209 const c = clock();
210 const tracker = new StrikeTracker({ limit: 3, windowMs: 10_000, now: c.now });
211 expect(tracker.record().violated).toBe(false);
212 expect(tracker.record().violated).toBe(false);
213 expect(tracker.record()).toEqual({ violated: true, strikes: 3 });
214 });
215
216 it("a strike that falls out of the window no longer counts", () => {
217 const c = clock();
218 const tracker = new StrikeTracker({ limit: 3, windowMs: 1000, now: c.now });
219 tracker.record();
220 c.advance(900);
221 tracker.record();
222 expect(tracker.count()).toBe(2);
223 c.advance(200);
224 const third = tracker.record();
225 expect(third.violated).toBe(false);
226 expect(third.strikes).toBe(2);
227 });
228
229 it("count reports the in-window strikes", () => {
230 const c = clock();
231 const tracker = new StrikeTracker({ limit: 10, windowMs: 10_000, now: c.now });
232 tracker.record();
233 tracker.record();
234 tracker.record();
235 expect(tracker.count()).toBe(3);
236 tracker.reset();
237 expect(tracker.count()).toBe(0);
238 });
239});
240
241describe("close codes + frame cap (frozen contract)", () => {
242 it("WS_CLOSE carries the documented 40xx values", () => {
243 expect(WS_CLOSE).toEqual({
244 ProtocolOrHeartbeat: 4000,
245 Unauthorized: 4001,
246 Forbidden: 4002,
247 RateOrPolicy: 4008,
248 PayloadTooLarge: 4009,
249 ServerShutdown: 4010,
250 });
251 });
252
253 it("the inbound frame cap is 256 KB", () => {
254 expect(MAX_FRAME_BYTES).toBe(256 * 1024);
255 });
256});
257