From 164db42d0fbe62bf123965c5dd288db1bda955fe Mon Sep 17 00:00:00 2001 From: William Valentin Date: Mon, 16 Feb 2026 21:58:36 -0800 Subject: [PATCH] test(companion): cover waitForIdle pending RPC lifecycle --- docs/plans/state.json | 11 ++++++++ src/companion/runtimeClient.test.ts | 43 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/docs/plans/state.json b/docs/plans/state.json index 4735f1c..3845dd7 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -990,6 +990,17 @@ ], "test_status": "pnpm test:run src/companion/platformClients.integration.test.ts src/companion/platformClients.test.ts src/companion/runtimeClient.test.ts src/companion/heartbeatLoop.test.ts + pnpm typecheck passing" }, + "companion-runtime-wait-for-idle-pending-rpc-coverage": { + "status": "completed", + "date": "2026-02-17", + "updated": "2026-02-17", + "summary": "Added runtime regression coverage for `waitForIdle()` pending-RPC behavior, verifying idle resolution after pending requests are rejected on teardown.", + "files_modified": [ + "src/companion/runtimeClient.test.ts", + "docs/plans/state.json" + ], + "test_status": "pnpm test:run src/companion/runtimeClient.test.ts src/companion/platformClients.test.ts src/companion/heartbeatLoop.test.ts src/companion/platformClients.integration.test.ts + pnpm typecheck passing" + }, "browser-tools-activation-clarity": { "status": "completed", "date": "2026-02-17", diff --git a/src/companion/runtimeClient.test.ts b/src/companion/runtimeClient.test.ts index 96f20f4..15f9111 100644 --- a/src/companion/runtimeClient.test.ts +++ b/src/companion/runtimeClient.test.ts @@ -730,6 +730,49 @@ describe('CompanionRuntimeClient', () => { await pendingWait; }); + it('waitForIdle resolves after pending RPC requests are rejected', async () => { + class FakeWebSocket extends EventEmitter { + readyState: number = WebSocket.CONNECTING; + + constructor() { + super(); + queueMicrotask(() => { + this.readyState = WebSocket.OPEN; + this.emit('open'); + }); + } + + send(_payload: string, callback?: (error?: Error) => void): void { + callback?.(); + } + + close(_code?: number, _reason?: string): void { + this.readyState = WebSocket.CLOSED; + this.emit('close'); + } + } + + const client = new CompanionRuntimeClient({ + url: 'ws://127.0.0.1:1', + websocketFactory: () => new FakeWebSocket() as unknown as WebSocket, + requestTimeoutMs: 10_000, + }); + await client.connect(); + + const pendingCall = client.call('system.capabilities').catch(() => undefined); + expect(client.pendingRequestCount).toBe(1); + + const idle = client.waitForIdle({ timeoutMs: 1_000, pollIntervalMs: 5 }); + setTimeout(() => { + client.disconnect(); + }, 20); + + await expect(idle).resolves.toBeUndefined(); + await pendingCall; + expect(client.pendingRequestCount).toBe(0); + expect(client.hasPendingWork).toBe(false); + }); + it('waitForIdle supports AbortSignal cancellation', async () => { const client = new CompanionRuntimeClient({ url: 'ws://127.0.0.1:1',