@sylphx/lens-server 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,807 @@
1
+ /**
2
+ * @sylphx/lens-server - Server Tests
3
+ *
4
+ * Tests for Lens server operations and GraphStateManager integration.
5
+ */
6
+
7
+ import { describe, expect, it } from "bun:test";
8
+ import { entity, mutation, query, t } from "@sylphx/lens-core";
9
+ import { z } from "zod";
10
+ import { type WebSocketLike, createServer } from "./create";
11
+
12
+ // =============================================================================
13
+ // Test Fixtures
14
+ // =============================================================================
15
+
16
+ // Entities
17
+ const User = entity("User", {
18
+ id: t.id(),
19
+ name: t.string(),
20
+ email: t.string(),
21
+ bio: t.string().nullable(),
22
+ });
23
+
24
+ const Post = entity("Post", {
25
+ id: t.id(),
26
+ title: t.string(),
27
+ content: t.string(),
28
+ authorId: t.string(),
29
+ });
30
+
31
+ // Mock data
32
+ const mockUsers = [
33
+ { id: "user-1", name: "Alice", email: "alice@example.com", bio: "Developer" },
34
+ { id: "user-2", name: "Bob", email: "bob@example.com", bio: "Designer" },
35
+ ];
36
+
37
+ const mockPosts = [
38
+ { id: "post-1", title: "Hello", content: "World", authorId: "user-1" },
39
+ { id: "post-2", title: "Test", content: "Post", authorId: "user-1" },
40
+ ];
41
+
42
+ // Mock WebSocket factory
43
+ function createMockWs(): WebSocketLike & { messages: string[] } {
44
+ const messages: string[] = [];
45
+ return {
46
+ messages,
47
+ send: (data: string) => messages.push(data),
48
+ close: () => {},
49
+ onmessage: null,
50
+ onclose: null,
51
+ onerror: null,
52
+ };
53
+ }
54
+
55
+ // =============================================================================
56
+ // Test: Server Creation
57
+ // =============================================================================
58
+
59
+ describe("createServer", () => {
60
+ it("creates a server instance", () => {
61
+ const server = createServer({
62
+ entities: { User, Post },
63
+ });
64
+
65
+ expect(server).toBeDefined();
66
+ expect(typeof server.executeQuery).toBe("function");
67
+ expect(typeof server.executeMutation).toBe("function");
68
+ expect(typeof server.handleWebSocket).toBe("function");
69
+ expect(typeof server.handleRequest).toBe("function");
70
+ expect(typeof server.getStateManager).toBe("function");
71
+ });
72
+
73
+ it("throws for invalid query definition", () => {
74
+ expect(() =>
75
+ createServer({
76
+ entities: { User },
77
+ queries: {
78
+ invalidQuery: { notAQuery: true } as never,
79
+ },
80
+ }),
81
+ ).toThrow("Invalid query definition: invalidQuery");
82
+ });
83
+
84
+ it("throws for invalid mutation definition", () => {
85
+ expect(() =>
86
+ createServer({
87
+ entities: { User },
88
+ mutations: {
89
+ invalidMutation: { notAMutation: true } as never,
90
+ },
91
+ }),
92
+ ).toThrow("Invalid mutation definition: invalidMutation");
93
+ });
94
+ });
95
+
96
+ // =============================================================================
97
+ // Test: Query Execution
98
+ // =============================================================================
99
+
100
+ describe("executeQuery", () => {
101
+ it("executes a simple query", async () => {
102
+ const getUsers = query()
103
+ .returns([User])
104
+ .resolve(() => mockUsers);
105
+
106
+ const server = createServer({
107
+ entities: { User },
108
+ queries: { getUsers },
109
+ });
110
+
111
+ const result = await server.executeQuery("getUsers");
112
+ expect(result).toEqual(mockUsers);
113
+ });
114
+
115
+ it("executes a query with input", async () => {
116
+ const getUser = query()
117
+ .input(z.object({ id: z.string() }))
118
+ .returns(User)
119
+ .resolve(({ input }) => {
120
+ return mockUsers.find((u) => u.id === input.id) ?? null;
121
+ });
122
+
123
+ const server = createServer({
124
+ entities: { User },
125
+ queries: { getUser },
126
+ });
127
+
128
+ const result = await server.executeQuery("getUser", { id: "user-1" });
129
+ expect(result).toEqual(mockUsers[0]);
130
+ });
131
+
132
+ it("validates query input", async () => {
133
+ const getUser = query()
134
+ .input(z.object({ id: z.string() }))
135
+ .returns(User)
136
+ .resolve(({ input }) => mockUsers.find((u) => u.id === input.id) ?? null);
137
+
138
+ const server = createServer({
139
+ entities: { User },
140
+ queries: { getUser },
141
+ });
142
+
143
+ await expect(server.executeQuery("getUser", { id: 123 as unknown as string })).rejects.toThrow(
144
+ "Invalid input",
145
+ );
146
+ });
147
+
148
+ it("throws for unknown query", async () => {
149
+ const server = createServer({
150
+ entities: { User },
151
+ queries: {},
152
+ });
153
+
154
+ await expect(server.executeQuery("unknownQuery")).rejects.toThrow(
155
+ "Query not found: unknownQuery",
156
+ );
157
+ });
158
+ });
159
+
160
+ // =============================================================================
161
+ // Test: Mutation Execution
162
+ // =============================================================================
163
+
164
+ describe("executeMutation", () => {
165
+ it("executes a simple mutation", async () => {
166
+ const createUser = mutation()
167
+ .input(z.object({ name: z.string(), email: z.string() }))
168
+ .returns(User)
169
+ .resolve(({ input }) => ({
170
+ id: "user-new",
171
+ name: input.name,
172
+ email: input.email,
173
+ }));
174
+
175
+ const server = createServer({
176
+ entities: { User },
177
+ mutations: { createUser },
178
+ });
179
+
180
+ const result = await server.executeMutation("createUser", {
181
+ name: "Charlie",
182
+ email: "charlie@example.com",
183
+ });
184
+
185
+ expect(result).toEqual({
186
+ id: "user-new",
187
+ name: "Charlie",
188
+ email: "charlie@example.com",
189
+ });
190
+ });
191
+
192
+ it("validates mutation input", async () => {
193
+ const createUser = mutation()
194
+ .input(z.object({ name: z.string(), email: z.string().email() }))
195
+ .returns(User)
196
+ .resolve(({ input }) => ({ id: "new", ...input }));
197
+
198
+ const server = createServer({
199
+ entities: { User },
200
+ mutations: { createUser },
201
+ });
202
+
203
+ await expect(
204
+ server.executeMutation("createUser", { name: "Test", email: "invalid-email" }),
205
+ ).rejects.toThrow("Invalid input");
206
+ });
207
+
208
+ it("throws for unknown mutation", async () => {
209
+ const server = createServer({
210
+ entities: { User },
211
+ mutations: {},
212
+ });
213
+
214
+ await expect(server.executeMutation("unknownMutation", {})).rejects.toThrow(
215
+ "Mutation not found: unknownMutation",
216
+ );
217
+ });
218
+ });
219
+
220
+ // =============================================================================
221
+ // Test: WebSocket Protocol
222
+ // =============================================================================
223
+
224
+ describe("WebSocket Protocol", () => {
225
+ it("handles handshake message", () => {
226
+ const server = createServer({
227
+ entities: { User },
228
+ queries: {},
229
+ mutations: {},
230
+ version: "2.1.0",
231
+ });
232
+
233
+ const ws = createMockWs();
234
+ server.handleWebSocket(ws);
235
+
236
+ // Simulate handshake
237
+ ws.onmessage?.({ data: JSON.stringify({ type: "handshake", id: "hs-1" }) });
238
+
239
+ expect(ws.messages.length).toBe(1);
240
+ const response = JSON.parse(ws.messages[0]);
241
+ expect(response.type).toBe("handshake");
242
+ expect(response.id).toBe("hs-1");
243
+ expect(response.version).toBe("2.1.0");
244
+ });
245
+
246
+ it("handles query message", async () => {
247
+ const getUsers = query()
248
+ .returns([User])
249
+ .resolve(() => mockUsers);
250
+
251
+ const server = createServer({
252
+ entities: { User },
253
+ queries: { getUsers },
254
+ });
255
+
256
+ const ws = createMockWs();
257
+ server.handleWebSocket(ws);
258
+
259
+ // Simulate query
260
+ ws.onmessage?.({ data: JSON.stringify({ type: "query", id: "q-1", operation: "getUsers" }) });
261
+
262
+ await new Promise((r) => setTimeout(r, 20));
263
+
264
+ expect(ws.messages.length).toBe(1);
265
+ const response = JSON.parse(ws.messages[0]);
266
+ expect(response.type).toBe("result");
267
+ expect(response.id).toBe("q-1");
268
+ expect(response.data).toEqual(mockUsers);
269
+ });
270
+
271
+ it("handles mutation message", async () => {
272
+ const createUser = mutation()
273
+ .input(z.object({ name: z.string() }))
274
+ .returns(User)
275
+ .resolve(({ input }) => ({ id: "new", name: input.name, email: "" }));
276
+
277
+ const server = createServer({
278
+ entities: { User },
279
+ mutations: { createUser },
280
+ });
281
+
282
+ const ws = createMockWs();
283
+ server.handleWebSocket(ws);
284
+
285
+ // Simulate mutation
286
+ ws.onmessage?.({
287
+ data: JSON.stringify({
288
+ type: "mutation",
289
+ id: "m-1",
290
+ operation: "createUser",
291
+ input: { name: "Test" },
292
+ }),
293
+ });
294
+
295
+ await new Promise((r) => setTimeout(r, 20));
296
+
297
+ expect(ws.messages.length).toBe(1);
298
+ const response = JSON.parse(ws.messages[0]);
299
+ expect(response.type).toBe("result");
300
+ expect(response.id).toBe("m-1");
301
+ expect(response.data).toEqual({ id: "new", name: "Test", email: "" });
302
+ });
303
+
304
+ it("handles parse error", () => {
305
+ const server = createServer({
306
+ entities: { User },
307
+ });
308
+
309
+ const ws = createMockWs();
310
+ server.handleWebSocket(ws);
311
+
312
+ // Send invalid JSON
313
+ ws.onmessage?.({ data: "invalid json" });
314
+
315
+ expect(ws.messages.length).toBe(1);
316
+ const response = JSON.parse(ws.messages[0]);
317
+ expect(response.type).toBe("error");
318
+ expect(response.error.code).toBe("PARSE_ERROR");
319
+ });
320
+ });
321
+
322
+ // =============================================================================
323
+ // Test: Subscribe Protocol (Field-Level)
324
+ // =============================================================================
325
+
326
+ describe("Subscribe Protocol", () => {
327
+ it("handles subscribe message", async () => {
328
+ const getUser = query()
329
+ .input(z.object({ id: z.string() }))
330
+ .returns(User)
331
+ .resolve(({ input }) => mockUsers.find((u) => u.id === input.id) ?? null);
332
+
333
+ const server = createServer({
334
+ entities: { User },
335
+ queries: { getUser },
336
+ });
337
+
338
+ const ws = createMockWs();
339
+ server.handleWebSocket(ws);
340
+
341
+ // Subscribe to user
342
+ ws.onmessage?.({
343
+ data: JSON.stringify({
344
+ type: "subscribe",
345
+ id: "sub-1",
346
+ operation: "getUser",
347
+ input: { id: "user-1" },
348
+ fields: ["name", "email"],
349
+ }),
350
+ });
351
+
352
+ await new Promise((r) => setTimeout(r, 50));
353
+
354
+ // Should receive messages (either from GraphStateManager or operation-level)
355
+ expect(ws.messages.length).toBeGreaterThan(0);
356
+
357
+ // Find the operation-level data message (has subscription id)
358
+ const dataMessage = ws.messages
359
+ .map((m) => JSON.parse(m))
360
+ .find((m) => m.type === "data" && m.id === "sub-1");
361
+
362
+ // Should have received operation-level data
363
+ expect(dataMessage).toBeDefined();
364
+ expect(dataMessage.data).toMatchObject({ name: "Alice" });
365
+ });
366
+
367
+ it("handles unsubscribe message", async () => {
368
+ const getUser = query()
369
+ .input(z.object({ id: z.string() }))
370
+ .returns(User)
371
+ .resolve(({ input }) => mockUsers.find((u) => u.id === input.id) ?? null);
372
+
373
+ const server = createServer({
374
+ entities: { User },
375
+ queries: { getUser },
376
+ });
377
+
378
+ const ws = createMockWs();
379
+ server.handleWebSocket(ws);
380
+
381
+ // Subscribe
382
+ ws.onmessage?.({
383
+ data: JSON.stringify({
384
+ type: "subscribe",
385
+ id: "sub-1",
386
+ operation: "getUser",
387
+ input: { id: "user-1" },
388
+ fields: "*",
389
+ }),
390
+ });
391
+
392
+ await new Promise((r) => setTimeout(r, 20));
393
+
394
+ // Unsubscribe
395
+ ws.onmessage?.({
396
+ data: JSON.stringify({ type: "unsubscribe", id: "sub-1" }),
397
+ });
398
+
399
+ // Should not throw
400
+ expect(true).toBe(true);
401
+ });
402
+
403
+ it("handles updateFields message", async () => {
404
+ const getUser = query()
405
+ .input(z.object({ id: z.string() }))
406
+ .returns(User)
407
+ .resolve(({ input }) => mockUsers.find((u) => u.id === input.id) ?? null);
408
+
409
+ const server = createServer({
410
+ entities: { User },
411
+ queries: { getUser },
412
+ });
413
+
414
+ const ws = createMockWs();
415
+ server.handleWebSocket(ws);
416
+
417
+ // Subscribe with partial fields
418
+ ws.onmessage?.({
419
+ data: JSON.stringify({
420
+ type: "subscribe",
421
+ id: "sub-1",
422
+ operation: "getUser",
423
+ input: { id: "user-1" },
424
+ fields: ["name"],
425
+ }),
426
+ });
427
+
428
+ await new Promise((r) => setTimeout(r, 20));
429
+
430
+ // Update fields
431
+ ws.onmessage?.({
432
+ data: JSON.stringify({
433
+ type: "updateFields",
434
+ id: "sub-1",
435
+ addFields: ["email"],
436
+ removeFields: [],
437
+ }),
438
+ });
439
+
440
+ // Should not throw
441
+ expect(true).toBe(true);
442
+ });
443
+ });
444
+
445
+ // =============================================================================
446
+ // Test: GraphStateManager Integration
447
+ // =============================================================================
448
+
449
+ describe("GraphStateManager Integration", () => {
450
+ it("provides access to state manager", () => {
451
+ const server = createServer({
452
+ entities: { User },
453
+ });
454
+
455
+ const stateManager = server.getStateManager();
456
+ expect(stateManager).toBeDefined();
457
+ expect(typeof stateManager.emit).toBe("function");
458
+ expect(typeof stateManager.subscribe).toBe("function");
459
+ });
460
+
461
+ it("tracks client state after subscribe", async () => {
462
+ const getUser = query()
463
+ .input(z.object({ id: z.string() }))
464
+ .returns(User)
465
+ .resolve(({ input }) => mockUsers.find((u) => u.id === input.id) ?? null);
466
+
467
+ const server = createServer({
468
+ entities: { User },
469
+ queries: { getUser },
470
+ });
471
+
472
+ const stateManager = server.getStateManager();
473
+
474
+ const ws = createMockWs();
475
+ server.handleWebSocket(ws);
476
+
477
+ // Subscribe
478
+ ws.onmessage?.({
479
+ data: JSON.stringify({
480
+ type: "subscribe",
481
+ id: "sub-1",
482
+ operation: "getUser",
483
+ input: { id: "user-1" },
484
+ fields: "*",
485
+ }),
486
+ });
487
+
488
+ await new Promise((r) => setTimeout(r, 50));
489
+
490
+ // State manager should have the client registered
491
+ const stats = stateManager.getStats();
492
+ expect(stats.clients).toBe(1);
493
+ });
494
+
495
+ it("removes client state on disconnect", async () => {
496
+ const server = createServer({
497
+ entities: { User },
498
+ });
499
+
500
+ const stateManager = server.getStateManager();
501
+
502
+ const ws = createMockWs();
503
+ server.handleWebSocket(ws);
504
+
505
+ // Simulate disconnect
506
+ ws.onclose?.();
507
+
508
+ const stats = stateManager.getStats();
509
+ expect(stats.clients).toBe(0);
510
+ });
511
+ });
512
+
513
+ // =============================================================================
514
+ // Test: HTTP Handler
515
+ // =============================================================================
516
+
517
+ describe("handleRequest", () => {
518
+ it("handles query request", async () => {
519
+ const getUsers = query()
520
+ .returns([User])
521
+ .resolve(() => mockUsers);
522
+
523
+ const server = createServer({
524
+ entities: { User },
525
+ queries: { getUsers },
526
+ });
527
+
528
+ const request = new Request("http://localhost/api", {
529
+ method: "POST",
530
+ headers: { "Content-Type": "application/json" },
531
+ body: JSON.stringify({ type: "query", operation: "getUsers" }),
532
+ });
533
+
534
+ const response = await server.handleRequest(request);
535
+ expect(response.status).toBe(200);
536
+
537
+ const body = await response.json();
538
+ expect(body.data).toEqual(mockUsers);
539
+ });
540
+
541
+ it("handles mutation request", async () => {
542
+ const createUser = mutation()
543
+ .input(z.object({ name: z.string() }))
544
+ .returns(User)
545
+ .resolve(({ input }) => ({ id: "new", name: input.name, email: "" }));
546
+
547
+ const server = createServer({
548
+ entities: { User },
549
+ mutations: { createUser },
550
+ });
551
+
552
+ const request = new Request("http://localhost/api", {
553
+ method: "POST",
554
+ headers: { "Content-Type": "application/json" },
555
+ body: JSON.stringify({ type: "mutation", operation: "createUser", input: { name: "Test" } }),
556
+ });
557
+
558
+ const response = await server.handleRequest(request);
559
+ expect(response.status).toBe(200);
560
+
561
+ const body = await response.json();
562
+ expect(body.data).toEqual({ id: "new", name: "Test", email: "" });
563
+ });
564
+
565
+ it("rejects non-POST requests", async () => {
566
+ const server = createServer({
567
+ entities: { User },
568
+ });
569
+
570
+ const request = new Request("http://localhost/api", { method: "GET" });
571
+ const response = await server.handleRequest(request);
572
+
573
+ expect(response.status).toBe(405);
574
+ });
575
+ });
576
+
577
+ // =============================================================================
578
+ // Test: Streaming (Async Generator) Support
579
+ // =============================================================================
580
+
581
+ describe("Streaming Support", () => {
582
+ it("handles async generator query", async () => {
583
+ const streamQuery = query()
584
+ .returns(User)
585
+ .resolve(async function* () {
586
+ yield mockUsers[0];
587
+ yield mockUsers[1];
588
+ });
589
+
590
+ const server = createServer({
591
+ entities: { User },
592
+ queries: { streamQuery },
593
+ });
594
+
595
+ // executeQuery returns first value
596
+ const result = await server.executeQuery("streamQuery");
597
+ expect(result).toEqual(mockUsers[0]);
598
+ });
599
+
600
+ it("streams values via WebSocket subscribe", async () => {
601
+ let yieldCount = 0;
602
+
603
+ const streamQuery = query()
604
+ .returns(User)
605
+ .resolve(async function* () {
606
+ yieldCount++;
607
+ yield mockUsers[0];
608
+ yieldCount++;
609
+ yield mockUsers[1];
610
+ });
611
+
612
+ const server = createServer({
613
+ entities: { User },
614
+ queries: { streamQuery },
615
+ });
616
+
617
+ const ws = createMockWs();
618
+ server.handleWebSocket(ws);
619
+
620
+ // Subscribe to stream
621
+ ws.onmessage?.({
622
+ data: JSON.stringify({
623
+ type: "subscribe",
624
+ id: "sub-1",
625
+ operation: "streamQuery",
626
+ fields: "*",
627
+ }),
628
+ });
629
+
630
+ await new Promise((r) => setTimeout(r, 50));
631
+
632
+ expect(yieldCount).toBe(2);
633
+ expect(ws.messages.length).toBeGreaterThanOrEqual(1);
634
+ });
635
+ });
636
+
637
+ // =============================================================================
638
+ // Test: Minimum Transfer (Diff Computation)
639
+ // =============================================================================
640
+
641
+ describe("Minimum Transfer", () => {
642
+ it("sends initial data on subscribe", async () => {
643
+ let emitFn: ((data: unknown) => void) | null = null;
644
+
645
+ const liveQuery = query()
646
+ .returns(User)
647
+ .resolve(({ ctx }) => {
648
+ emitFn = ctx.emit;
649
+ return { id: "1", name: "Alice", email: "alice@example.com" };
650
+ });
651
+
652
+ const server = createServer({
653
+ entities: { User },
654
+ queries: { liveQuery },
655
+ });
656
+
657
+ const ws = createMockWs();
658
+ server.handleWebSocket(ws);
659
+
660
+ ws.onmessage?.({
661
+ data: JSON.stringify({
662
+ type: "subscribe",
663
+ id: "sub-1",
664
+ operation: "liveQuery",
665
+ fields: "*",
666
+ }),
667
+ });
668
+
669
+ await new Promise((r) => setTimeout(r, 50));
670
+
671
+ // First message should have data
672
+ expect(ws.messages.length).toBeGreaterThan(0);
673
+ const firstUpdate = JSON.parse(ws.messages[0]);
674
+ // Can be "data" or "update" with value strategy for initial
675
+ expect(["data", "update"]).toContain(firstUpdate.type);
676
+ // Verify we got the data
677
+ if (firstUpdate.type === "data") {
678
+ expect(firstUpdate.data).toMatchObject({ name: "Alice" });
679
+ } else {
680
+ expect(firstUpdate.updates).toBeDefined();
681
+ }
682
+ });
683
+
684
+ it("sends updates via ctx.emit", async () => {
685
+ let emitFn: ((data: unknown) => void) | null = null;
686
+
687
+ const liveQuery = query()
688
+ .returns(User)
689
+ .resolve(({ ctx }) => {
690
+ emitFn = ctx.emit;
691
+ return { id: "1", name: "Alice", email: "alice@example.com" };
692
+ });
693
+
694
+ const server = createServer({
695
+ entities: { User },
696
+ queries: { liveQuery },
697
+ });
698
+
699
+ const ws = createMockWs();
700
+ server.handleWebSocket(ws);
701
+
702
+ ws.onmessage?.({
703
+ data: JSON.stringify({
704
+ type: "subscribe",
705
+ id: "sub-1",
706
+ operation: "liveQuery",
707
+ fields: "*",
708
+ }),
709
+ });
710
+
711
+ await new Promise((r) => setTimeout(r, 50));
712
+
713
+ const initialCount = ws.messages.length;
714
+
715
+ // Emit update with changed data
716
+ emitFn?.({ id: "1", name: "Bob", email: "bob@example.com" });
717
+
718
+ await new Promise((r) => setTimeout(r, 50));
719
+
720
+ // Should have received additional messages
721
+ expect(ws.messages.length).toBeGreaterThanOrEqual(initialCount);
722
+ });
723
+ });
724
+
725
+ // =============================================================================
726
+ // Test: ctx.onCleanup
727
+ // =============================================================================
728
+
729
+ describe("ctx.onCleanup", () => {
730
+ it("calls cleanup function on unsubscribe", async () => {
731
+ let cleanedUp = false;
732
+
733
+ const liveQuery = query()
734
+ .returns(User)
735
+ .resolve(({ ctx }) => {
736
+ ctx.onCleanup(() => {
737
+ cleanedUp = true;
738
+ });
739
+ return mockUsers[0];
740
+ });
741
+
742
+ const server = createServer({
743
+ entities: { User },
744
+ queries: { liveQuery },
745
+ });
746
+
747
+ const ws = createMockWs();
748
+ server.handleWebSocket(ws);
749
+
750
+ // Subscribe
751
+ ws.onmessage?.({
752
+ data: JSON.stringify({
753
+ type: "subscribe",
754
+ id: "sub-1",
755
+ operation: "liveQuery",
756
+ fields: "*",
757
+ }),
758
+ });
759
+
760
+ await new Promise((r) => setTimeout(r, 20));
761
+
762
+ // Unsubscribe
763
+ ws.onmessage?.({
764
+ data: JSON.stringify({ type: "unsubscribe", id: "sub-1" }),
765
+ });
766
+
767
+ expect(cleanedUp).toBe(true);
768
+ });
769
+
770
+ it("calls cleanup on client disconnect", async () => {
771
+ let cleanedUp = false;
772
+
773
+ const liveQuery = query()
774
+ .returns(User)
775
+ .resolve(({ ctx }) => {
776
+ ctx.onCleanup(() => {
777
+ cleanedUp = true;
778
+ });
779
+ return mockUsers[0];
780
+ });
781
+
782
+ const server = createServer({
783
+ entities: { User },
784
+ queries: { liveQuery },
785
+ });
786
+
787
+ const ws = createMockWs();
788
+ server.handleWebSocket(ws);
789
+
790
+ // Subscribe
791
+ ws.onmessage?.({
792
+ data: JSON.stringify({
793
+ type: "subscribe",
794
+ id: "sub-1",
795
+ operation: "liveQuery",
796
+ fields: "*",
797
+ }),
798
+ });
799
+
800
+ await new Promise((r) => setTimeout(r, 20));
801
+
802
+ // Disconnect
803
+ ws.onclose?.();
804
+
805
+ expect(cleanedUp).toBe(true);
806
+ });
807
+ });