From 9008031e96e3bdd04ec6d2bf8bf240ba96011a3a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 4 May 2026 09:07:21 -0700 Subject: [PATCH] fix(qa-channel): settle aborted bus polls --- extensions/qa-channel/src/bus-client.test.ts | 43 ++++++++++++++++++++ extensions/qa-channel/src/bus-client.ts | 4 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/extensions/qa-channel/src/bus-client.test.ts b/extensions/qa-channel/src/bus-client.test.ts index 10a4baf268c..24e3f336627 100644 --- a/extensions/qa-channel/src/bus-client.test.ts +++ b/extensions/qa-channel/src/bus-client.test.ts @@ -1,4 +1,5 @@ import { createServer } from "node:http"; +import { setTimeout as sleep } from "node:timers/promises"; import { afterEach, describe, expect, it } from "vitest"; import { buildQaTarget, getQaBusState, parseQaTarget, pollQaBus } from "./bus-client.js"; @@ -69,6 +70,48 @@ describe("qa-bus client", () => { ).rejects.toThrow(SyntaxError); }); + it("rejects immediately when a poll request is aborted", async () => { + const server = createServer((_req, _res) => { + // Keep the request open so the client abort path owns the outcome. + }); + + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => resolve()); + }); + + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("test server failed to bind"); + } + + stops.push(async () => { + server.closeAllConnections?.(); + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + }); + + const abort = new AbortController(); + const request = pollQaBus({ + baseUrl: `http://127.0.0.1:${address.port}`, + accountId: "acct-a", + cursor: 0, + timeoutMs: 30_000, + signal: abort.signal, + }); + abort.abort(); + + await expect( + Promise.race([ + request, + sleep(500).then(() => { + throw new Error("poll abort did not settle"); + }), + ]), + ).rejects.toMatchObject({ name: "AbortError" }); + }); + it("preserves baseUrl path prefixes when composing bus URLs", async () => { const server = await startJsonServer((req) => ({ statusCode: req.url === "/qa-bus/v1/state" ? 200 : 404, diff --git a/extensions/qa-channel/src/bus-client.ts b/extensions/qa-channel/src/bus-client.ts index a1c4bffbe3d..7fba6036118 100644 --- a/extensions/qa-channel/src/bus-client.ts +++ b/extensions/qa-channel/src/bus-client.ts @@ -95,7 +95,9 @@ async function postJson( ); const onAbort = () => { - request.destroy(abortError()); + const error = abortError(); + request.destroy(error); + reject(error); }; signal?.addEventListener("abort", onAbort, { once: true }); request.on("error", (error) => {