From 508496e3de29c08bf5bebf79634a912c229f36fc Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 20:18:18 -0700 Subject: [PATCH 1/2] test(web): a failed stock request carries the hub's error code and message (CL-8422) --- apps/web/src/needs-converge.test.ts | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/apps/web/src/needs-converge.test.ts b/apps/web/src/needs-converge.test.ts index 1e27948f01..45281aa2da 100644 --- a/apps/web/src/needs-converge.test.ts +++ b/apps/web/src/needs-converge.test.ts @@ -6,6 +6,7 @@ import { createFetchStockHub, forkSubThread, StockHubCapabilityError, + StockHubRequestError, type HubSnapshot, type MailMessage, type StockHub, @@ -701,6 +702,36 @@ describe("stock-only fetch hub", () => { }) as typeof fetch; } + function stubFetchRejecting(body: unknown, status: number): typeof fetch { + return (async () => new Response(JSON.stringify(body), { status })) as unknown as typeof fetch; + } + + test("a failed request carries the hub's error code and message", async () => { + const hub = createFetchStockHub( + stubFetchRejecting({ error: { code: "invalid_workflow", message: "entry threw" } }, 409), + ); + + const error = await hub + .deployWorkflow("tnt_1", { + source: { + kind: "asset", + assetId: "ast_1", + package: { format: "source", commitSha: "abc" }, + }, + entry: "./workflow.js", + sourceOfferingIds: ["off_1"], + defaultSourceOfferingId: "off_1", + }) + .then( + () => null, + (cause: unknown) => cause, + ); + expect(error).toBeInstanceOf(StockHubRequestError); + expect((error as StockHubRequestError).message).toBe( + "Stock request deployWorkflow failed with HTTP 409. invalid_workflow: entry threw", + ); + }); + test("mailbox search and thread reads stay typed upstream gaps", async () => { const hub = createFetchStockHub(stubFetch([], {})); From 6ae67f5c39c1626f1c2e16a6292a418accca2cc4 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 20:18:18 -0700 Subject: [PATCH 2/2] fix(web): stock hub request failures include the hub's error message (CL-8422) --- apps/web/src/needs-converge.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/web/src/needs-converge.ts b/apps/web/src/needs-converge.ts index 9938b57121..c1c5d8b9a7 100644 --- a/apps/web/src/needs-converge.ts +++ b/apps/web/src/needs-converge.ts @@ -528,12 +528,21 @@ async function resolveRoleId( return found.id; } +const HubErrorEnvelope = type({ + error: { code: "string", "message?": "string", "userMessage?": "string" }, +}); + async function readJson(response: Response, operation: string): Promise { if (!response.ok) { + const envelope = HubErrorEnvelope(await response.json().catch(() => undefined)); + const reason = + envelope instanceof type.errors + ? "" + : ` ${envelope.error.code}: ${envelope.error.userMessage ?? envelope.error.message ?? ""}`; throw new StockHubRequestError( operation, response.status, - `Stock request ${operation} failed with HTTP ${response.status}.`, + `Stock request ${operation} failed with HTTP ${response.status}.${reason}`, ); } return (await response.json()) as unknown;