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