gsd-pi 2.53.0-dev.07ffe51 → 2.53.0-dev.a67436f

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.
Files changed (57) hide show
  1. package/dist/headless-ui.d.ts +2 -2
  2. package/dist/headless-ui.js +18 -15
  3. package/dist/headless.d.ts +11 -0
  4. package/dist/headless.js +178 -38
  5. package/dist/web/standalone/.next/BUILD_ID +1 -1
  6. package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
  7. package/dist/web/standalone/.next/build-manifest.json +2 -2
  8. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  9. package/dist/web/standalone/.next/required-server-files.json +1 -1
  10. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  11. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  12. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  19. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/index.html +1 -1
  27. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
  34. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  35. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  36. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  37. package/dist/web/standalone/server.js +1 -1
  38. package/package.json +1 -1
  39. package/packages/mcp-server/README.md +6 -6
  40. package/packages/mcp-server/package.json +14 -4
  41. package/packages/mcp-server/src/cli.ts +1 -1
  42. package/packages/mcp-server/src/index.ts +1 -1
  43. package/packages/mcp-server/src/mcp-server.test.ts +2 -2
  44. package/packages/mcp-server/src/session-manager.ts +2 -2
  45. package/packages/mcp-server/src/types.ts +1 -1
  46. package/packages/rpc-client/README.md +125 -0
  47. package/packages/rpc-client/examples/basic-usage.ts +13 -0
  48. package/packages/rpc-client/package.json +17 -3
  49. package/packages/rpc-client/src/index.ts +10 -0
  50. package/packages/rpc-client/src/jsonl.ts +64 -0
  51. package/packages/rpc-client/src/rpc-client.test.ts +568 -0
  52. package/packages/rpc-client/src/rpc-client.ts +666 -0
  53. package/packages/rpc-client/src/rpc-types.ts +399 -0
  54. package/packages/rpc-client/tsconfig.examples.json +17 -0
  55. package/packages/rpc-client/tsconfig.json +24 -0
  56. /package/dist/web/standalone/.next/static/{Q5pfrfJIvgUKR3LJLVB0T → YO-PWFRitlHM-L-dotlmm}/_buildManifest.js +0 -0
  57. /package/dist/web/standalone/.next/static/{Q5pfrfJIvgUKR3LJLVB0T → YO-PWFRitlHM-L-dotlmm}/_ssgManifest.js +0 -0
@@ -0,0 +1,568 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { PassThrough } from "node:stream";
4
+ import { serializeJsonLine, attachJsonlLineReader } from "./jsonl.js";
5
+ import type {
6
+ RpcInitResult,
7
+ RpcExecutionCompleteEvent,
8
+ RpcCostUpdateEvent,
9
+ RpcProtocolVersion,
10
+ SessionStats,
11
+ RpcV2Event,
12
+ } from "./rpc-types.js";
13
+ import { RpcClient } from "./rpc-client.js";
14
+ import type { SdkAgentEvent } from "./rpc-client.js";
15
+
16
+ // ============================================================================
17
+ // JSONL Tests
18
+ // ============================================================================
19
+
20
+ describe("serializeJsonLine", () => {
21
+ it("produces valid JSON terminated with LF", () => {
22
+ const result = serializeJsonLine({ type: "test", value: 42 });
23
+ assert.ok(result.endsWith("\n"), "must end with LF");
24
+ const parsed = JSON.parse(result.trim());
25
+ assert.equal(parsed.type, "test");
26
+ assert.equal(parsed.value, 42);
27
+ });
28
+
29
+ it("serializes strings with special characters", () => {
30
+ const result = serializeJsonLine({ msg: "hello\nworld" });
31
+ assert.ok(result.endsWith("\n"));
32
+ // The embedded \n must be escaped inside the JSON — only the trailing LF is the framing delimiter
33
+ const lines = result.split("\n");
34
+ // Should be exactly 2 parts: the JSON line and the empty string after trailing LF
35
+ assert.equal(lines.length, 2);
36
+ assert.equal(lines[1], "");
37
+ const parsed = JSON.parse(lines[0]);
38
+ assert.equal(parsed.msg, "hello\nworld");
39
+ });
40
+
41
+ it("handles empty objects", () => {
42
+ const result = serializeJsonLine({});
43
+ assert.equal(result, "{}\n");
44
+ });
45
+ });
46
+
47
+ describe("attachJsonlLineReader", () => {
48
+ it("splits on LF correctly", async () => {
49
+ const stream = new PassThrough();
50
+ const lines: string[] = [];
51
+
52
+ attachJsonlLineReader(stream, (line) => lines.push(line));
53
+
54
+ stream.write('{"a":1}\n{"b":2}\n');
55
+ stream.end();
56
+
57
+ // Let microtask queue flush
58
+ await new Promise((r) => setTimeout(r, 10));
59
+
60
+ assert.equal(lines.length, 2);
61
+ assert.equal(JSON.parse(lines[0]).a, 1);
62
+ assert.equal(JSON.parse(lines[1]).b, 2);
63
+ });
64
+
65
+ it("handles chunked data across boundaries", async () => {
66
+ const stream = new PassThrough();
67
+ const lines: string[] = [];
68
+
69
+ attachJsonlLineReader(stream, (line) => lines.push(line));
70
+
71
+ // Write in fragments that split mid-line
72
+ stream.write('{"type":"hel');
73
+ stream.write('lo"}\n{"type":"w');
74
+ stream.write('orld"}\n');
75
+ stream.end();
76
+
77
+ await new Promise((r) => setTimeout(r, 10));
78
+
79
+ assert.equal(lines.length, 2);
80
+ assert.equal(JSON.parse(lines[0]).type, "hello");
81
+ assert.equal(JSON.parse(lines[1]).type, "world");
82
+ });
83
+
84
+ it("emits trailing data on stream end", async () => {
85
+ const stream = new PassThrough();
86
+ const lines: string[] = [];
87
+
88
+ attachJsonlLineReader(stream, (line) => lines.push(line));
89
+
90
+ stream.write('{"final":true}');
91
+ stream.end();
92
+
93
+ await new Promise((r) => setTimeout(r, 10));
94
+
95
+ assert.equal(lines.length, 1);
96
+ assert.equal(JSON.parse(lines[0]).final, true);
97
+ });
98
+
99
+ it("returns a detach function that stops reading", async () => {
100
+ const stream = new PassThrough();
101
+ const lines: string[] = [];
102
+
103
+ const detach = attachJsonlLineReader(stream, (line) => lines.push(line));
104
+
105
+ stream.write('{"a":1}\n');
106
+ await new Promise((r) => setTimeout(r, 10));
107
+ assert.equal(lines.length, 1);
108
+
109
+ detach();
110
+
111
+ stream.write('{"b":2}\n');
112
+ stream.end();
113
+ await new Promise((r) => setTimeout(r, 10));
114
+
115
+ // Should still be 1 — detach removed listeners
116
+ assert.equal(lines.length, 1);
117
+ });
118
+
119
+ it("strips CR from CRLF line endings", async () => {
120
+ const stream = new PassThrough();
121
+ const lines: string[] = [];
122
+
123
+ attachJsonlLineReader(stream, (line) => lines.push(line));
124
+
125
+ stream.write('{"v":1}\r\n');
126
+ stream.end();
127
+
128
+ await new Promise((r) => setTimeout(r, 10));
129
+
130
+ assert.equal(lines.length, 1);
131
+ assert.equal(JSON.parse(lines[0]).v, 1);
132
+ });
133
+ });
134
+
135
+ // ============================================================================
136
+ // Type Shape Tests
137
+ // ============================================================================
138
+
139
+ describe("type shapes", () => {
140
+ it("RpcInitResult has protocolVersion, sessionId, capabilities", () => {
141
+ const init: RpcInitResult = {
142
+ protocolVersion: 2,
143
+ sessionId: "sess_123",
144
+ capabilities: {
145
+ events: ["execution_complete", "cost_update"],
146
+ commands: ["prompt", "steer"],
147
+ },
148
+ };
149
+ assert.equal(init.protocolVersion, 2);
150
+ assert.equal(init.sessionId, "sess_123");
151
+ assert.ok(Array.isArray(init.capabilities.events));
152
+ assert.ok(Array.isArray(init.capabilities.commands));
153
+ });
154
+
155
+ it("RpcExecutionCompleteEvent has required fields", () => {
156
+ const event: RpcExecutionCompleteEvent = {
157
+ type: "execution_complete",
158
+ runId: "run_abc",
159
+ status: "completed",
160
+ stats: {
161
+ sessionFile: "/tmp/session.json",
162
+ sessionId: "sess_123",
163
+ userMessages: 5,
164
+ assistantMessages: 5,
165
+ toolCalls: 3,
166
+ toolResults: 3,
167
+ totalMessages: 10,
168
+ tokens: { input: 1000, output: 500, cacheRead: 200, cacheWrite: 100, total: 1800 },
169
+ cost: 0.05,
170
+ },
171
+ };
172
+ assert.equal(event.type, "execution_complete");
173
+ assert.equal(event.runId, "run_abc");
174
+ assert.equal(event.status, "completed");
175
+ assert.ok(event.stats);
176
+ assert.equal(event.stats.sessionId, "sess_123");
177
+ });
178
+
179
+ it("RpcCostUpdateEvent has required fields", () => {
180
+ const event: RpcCostUpdateEvent = {
181
+ type: "cost_update",
182
+ runId: "run_abc",
183
+ turnCost: 0.01,
184
+ cumulativeCost: 0.05,
185
+ tokens: { input: 500, output: 200, cacheRead: 100, cacheWrite: 50 },
186
+ };
187
+ assert.equal(event.type, "cost_update");
188
+ assert.equal(event.runId, "run_abc");
189
+ assert.equal(event.turnCost, 0.01);
190
+ assert.equal(event.cumulativeCost, 0.05);
191
+ assert.ok(event.tokens);
192
+ });
193
+
194
+ it("SessionStats has all expected fields", () => {
195
+ const stats: SessionStats = {
196
+ sessionFile: "/tmp/session.json",
197
+ sessionId: "s1",
198
+ userMessages: 10,
199
+ assistantMessages: 10,
200
+ toolCalls: 5,
201
+ toolResults: 5,
202
+ totalMessages: 20,
203
+ tokens: { input: 2000, output: 1000, cacheRead: 500, cacheWrite: 200, total: 3700 },
204
+ cost: 0.10,
205
+ };
206
+ assert.equal(stats.sessionId, "s1");
207
+ assert.equal(stats.userMessages, 10);
208
+ assert.equal(stats.tokens.total, 3700);
209
+ assert.equal(stats.cost, 0.10);
210
+ });
211
+
212
+ it("RpcProtocolVersion accepts 1 and 2", () => {
213
+ const v1: RpcProtocolVersion = 1;
214
+ const v2: RpcProtocolVersion = 2;
215
+ assert.equal(v1, 1);
216
+ assert.equal(v2, 2);
217
+ });
218
+
219
+ it("RpcV2Event discriminated union covers both event types", () => {
220
+ const events: RpcV2Event[] = [
221
+ {
222
+ type: "execution_complete",
223
+ runId: "r1",
224
+ status: "completed",
225
+ stats: {
226
+ sessionFile: undefined,
227
+ sessionId: "s1",
228
+ userMessages: 1,
229
+ assistantMessages: 1,
230
+ toolCalls: 0,
231
+ toolResults: 0,
232
+ totalMessages: 2,
233
+ tokens: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0, total: 150 },
234
+ cost: 0.001,
235
+ },
236
+ },
237
+ {
238
+ type: "cost_update",
239
+ runId: "r1",
240
+ turnCost: 0.001,
241
+ cumulativeCost: 0.001,
242
+ tokens: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0 },
243
+ },
244
+ ];
245
+ assert.equal(events.length, 2);
246
+ assert.equal(events[0].type, "execution_complete");
247
+ assert.equal(events[1].type, "cost_update");
248
+ });
249
+ });
250
+
251
+ // ============================================================================
252
+ // RpcClient Construction Tests
253
+ // ============================================================================
254
+
255
+ describe("RpcClient construction", () => {
256
+ it("creates with default options", () => {
257
+ const client = new RpcClient();
258
+ assert.ok(client);
259
+ });
260
+
261
+ it("creates with custom options", () => {
262
+ const client = new RpcClient({
263
+ cliPath: "/usr/local/bin/gsd",
264
+ cwd: "/tmp",
265
+ env: { NODE_ENV: "test" },
266
+ provider: "anthropic",
267
+ model: "claude-sonnet",
268
+ args: ["--verbose"],
269
+ });
270
+ assert.ok(client);
271
+ });
272
+ });
273
+
274
+ // ============================================================================
275
+ // events() Generator Tests
276
+ // ============================================================================
277
+
278
+ describe("events() async generator", () => {
279
+ it("yields events from a mock stream in order", async () => {
280
+ const client = new RpcClient();
281
+
282
+ // Reach into the client to set up a mock process with a PassThrough stdout
283
+ const mockStdout = new PassThrough();
284
+ const mockStderr = new PassThrough();
285
+ const mockStdin = new PassThrough();
286
+
287
+ // Simulate a started process by setting internal state
288
+ // We use Object.assign to set private fields for testing
289
+ const clientAny = client as any;
290
+ clientAny.process = {
291
+ stdout: mockStdout,
292
+ stderr: mockStderr,
293
+ stdin: mockStdin,
294
+ exitCode: null,
295
+ kill: () => {},
296
+ on: (event: string, handler: (...args: any[]) => void) => {
297
+ if (event === "exit") {
298
+ // Store exit handler so we can trigger it
299
+ clientAny._testExitHandler = handler;
300
+ }
301
+ },
302
+ removeListener: () => {},
303
+ };
304
+
305
+ // Attach the JSONL reader like start() does
306
+ clientAny.stopReadingStdout = attachJsonlLineReader(mockStdout, (line: string) => {
307
+ clientAny.handleLine(line);
308
+ });
309
+
310
+ // Collect events from the generator
311
+ const received: SdkAgentEvent[] = [];
312
+ const genPromise = (async () => {
313
+ for await (const event of client.events()) {
314
+ received.push(event);
315
+ if (event.type === "done") break;
316
+ }
317
+ })();
318
+
319
+ // Simulate server sending events
320
+ await new Promise((r) => setTimeout(r, 20));
321
+ mockStdout.write(serializeJsonLine({ type: "agent_start", runId: "r1" }));
322
+ await new Promise((r) => setTimeout(r, 20));
323
+ mockStdout.write(serializeJsonLine({ type: "token", text: "hello" }));
324
+ await new Promise((r) => setTimeout(r, 20));
325
+ mockStdout.write(serializeJsonLine({ type: "done" }));
326
+
327
+ await genPromise;
328
+
329
+ assert.equal(received.length, 3);
330
+ assert.equal(received[0].type, "agent_start");
331
+ assert.equal(received[1].type, "token");
332
+ assert.equal(received[2].type, "done");
333
+ });
334
+
335
+ it("terminates when process exits", async () => {
336
+ const client = new RpcClient();
337
+ const mockStdout = new PassThrough();
338
+ const mockStderr = new PassThrough();
339
+ const mockStdin = new PassThrough();
340
+
341
+ const exitHandlers: Array<() => void> = [];
342
+ const clientAny = client as any;
343
+ clientAny.process = {
344
+ stdout: mockStdout,
345
+ stderr: mockStderr,
346
+ stdin: mockStdin,
347
+ exitCode: null,
348
+ kill: () => {},
349
+ on: (event: string, handler: () => void) => {
350
+ if (event === "exit") exitHandlers.push(handler);
351
+ },
352
+ removeListener: (event: string, handler: () => void) => {
353
+ const idx = exitHandlers.indexOf(handler);
354
+ if (idx !== -1) exitHandlers.splice(idx, 1);
355
+ },
356
+ };
357
+
358
+ clientAny.stopReadingStdout = attachJsonlLineReader(mockStdout, (line: string) => {
359
+ clientAny.handleLine(line);
360
+ });
361
+
362
+ const received: SdkAgentEvent[] = [];
363
+ const genPromise = (async () => {
364
+ for await (const event of client.events()) {
365
+ received.push(event);
366
+ }
367
+ })();
368
+
369
+ // Send one event, then simulate process exit
370
+ await new Promise((r) => setTimeout(r, 20));
371
+ mockStdout.write(serializeJsonLine({ type: "agent_start" }));
372
+ await new Promise((r) => setTimeout(r, 20));
373
+
374
+ // Fire exit handlers
375
+ for (const h of exitHandlers) h();
376
+
377
+ await genPromise;
378
+
379
+ assert.equal(received.length, 1);
380
+ assert.equal(received[0].type, "agent_start");
381
+ });
382
+
383
+ it("throws if client not started", async () => {
384
+ const client = new RpcClient();
385
+ await assert.rejects(async () => {
386
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
387
+ for await (const _event of client.events()) {
388
+ // should not reach
389
+ }
390
+ }, /Client not started/);
391
+ });
392
+ });
393
+
394
+ // ============================================================================
395
+ // sendUIResponse Serialization Test
396
+ // ============================================================================
397
+
398
+ describe("sendUIResponse serialization", () => {
399
+ it("writes correct JSONL to stdin", () => {
400
+ const client = new RpcClient();
401
+ const chunks: string[] = [];
402
+ const mockStdin = {
403
+ write: (data: string) => {
404
+ chunks.push(data);
405
+ return true;
406
+ },
407
+ };
408
+
409
+ const clientAny = client as any;
410
+ clientAny.process = { stdin: mockStdin };
411
+
412
+ client.sendUIResponse("ui_1", { value: "hello" });
413
+
414
+ assert.equal(chunks.length, 1);
415
+ const parsed = JSON.parse(chunks[0].trim());
416
+ assert.equal(parsed.type, "extension_ui_response");
417
+ assert.equal(parsed.id, "ui_1");
418
+ assert.equal(parsed.value, "hello");
419
+ });
420
+
421
+ it("serializes confirmed response", () => {
422
+ const client = new RpcClient();
423
+ const chunks: string[] = [];
424
+ const mockStdin = {
425
+ write: (data: string) => {
426
+ chunks.push(data);
427
+ return true;
428
+ },
429
+ };
430
+ const clientAny = client as any;
431
+ clientAny.process = { stdin: mockStdin };
432
+
433
+ client.sendUIResponse("ui_2", { confirmed: true });
434
+
435
+ const parsed = JSON.parse(chunks[0].trim());
436
+ assert.equal(parsed.confirmed, true);
437
+ assert.equal(parsed.id, "ui_2");
438
+ });
439
+
440
+ it("serializes cancelled response", () => {
441
+ const client = new RpcClient();
442
+ const chunks: string[] = [];
443
+ const mockStdin = {
444
+ write: (data: string) => {
445
+ chunks.push(data);
446
+ return true;
447
+ },
448
+ };
449
+ const clientAny = client as any;
450
+ clientAny.process = { stdin: mockStdin };
451
+
452
+ client.sendUIResponse("ui_3", { cancelled: true });
453
+
454
+ const parsed = JSON.parse(chunks[0].trim());
455
+ assert.equal(parsed.cancelled, true);
456
+ });
457
+ });
458
+
459
+ // ============================================================================
460
+ // init/shutdown/subscribe Serialization Tests
461
+ // ============================================================================
462
+
463
+ describe("v2 command serialization", () => {
464
+ // Helper: capture what the client sends to stdin
465
+ function createMockClient(): { client: RpcClient; sent: any[]; respondNext: (data?: any) => void } {
466
+ const client = new RpcClient();
467
+ const sent: any[] = [];
468
+ let respondFn: ((data: any) => void) | null = null;
469
+
470
+ const clientAny = client as any;
471
+ clientAny.process = {
472
+ stdin: {
473
+ write: (data: string) => {
474
+ const parsed = JSON.parse(data.trim());
475
+ sent.push(parsed);
476
+ // Auto-respond with success after a tick
477
+ if (respondFn) {
478
+ setTimeout(() => respondFn!(parsed), 5);
479
+ }
480
+ return true;
481
+ },
482
+ },
483
+ stderr: new PassThrough(),
484
+ exitCode: null,
485
+ kill: () => {},
486
+ on: () => {},
487
+ removeListener: () => {},
488
+ };
489
+
490
+ const respondNext = (overrides: any = {}) => {
491
+ respondFn = (parsed) => {
492
+ const response = {
493
+ type: "response",
494
+ id: parsed.id,
495
+ command: parsed.type,
496
+ success: true,
497
+ data: {},
498
+ ...overrides,
499
+ };
500
+ clientAny.handleLine(JSON.stringify(response));
501
+ };
502
+ };
503
+
504
+ return { client, sent, respondNext };
505
+ }
506
+
507
+ it("init sends correct v2 init command", async () => {
508
+ const { client, sent, respondNext } = createMockClient();
509
+ respondNext({ data: { protocolVersion: 2, sessionId: "s1", capabilities: { events: [], commands: [] } } });
510
+
511
+ const result = await client.init({ clientId: "test-app" });
512
+
513
+ assert.equal(sent.length, 1);
514
+ assert.equal(sent[0].type, "init");
515
+ assert.equal(sent[0].protocolVersion, 2);
516
+ assert.equal(sent[0].clientId, "test-app");
517
+ assert.equal(result.protocolVersion, 2);
518
+ assert.equal(result.sessionId, "s1");
519
+ });
520
+
521
+ it("shutdown sends shutdown command", async () => {
522
+ const { client, sent, respondNext } = createMockClient();
523
+
524
+ // Override the process exit wait
525
+ const clientAny = client as any;
526
+ const originalProcess = clientAny.process;
527
+ const exitHandlers: Array<(code: number) => void> = [];
528
+ clientAny.process = {
529
+ ...originalProcess,
530
+ on: (event: string, handler: (code: number) => void) => {
531
+ if (event === "exit") exitHandlers.push(handler);
532
+ },
533
+ };
534
+
535
+ respondNext();
536
+
537
+ // Call shutdown and simulate process exit
538
+ const shutdownPromise = client.shutdown();
539
+ await new Promise((r) => setTimeout(r, 20));
540
+ for (const h of exitHandlers) h(0);
541
+
542
+ await shutdownPromise;
543
+
544
+ assert.equal(sent.length, 1);
545
+ assert.equal(sent[0].type, "shutdown");
546
+ });
547
+
548
+ it("subscribe sends subscribe command with event list", async () => {
549
+ const { client, sent, respondNext } = createMockClient();
550
+ respondNext();
551
+
552
+ await client.subscribe(["execution_complete", "cost_update"]);
553
+
554
+ assert.equal(sent.length, 1);
555
+ assert.equal(sent[0].type, "subscribe");
556
+ assert.deepEqual(sent[0].events, ["execution_complete", "cost_update"]);
557
+ });
558
+
559
+ it("subscribe with wildcard", async () => {
560
+ const { client, sent, respondNext } = createMockClient();
561
+ respondNext();
562
+
563
+ await client.subscribe(["*"]);
564
+
565
+ assert.equal(sent[0].events.length, 1);
566
+ assert.equal(sent[0].events[0], "*");
567
+ });
568
+ });