From 825d4ed9c72233bc862668fec6010862bcddadea Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 19 Sep 2026 21:47:38 +0900 Subject: [PATCH] test(devin): assert which error the refused send raises and what the caller sees Two follow-ups to #5152, both test-only. Nothing in production changes and the merged branch is untouched. The adapter-direct case caught the refusal with a bare catch that discarded it, so it passed for any error at all. If the adapter had thrown an ordinary upstream failure instead of the budget refusal, the case would still have been green while the turn was reported to the operator as a provider problem rather than a budget decision. It now pins SendBudgetExhaustedError. The outer case reported the HTTP status only inside its failure message. It is now asserted: the refusal reaches the client as an error code on a buffered failed response, so the status is 200. That is worth stating, because the obvious guess is 429 and this path does not use it. Local checks: NOT RUN. Hosted CI on this PR is the executable evidence. --- tests/adapters/adapter-inner-send-budget-wiring.test.ts | 9 ++++++++- tests/responses/responses-send-budget-counts.test.ts | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/adapters/adapter-inner-send-budget-wiring.test.ts b/tests/adapters/adapter-inner-send-budget-wiring.test.ts index 9885c73af9..c5341a759f 100644 --- a/tests/adapters/adapter-inner-send-budget-wiring.test.ts +++ b/tests/adapters/adapter-inner-send-budget-wiring.test.ts @@ -13,6 +13,7 @@ import { createDevinAdapter, DEVIN_API_SERVER } from "../../src/adapters/devin"; import { setCachedCatalogForTests } from "../../src/adapters/devin/cloud-direct/catalog"; import { resetKiroThrottleStateForTests } from "../../src/adapters/kiro-retry"; import type { AdapterFetchContext } from "../../src/adapters/base"; +import { SendBudgetExhaustedError } from "../../src/lib/upstream-retry"; import { encodeMessage } from "../../src/lib/eventstream-decoder"; import { createRequestExecutionBudget, type RequestExecutionBudgetPolicy } from "../../src/lib/request-execution-budget"; import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../src/types"; @@ -206,6 +207,7 @@ describe("Devin runTurn execution wiring", () => { adapter: "devin", baseUrl: DEVIN_API_SERVER, apiKey, } as unknown as OcxProviderConfig); + let refusal: unknown; try { await adapter.runTurn?.( { @@ -223,7 +225,7 @@ describe("Devin runTurn execution wiring", () => { onPhysicalSend: send => { observed.push(send); }, }, event => events.push(event), - ).catch(() => { /* the refusal escapes the adapter for the caller to map */ }); + ).catch((error: unknown) => { refusal = error; }); } finally { setCachedCatalogForTests(null); if (previousHome === undefined) delete process.env.OPENCODEX_HOME; @@ -233,6 +235,11 @@ describe("Devin runTurn execution wiring", () => { removeTreeWithRetry(home); } + // The refusal escapes the adapter for the caller to map, and WHICH error escapes is the + // contract: a bare catch here passed even when the adapter threw something else entirely, + // which would have left the turn reported as an ordinary upstream failure instead of a + // budget refusal. + expect(refusal).toBeInstanceOf(SendBudgetExhaustedError); expect(urls.filter(url => url.includes("GetChatMessage"))).toHaveLength(0); expect(observed).toEqual([]); expect(budget.used).toBe(0); diff --git a/tests/responses/responses-send-budget-counts.test.ts b/tests/responses/responses-send-budget-counts.test.ts index d6119caa0f..7585db3728 100644 --- a/tests/responses/responses-send-budget-counts.test.ts +++ b/tests/responses/responses-send-budget-counts.test.ts @@ -217,12 +217,17 @@ describe("upstream sends per logical request", () => { // without proving the refusal was recorded against the turn that was refused. expect(attempts).toHaveLength(1); expect({ + // Pinned, not merely reported in the failure message. The refusal reaches the client as + // an error code on a buffered FAILED response, so the outer HTTP status stays 200; a + // reader who assumes 429 here would be describing a transport this path never uses. + status: response.status, adapter: attempts[0]?.adapter, sendCount: attempts[0]?.sendCount, chatCalls: urls.filter(url => url.includes("GetChatMessage")).length, totalSends: totalSends(logCtx), refused: body.includes("request_send_budget_exhausted"), }, `status ${response.status}: ${body.slice(0, 240)}`).toEqual({ + status: 200, adapter: "devin", sendCount: 0, chatCalls: 0,