@sjawhar/opencode-legion-envoy 0.5.2 → 0.6.1

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.
@@ -1,634 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
2
-
3
- // Suppress console.error during tests
4
- const originalError = console.error;
5
- beforeEach(() => {
6
- console.error = mock(() => {});
7
- });
8
- afterEach(() => {
9
- console.error = originalError;
10
- });
11
-
12
- describe("envoy plugin init", () => {
13
- it("returns immediately without blocking on port resolution or Envoy calls", async () => {
14
- // Simulate NATS/Envoy being unavailable — plugin init must still complete fast
15
- const originalEnvoyUrl = process.env.ENVOY_URL;
16
- process.env.ENVOY_URL = "http://127.0.0.1:59999"; // Non-existent
17
-
18
- try {
19
- const pluginModule = await import("../server");
20
- const initPlugin = pluginModule.default;
21
-
22
- const start = performance.now();
23
- const hooks = await initPlugin({ serverUrl: new URL("http://127.0.0.1:13381") } as never);
24
- const elapsed = performance.now() - start;
25
-
26
- // Plugin init must complete in under 1 second regardless of NATS state
27
- expect(elapsed).toBeLessThan(1000);
28
- expect(hooks.tool).toBeDefined();
29
- expect(hooks.tool.envoy_subscribe).toBeDefined();
30
- expect(hooks.tool.envoy_unsubscribe).toBeDefined();
31
- expect(hooks.tool.envoy_list).toBeDefined();
32
- expect(hooks.tool.envoy_send).toBeDefined();
33
- expect(hooks.tool.envoy_publish).toBeDefined();
34
- expect(hooks.tool.envoy_whoami).toBeDefined();
35
- expect(hooks.tool.envoy_sessions).toBeDefined();
36
- } finally {
37
- process.env.ENVOY_URL = originalEnvoyUrl;
38
- }
39
- });
40
-
41
- it("call() includes a timeout to prevent hanging on unresponsive Envoy", async () => {
42
- // The call function has AbortSignal.timeout — verify it doesn't hang
43
- // We test this indirectly: a tool call to non-existent Envoy should reject within timeout
44
- const originalEnvoyUrl = process.env.ENVOY_URL;
45
- process.env.ENVOY_URL = "http://127.0.0.1:59999";
46
-
47
- try {
48
- const pluginModule = await import("../server");
49
- const initPlugin = pluginModule.default;
50
- const hooks = await initPlugin({ serverUrl: new URL("http://127.0.0.1:13381") } as never);
51
-
52
- const start = performance.now();
53
- try {
54
- await hooks.tool.envoy_list.execute({}, {
55
- sessionID: "ses_test",
56
- directory: "/tmp",
57
- metadata: () => {},
58
- } as never);
59
- } catch {
60
- // Expected to fail — Envoy is not running
61
- }
62
- const elapsed = performance.now() - start;
63
-
64
- // Should fail fast due to connection refused, not hang indefinitely
65
- expect(elapsed).toBeLessThan(6000);
66
- } finally {
67
- process.env.ENVOY_URL = originalEnvoyUrl;
68
- }
69
- });
70
- });
71
-
72
- describe("envoy_whoami", () => {
73
- it("returns session identity when Envoy is unavailable", async () => {
74
- const originalEnvoyUrl = process.env.ENVOY_URL;
75
- const originalHostname = process.env.HOSTNAME;
76
- process.env.ENVOY_URL = "http://127.0.0.1:59999";
77
- process.env.HOSTNAME = "test-machine";
78
-
79
- try {
80
- const pluginModule = await import("../server");
81
- const initPlugin = pluginModule.default;
82
- const hooks = await initPlugin({
83
- serverUrl: new URL("http://127.0.0.1:13381"),
84
- } as never);
85
-
86
- const result = await hooks.tool.envoy_whoami.execute({}, {
87
- sessionID: "ses_test_whoami",
88
- directory: "/tmp/test-workspace",
89
- metadata: mock(() => {}),
90
- } as never);
91
-
92
- const parsed = JSON.parse(typeof result === "string" ? result : result.output);
93
- expect(parsed.session_id).toBe("ses_test_whoami");
94
- expect(parsed.machine_id).toBe("test-machine");
95
- expect(parsed.dir).toBe("/tmp/test-workspace");
96
- expect(parsed).not.toHaveProperty("topics");
97
- expect(parsed.port === null || typeof parsed.port === "number").toBe(true);
98
- } finally {
99
- process.env.ENVOY_URL = originalEnvoyUrl;
100
- if (originalHostname === undefined) {
101
- delete process.env.HOSTNAME;
102
- } else {
103
- process.env.HOSTNAME = originalHostname;
104
- }
105
- }
106
- });
107
-
108
- it("uses 'unknown' for machine_id when HOSTNAME is not set", async () => {
109
- const originalEnvoyUrl = process.env.ENVOY_URL;
110
- const originalHostname = process.env.HOSTNAME;
111
- process.env.ENVOY_URL = "http://127.0.0.1:59999";
112
- delete process.env.HOSTNAME;
113
-
114
- try {
115
- const pluginModule = await import("../server");
116
- const initPlugin = pluginModule.default;
117
- const hooks = await initPlugin({
118
- serverUrl: new URL("http://127.0.0.1:13381"),
119
- } as never);
120
-
121
- const result = await hooks.tool.envoy_whoami.execute({}, {
122
- sessionID: "ses_no_hostname",
123
- directory: "/tmp",
124
- metadata: mock(() => {}),
125
- } as never);
126
-
127
- const parsed = JSON.parse(typeof result === "string" ? result : result.output);
128
- expect(parsed.machine_id).toBe("unknown");
129
- } finally {
130
- process.env.ENVOY_URL = originalEnvoyUrl;
131
- if (originalHostname === undefined) {
132
- delete process.env.HOSTNAME;
133
- } else {
134
- process.env.HOSTNAME = originalHostname;
135
- }
136
- }
137
- });
138
- });
139
-
140
- describe("envoy_sessions", () => {
141
- it("rejects with error when Envoy is unavailable", async () => {
142
- const originalEnvoyUrl = process.env.ENVOY_URL;
143
- process.env.ENVOY_URL = "http://127.0.0.1:59999";
144
-
145
- try {
146
- const pluginModule = await import("../server");
147
- const initPlugin = pluginModule.default;
148
- const hooks = await initPlugin({
149
- serverUrl: new URL("http://127.0.0.1:13381"),
150
- } as never);
151
-
152
- await expect(
153
- hooks.tool.envoy_sessions.execute({}, {
154
- sessionID: "ses_test",
155
- directory: "/tmp",
156
- metadata: mock(() => {}),
157
- } as never)
158
- ).rejects.toThrow();
159
- } finally {
160
- process.env.ENVOY_URL = originalEnvoyUrl;
161
- }
162
- });
163
- });
164
-
165
- describe("session title", () => {
166
- it("includes title in follow-up subscribe after session activation", async () => {
167
- const originalEnvoyUrl = process.env.ENVOY_URL;
168
- process.env.ENVOY_URL = "http://127.0.0.1:59999";
169
-
170
- const fetchCalls: { url: string; body?: string }[] = [];
171
- const originalFetch = globalThis.fetch;
172
- globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
173
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
174
- if (init?.body) {
175
- fetchCalls.push({ url, body: init.body as string });
176
- } else {
177
- fetchCalls.push({ url });
178
- }
179
- // Serve API: return session with title
180
- if (url.includes("/session/ses_title_test")) {
181
- return new Response(JSON.stringify({ id: "ses_title_test", title: "Test Title" }), {
182
- status: 200,
183
- headers: { "Content-Type": "application/json" },
184
- });
185
- }
186
- // Envoy subscribe calls: return success
187
- if (url.includes("/v1/interests/subscribe")) {
188
- return new Response(JSON.stringify({ session_id: "ses_title_test", topics: [] }), {
189
- status: 200,
190
- headers: { "Content-Type": "application/json" },
191
- });
192
- }
193
- // Port resolution calls
194
- if (url.includes("/session") && !url.includes("ses_title_test")) {
195
- return new Response("not found", { status: 404 });
196
- }
197
- throw new Error("connection refused");
198
- }) as typeof fetch;
199
-
200
- try {
201
- const pluginModule = await import("../server");
202
- const hooks = await pluginModule.default({
203
- serverUrl: new URL("http://127.0.0.1:13381/"),
204
- } as never);
205
-
206
- await hooks.event({
207
- event: {
208
- type: "session.status",
209
- properties: {
210
- sessionID: "ses_title_test",
211
- status: { type: "busy" },
212
- },
213
- },
214
- });
215
-
216
- // Allow async title fetch and follow-up subscribe to complete
217
- await new Promise((r) => setTimeout(r, 500));
218
-
219
- const subscribeCalls = fetchCalls.filter(
220
- (c) => c.url.includes("/v1/interests/subscribe") && c.body
221
- );
222
- const hasTitle = subscribeCalls.some((c) => {
223
- const body = JSON.parse(c.body as string);
224
- return body.title === "Test Title";
225
- });
226
- expect(hasTitle).toBe(true);
227
- } finally {
228
- globalThis.fetch = originalFetch;
229
- process.env.ENVOY_URL = originalEnvoyUrl;
230
- }
231
- });
232
- });
233
-
234
- describe("heartbeat refreshes all busy sessions (fix 1a)", () => {
235
- it("re-subscribes every session that has been busy, not just the most recent", async () => {
236
- const originalEnvoyUrl = process.env.ENVOY_URL;
237
- const originalHb = process.env.ENVOY_HEARTBEAT_MS;
238
- process.env.ENVOY_URL = "http://127.0.0.1:59999";
239
- process.env.ENVOY_HEARTBEAT_MS = "40";
240
-
241
- const subs: { id: string; t: number }[] = [];
242
- const originalFetch = globalThis.fetch;
243
- globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
244
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
245
- if (url.includes("/v1/interests/subscribe") && init?.body) {
246
- const body = JSON.parse(init.body as string);
247
- subs.push({ id: body.session_id, t: Date.now() });
248
- return new Response(JSON.stringify({ session_id: body.session_id, topics: [] }), {
249
- status: 200,
250
- headers: { "Content-Type": "application/json" },
251
- });
252
- }
253
- if (url.includes("/v1/sessions")) {
254
- return new Response(JSON.stringify([]), {
255
- status: 200,
256
- headers: { "Content-Type": "application/json" },
257
- });
258
- }
259
- // Serve title lookups -> 404 (no title, avoids follow-up subscribe noise)
260
- if (url.includes("/session/")) return new Response("not found", { status: 404 });
261
- throw new Error("connection refused");
262
- }) as typeof fetch;
263
-
264
- let dispose: (() => void) | undefined;
265
- try {
266
- const pluginModule = await import("../server");
267
- const hooks = await pluginModule.default({
268
- serverUrl: new URL("http://127.0.0.1:13381/"),
269
- } as never);
270
- dispose = (hooks as { dispose?: () => void }).dispose;
271
-
272
- const busy = (id: string) =>
273
- hooks.event({
274
- event: {
275
- type: "session.status",
276
- properties: { sessionID: id, status: { type: "busy" } },
277
- },
278
- });
279
- await busy("ses_A");
280
- await busy("ses_B");
281
-
282
- // Settle (< one heartbeat tick): capture ses_A's count before heartbeats run
283
- await new Promise((r) => setTimeout(r, 30));
284
- const aStart = subs.filter((s) => s.id === "ses_A").length;
285
-
286
- // ~4 heartbeat ticks at 40ms
287
- await new Promise((r) => setTimeout(r, 180));
288
- const aEnd = subs.filter((s) => s.id === "ses_A").length;
289
- const bEnd = subs.filter((s) => s.id === "ses_B").length;
290
-
291
- // ses_A is now idle (ses_B is the most-recently-busy). The heartbeat must
292
- // keep refreshing ses_A's registration, not only ses_B's.
293
- expect(aEnd).toBeGreaterThan(aStart);
294
- expect(bEnd).toBeGreaterThan(1);
295
- } finally {
296
- dispose?.();
297
- globalThis.fetch = originalFetch;
298
- process.env.ENVOY_URL = originalEnvoyUrl;
299
- if (originalHb === undefined) delete process.env.ENVOY_HEARTBEAT_MS;
300
- else process.env.ENVOY_HEARTBEAT_MS = originalHb;
301
- }
302
- });
303
- });
304
-
305
- describe("prunes deleted sessions from the heartbeat (fix 2)", () => {
306
- it("stops re-subscribing a session after session.deleted", async () => {
307
- const originalEnvoyUrl = process.env.ENVOY_URL;
308
- const originalHb = process.env.ENVOY_HEARTBEAT_MS;
309
- process.env.ENVOY_URL = "http://127.0.0.1:59999";
310
- process.env.ENVOY_HEARTBEAT_MS = "40";
311
-
312
- const subs: string[] = [];
313
- const originalFetch = globalThis.fetch;
314
- globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
315
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
316
- if (url.includes("/v1/interests/subscribe") && init?.body) {
317
- const body = JSON.parse(init.body as string);
318
- subs.push(body.session_id);
319
- return new Response(JSON.stringify({ session_id: body.session_id, topics: [] }), {
320
- status: 200,
321
- headers: { "Content-Type": "application/json" },
322
- });
323
- }
324
- if (url.includes("/v1/interests/unsubscribe") || url.includes("/v1/sessions")) {
325
- return new Response(JSON.stringify([]), {
326
- status: 200,
327
- headers: { "Content-Type": "application/json" },
328
- });
329
- }
330
- if (url.includes("/session/")) return new Response("not found", { status: 404 });
331
- throw new Error("connection refused");
332
- }) as typeof fetch;
333
-
334
- let dispose: (() => void) | undefined;
335
- try {
336
- const pluginModule = await import("../server");
337
- const hooks = await pluginModule.default({
338
- serverUrl: new URL("http://127.0.0.1:13381/"),
339
- } as never);
340
- dispose = (hooks as { dispose?: () => void }).dispose;
341
- const busy = (id: string) =>
342
- hooks.event({
343
- event: {
344
- type: "session.status",
345
- properties: { sessionID: id, status: { type: "busy" } },
346
- },
347
- });
348
- await busy("ses_A");
349
- await busy("ses_B");
350
-
351
- await hooks.event({ event: { type: "session.deleted", properties: { sessionID: "ses_A" } } });
352
- // Let any in-flight heartbeat settle, then mark counts.
353
- await new Promise((r) => setTimeout(r, 60));
354
- const aMark = subs.filter((s) => s === "ses_A").length;
355
- const bMark = subs.filter((s) => s === "ses_B").length;
356
-
357
- await new Promise((r) => setTimeout(r, 160));
358
- const aEnd = subs.filter((s) => s === "ses_A").length;
359
- const bEnd = subs.filter((s) => s === "ses_B").length;
360
-
361
- // ses_A was deleted -> heartbeat must stop refreshing it.
362
- expect(aEnd).toBe(aMark);
363
- // ses_B is still alive -> heartbeat keeps refreshing it.
364
- expect(bEnd).toBeGreaterThan(bMark);
365
- } finally {
366
- dispose?.();
367
- globalThis.fetch = originalFetch;
368
- process.env.ENVOY_URL = originalEnvoyUrl;
369
- if (originalHb === undefined) delete process.env.ENVOY_HEARTBEAT_MS;
370
- else process.env.ENVOY_HEARTBEAT_MS = originalHb;
371
- }
372
- });
373
- });
374
-
375
- describe("invalid ENVOY_HEARTBEAT_MS falls back to the default (fix 6)", () => {
376
- it("does not hammer subscribe when the env value is negative", async () => {
377
- const originalEnvoyUrl = process.env.ENVOY_URL;
378
- const originalHb = process.env.ENVOY_HEARTBEAT_MS;
379
- process.env.ENVOY_URL = "http://127.0.0.1:59999";
380
- process.env.ENVOY_HEARTBEAT_MS = "-5";
381
-
382
- const subs: string[] = [];
383
- const originalFetch = globalThis.fetch;
384
- globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
385
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
386
- if (url.includes("/v1/interests/subscribe") && init?.body) {
387
- const body = JSON.parse(init.body as string);
388
- subs.push(body.session_id);
389
- return new Response(JSON.stringify({ session_id: body.session_id, topics: [] }), {
390
- status: 200,
391
- headers: { "Content-Type": "application/json" },
392
- });
393
- }
394
- if (url.includes("/v1/sessions")) {
395
- return new Response(JSON.stringify([]), {
396
- status: 200,
397
- headers: { "Content-Type": "application/json" },
398
- });
399
- }
400
- if (url.includes("/session/")) return new Response("not found", { status: 404 });
401
- throw new Error("connection refused");
402
- }) as typeof fetch;
403
-
404
- try {
405
- const pluginModule = await import("../server");
406
- const hooks = await pluginModule.default({
407
- serverUrl: new URL("http://127.0.0.1:13381/"),
408
- } as never);
409
- await hooks.event({
410
- event: {
411
- type: "session.status",
412
- properties: { sessionID: "ses_A", status: { type: "busy" } },
413
- },
414
- });
415
- await new Promise((r) => setTimeout(r, 200));
416
-
417
- // A negative interval must NOT be honored (would hammer); only the initial
418
- // subscribe should have happened within this window.
419
- expect(subs.filter((s) => s === "ses_A").length).toBe(1);
420
- } finally {
421
- globalThis.fetch = originalFetch;
422
- process.env.ENVOY_URL = originalEnvoyUrl;
423
- if (originalHb === undefined) delete process.env.ENVOY_HEARTBEAT_MS;
424
- else process.env.ENVOY_HEARTBEAT_MS = originalHb;
425
- }
426
- });
427
- });
428
-
429
- describe("tool.execute.after auto-subscribes the caller to dispatch threads (AC#4)", () => {
430
- async function runHook(tool: string, output: string): Promise<string[][]> {
431
- const originalEnvoyUrl = process.env.ENVOY_URL;
432
- process.env.ENVOY_URL = "http://127.0.0.1:59999";
433
- const subscribed: string[][] = [];
434
- const originalFetch = globalThis.fetch;
435
- globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
436
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
437
- if (url.includes("/v1/interests/subscribe") && init?.body) {
438
- const body = JSON.parse(init.body as string) as { session_id: string; topics: string[] };
439
- subscribed.push([body.session_id, ...body.topics]);
440
- return new Response(JSON.stringify({ topics: [] }), {
441
- status: 200,
442
- headers: { "Content-Type": "application/json" },
443
- });
444
- }
445
- if (url.includes("/session/")) return new Response("not found", { status: 404 });
446
- return new Response("[]", { status: 200, headers: { "Content-Type": "application/json" } });
447
- }) as typeof fetch;
448
- try {
449
- const pluginModule = await import("../server");
450
- const hooks = await pluginModule.default({
451
- serverUrl: new URL("http://127.0.0.1:13381/"),
452
- } as never);
453
- const after = hooks["tool.execute.after"];
454
- expect(after).toBeDefined();
455
- await after?.(
456
- { tool, sessionID: "ses_dispatch", callID: "call_1", args: {} },
457
- { title: "Dispatch", output, metadata: {} }
458
- );
459
- return subscribed;
460
- } finally {
461
- globalThis.fetch = originalFetch;
462
- process.env.ENVOY_URL = originalEnvoyUrl;
463
- }
464
- }
465
-
466
- it("subscribes the calling session to the new thread's GitHub topic", async () => {
467
- const output = JSON.stringify({
468
- thread: 742,
469
- url: "https://github.com/sjawhar/legion/issues/742",
470
- });
471
- const subscribed = await runHook("envoy_dispatch", output);
472
- expect(subscribed).toContainEqual([
473
- "ses_dispatch",
474
- "notifications.github.sjawhar.legion.issue.742.>",
475
- ]);
476
- });
477
-
478
- it("does not subscribe for unrelated tools", async () => {
479
- const output = JSON.stringify({
480
- url: "https://github.com/sjawhar/legion/issues/9",
481
- });
482
- const subscribed = await runHook("envoy_subscribe", output);
483
- expect(subscribed.length).toBe(0);
484
- });
485
- });
486
-
487
- // Several live processes can hold the same session (opencode session state is on
488
- // shared disk). Envoy arbitrates competing route claims by whether the claiming
489
- // process is DRIVING the session, so the plugin must report that honestly:
490
- // sessions that have run in this process are driven; siblings re-adopted after a
491
- // serve restart are recovery claims that must not displace a live driver.
492
- describe("claims report whether this process drives the session", () => {
493
- it("marks sessions that have been busy in this process as driving", async () => {
494
- const originalEnvoyUrl = process.env.ENVOY_URL;
495
- process.env.ENVOY_URL = "http://127.0.0.1:59999";
496
-
497
- const claims: { id: string; driving: unknown }[] = [];
498
- const originalFetch = globalThis.fetch;
499
- globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
500
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
501
- if (url.includes("/v1/interests/subscribe") && init?.body) {
502
- const body = JSON.parse(init.body as string);
503
- claims.push({ id: body.session_id, driving: body.driving });
504
- return new Response(JSON.stringify({ session_id: body.session_id, topics: [] }), {
505
- status: 200,
506
- headers: { "Content-Type": "application/json" },
507
- });
508
- }
509
- if (url.includes("/v1/sessions")) {
510
- return new Response(JSON.stringify([]), {
511
- status: 200,
512
- headers: { "Content-Type": "application/json" },
513
- });
514
- }
515
- if (url.includes("/session/")) return new Response("not found", { status: 404 });
516
- throw new Error("connection refused");
517
- }) as typeof fetch;
518
-
519
- let dispose: (() => void) | undefined;
520
- try {
521
- const pluginModule = await import("../server");
522
- const hooks = await pluginModule.default({
523
- serverUrl: new URL("http://127.0.0.1:13381/"),
524
- } as never);
525
- dispose = (hooks as { dispose?: () => void }).dispose;
526
-
527
- await hooks.event({
528
- event: {
529
- type: "session.status",
530
- properties: { sessionID: "ses_driven", status: { type: "busy" } },
531
- },
532
- });
533
- await new Promise((r) => setTimeout(r, 30));
534
-
535
- const own = claims.filter((c) => c.id === "ses_driven");
536
- expect(own.length).toBeGreaterThan(0);
537
- expect(own.every((c) => c.driving === true)).toBe(true);
538
- } finally {
539
- dispose?.();
540
- globalThis.fetch = originalFetch;
541
- process.env.ENVOY_URL = originalEnvoyUrl;
542
- }
543
- });
544
- });
545
-
546
- // Serve-restart recovery must not hijack sessions that a LIVE process still
547
- // serves. Because opencode session state is on shared disk and every `oc -s`
548
- // launch is its own process, a new process in a shared directory re-pointed
549
- // every sibling session's route at itself (observed: 231 sessions claimed by one
550
- // process in a single burst, then refreshed every 2 minutes). Envoy then
551
- // delivers there, and that process starts its own model loop on a session
552
- // another process owns — two loops, one transcript.
553
- //
554
- // A process may therefore claim ONLY sessions it has actually run. Keeping
555
- // idle-but-owned sessions reachable is the daemon's job (it knows the serve port
556
- // and the session IDs it dispatched), not something a stranger process may
557
- // arrange by adopting routes.
558
- describe("a process claims only sessions it has run", () => {
559
- const runReadopt = async (siblingPortAlive: boolean) => {
560
- const originalEnvoyUrl = process.env.ENVOY_URL;
561
- process.env.ENVOY_URL = "http://127.0.0.1:59999";
562
- const siblingPort = 34751;
563
-
564
- const subscribed: string[] = [];
565
- const originalFetch = globalThis.fetch;
566
- globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
567
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
568
- if (url.includes("/v1/interests/subscribe") && init?.body) {
569
- const body = JSON.parse(init.body as string);
570
- subscribed.push(body.session_id);
571
- return new Response(JSON.stringify({ session_id: body.session_id, topics: [] }), {
572
- status: 200,
573
- headers: { "Content-Type": "application/json" },
574
- });
575
- }
576
- if (url.includes("/v1/sessions")) {
577
- return new Response(
578
- JSON.stringify([
579
- { session_id: "ses_self", machine_id: "m", dir: process.cwd(), port: 42145 },
580
- {
581
- session_id: "ses_sibling",
582
- machine_id: "m",
583
- dir: process.cwd(),
584
- port: siblingPort,
585
- },
586
- ]),
587
- { status: 200, headers: { "Content-Type": "application/json" } }
588
- );
589
- }
590
- // Any liveness probe at all means readopt is still trying to adopt.
591
- if (url.includes(`:${siblingPort}/`)) {
592
- if (siblingPortAlive) {
593
- return new Response(JSON.stringify({ healthy: true }), {
594
- status: 200,
595
- headers: { "Content-Type": "application/json" },
596
- });
597
- }
598
- throw new Error("connection refused");
599
- }
600
- if (url.includes("/session/")) return new Response("not found", { status: 404 });
601
- throw new Error("connection refused");
602
- }) as typeof fetch;
603
-
604
- let dispose: (() => void) | undefined;
605
- try {
606
- const pluginModule = await import("../server");
607
- const hooks = await pluginModule.default({
608
- serverUrl: new URL("http://127.0.0.1:13381/"),
609
- } as never);
610
- dispose = (hooks as { dispose?: () => void }).dispose;
611
- await hooks.event({
612
- event: {
613
- type: "session.status",
614
- properties: { sessionID: "ses_self", status: { type: "busy" } },
615
- },
616
- });
617
- await new Promise((r) => setTimeout(r, 80));
618
- return subscribed;
619
- } finally {
620
- dispose?.();
621
- globalThis.fetch = originalFetch;
622
- process.env.ENVOY_URL = originalEnvoyUrl;
623
- }
624
- };
625
-
626
- it("never claims a sibling session, whether or not its serve is alive", async () => {
627
- for (const siblingServeAlive of [true, false]) {
628
- const subscribed = await runReadopt(siblingServeAlive);
629
-
630
- expect(subscribed).toContain("ses_self");
631
- expect(subscribed).not.toContain("ses_sibling");
632
- }
633
- });
634
- });