Skip to content

Commit 480ed35

Browse files
fix(test): extend provider-http mock with readProviderJsonResponse (#96036)
Addresses the ClawSweeper P1 finding that `src/plugin-sdk/test-helpers/provider-http-mocks.ts` mocks the `openclaw/plugin-sdk/provider-http` module without exporting `readProviderJsonResponse`, causing import-time failure in Qwen/Alibaba video tests now that `dashscope-compatible.ts` routes submit/poll through the bounded helper. Changes: - `src/plugin-sdk/test-helpers/provider-http-mocks.ts` (+22): - Added `readProviderJsonResponse` type import - Added `ReadProviderJsonResponseParams` type alias - Added `readProviderJsonResponseMock` field to `ProviderHttpMocks` - Added hoisted mock default implementation that calls `response.json()` (parity with the legacy unbounded read, leaves cap enforcement to the production helper) - Added to `vi.mock(openclaw/plugin-sdk/provider-http, ...)` export map - Added to `installProviderHttpMockCleanup` mockClear list - `scripts/repro/issue-dashscope-response-cap.mjs` (+100): - Existing helper-only assertion kept (proves helper stops early) - New assertion: drives `pollDashscopeVideoTaskUntilComplete` end-to-end with a 100 MiB streaming body, asserts bounded-reader error surfaces before the runtime buffers the full payload - New assertion: drives `runDashscopeVideoGenerationTask` end-to-end with a 100 MiB streaming body on the submit response, asserts the submit path is also bounded - Negative control kept (raw `response.json()` buffers full body) Verified on commit: - `node scripts/run-vitest.mjs extensions/qwen/video-generation-provider.test.ts` — 5/5 passed (was 3/5 fail before this fix) - `node scripts/run-vitest.mjs extensions/alibaba/video-generation-provider.test.ts` — 3/3 passed - `node scripts/run-vitest.mjs src/video-generation/dashscope-compatible.bounded-json.test.ts` — 4/4 passed - `pnpm exec tsx scripts/repro/issue-dashscope-response-cap.mjs` — 4/4 PASS (helper + poll path + submit path + negative control) - `npx oxlint --import-plugin --config .oxlintrc.json` on changed files — exit 0 - `node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.extensions.test.json` — exit 0
1 parent ee14da9 commit 480ed35

2 files changed

Lines changed: 120 additions & 6 deletions

File tree

scripts/repro/issue-dashscope-response-cap.mjs

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,24 @@
99
* provider JSON response cap (16 MiB).
1010
* 2. Drives the production `readProviderJsonResponse` helper from
1111
* `src/agents/provider-http-errors.ts` (the same helper the dashscope
12-
* poll and submit call sites now route through).
13-
* 3. Asserts the helper throws a `JSON response exceeds` error and that
14-
* the response body is cancelled before all 32 chunks are pulled —
15-
* proving we do not buffer the entire payload before failing.
12+
* poll and submit call sites now route through) — proves the helper
13+
* stops early.
14+
* 3. Drives the changed call sites (`pollDashscopeVideoTaskUntilComplete`
15+
* and `runDashscopeVideoGenerationTask`) end-to-end with a streaming
16+
* body larger than the cap — proves the PR's actual fix surfaces the
17+
* bounded-reader error on the real production paths, not just on the
18+
* helper directly.
1619
* 4. Compares the bounded read to a raw `await response.json()` on a
1720
* smaller 2 MiB body so the difference is observable in a one-shot
1821
* repro script: bounded read errors with the cap message, raw read
1922
* buffers and then fails on JSON parse.
2023
*/
2124
import assert from "node:assert/strict";
2225
import { readProviderJsonResponse } from "../../src/agents/provider-http-errors.js";
26+
import {
27+
pollDashscopeVideoTaskUntilComplete,
28+
runDashscopeVideoGenerationTask,
29+
} from "../../src/video-generation/dashscope-compatible.js";
2330

2431
const PROVIDER_JSON_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
2532

@@ -74,11 +81,98 @@ function createStreamingJsonResponse({ chunkCount, chunkSize }) {
7481
`bounded reader must stop early; got ${readCount} / ${chunkCount} reads`,
7582
);
7683
console.log(
77-
`PASS bounded read: threw "${error.message}" after ${readCount} / ${chunkCount} chunks (stopped ~${(readCount * chunkSize) / (1024 * 1024)} MiB into a ${(chunkCount * chunkSize) / (1024 * 1024)} MiB body)`,
84+
`PASS bounded read on helper: threw "${error.message}" after ${readCount} / ${chunkCount} chunks (stopped ~${(readCount * chunkSize) / (1024 * 1024)} MiB into a ${(chunkCount * chunkSize) / (1024 * 1024)} MiB body)`,
7885
);
7986
}
8087

81-
// 2) Negative control: raw response.json() on a smaller but still invalid body
88+
// 2) Drive the changed poll call site end-to-end with a streaming body
89+
// larger than the cap. The bounded reader must throw before the runtime
90+
// buffers the whole payload — proves the PR's fix is wired into the
91+
// production path, not just sitting on the helper.
92+
{
93+
const chunkCount = 100;
94+
const chunkSize = 1024 * 1024;
95+
const streamed = createStreamingJsonResponse({ chunkCount, chunkSize });
96+
const fetchMock = () => Promise.resolve(streamed.response);
97+
const headers = new Headers({ authorization: "Bearer test" });
98+
99+
let error;
100+
try {
101+
await pollDashscopeVideoTaskUntilComplete({
102+
providerLabel: "dashscope",
103+
taskId: "task_overflow",
104+
headers,
105+
fetchFn: fetchMock,
106+
baseUrl: "https://dashscope.aliyuncs.com",
107+
});
108+
} catch (cause) {
109+
error = cause;
110+
}
111+
112+
assert.ok(error, "expected pollDashscopeVideoTaskUntilComplete to throw on oversize body");
113+
assert.match(
114+
error.message,
115+
/JSON response exceeds/,
116+
`poll path must surface bounded-reader error, got: ${error.message}`,
117+
);
118+
const readCount = streamed.getReadCount();
119+
assert.ok(
120+
readCount < chunkCount,
121+
`bounded reader on poll path must stop early; got ${readCount} / ${chunkCount} reads`,
122+
);
123+
console.log(
124+
`PASS poll call site: threw "${error.message}" after ${readCount} / ${chunkCount} chunks`,
125+
);
126+
}
127+
128+
// 3) Drive the changed submit call site end-to-end with a streaming body
129+
// larger than the cap. The bounded reader must throw before the runtime
130+
// buffers the whole payload — proves the submit path is also bounded.
131+
{
132+
const chunkCount = 100;
133+
const chunkSize = 1024 * 1024;
134+
const streamed = createStreamingJsonResponse({ chunkCount, chunkSize });
135+
const fetchMock = () => Promise.resolve(streamed.response);
136+
const headers = new Headers({ authorization: "Bearer test" });
137+
138+
let error;
139+
try {
140+
await runDashscopeVideoGenerationTask({
141+
providerLabel: "dashscope",
142+
model: "wan2.5-t2v-preview",
143+
req: {
144+
provider: "dashscope",
145+
model: "wan2.5-t2v-preview",
146+
prompt: "draw a square",
147+
cfg: {},
148+
},
149+
url: "https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis",
150+
headers,
151+
baseUrl: "https://dashscope.aliyuncs.com",
152+
fetchFn: fetchMock,
153+
defaultTimeoutMs: 60_000,
154+
});
155+
} catch (cause) {
156+
error = cause;
157+
}
158+
159+
assert.ok(error, "expected runDashscopeVideoGenerationTask to throw on oversize body");
160+
assert.match(
161+
error.message,
162+
/JSON response exceeds/,
163+
`submit path must surface bounded-reader error, got: ${error.message}`,
164+
);
165+
const readCount = streamed.getReadCount();
166+
assert.ok(
167+
readCount < chunkCount,
168+
`bounded reader on submit path must stop early; got ${readCount} / ${chunkCount} reads`,
169+
);
170+
console.log(
171+
`PASS submit call site: threw "${error.message}" after ${readCount} / ${chunkCount} chunks`,
172+
);
173+
}
174+
175+
// 4) Negative control: raw response.json() on a smaller but still invalid body
82176
// buffers the whole payload before failing on JSON parse — proves the
83177
// bounded read is the right shape.
84178
{

src/plugin-sdk/test-helpers/provider-http-mocks.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
fetchWithTimeoutGuarded,
99
pollProviderOperationJson,
1010
postMultipartRequest,
11+
readProviderJsonResponse,
1112
resolveProviderHttpRequestConfig,
1213
sanitizeConfiguredModelProviderRequest,
1314
} from "../provider-http.js";
@@ -23,6 +24,7 @@ type FetchProviderDownloadResponseParams = Parameters<typeof fetchProviderDownlo
2324
type SanitizeConfiguredModelProviderRequestParams = Parameters<
2425
typeof sanitizeConfiguredModelProviderRequest
2526
>[0];
27+
type ReadProviderJsonResponseParams = Parameters<typeof readProviderJsonResponse>;
2628

2729
type ResolveProviderHttpRequestConfigResult = {
2830
baseUrl: string;
@@ -48,6 +50,13 @@ interface ProviderHttpMocks {
4850
request: SanitizeConfiguredModelProviderRequestParams,
4951
) => SanitizeConfiguredModelProviderRequestParams
5052
>;
53+
readProviderJsonResponseMock: Mock<
54+
(
55+
response: Response,
56+
label: string,
57+
opts?: ReadProviderJsonResponseParams[2],
58+
) => Promise<unknown>
59+
>;
5160
resolveProviderHttpRequestConfigMock: Mock<
5261
(params: ResolveProviderHttpRequestConfigParams) => ResolveProviderHttpRequestConfigResult
5362
>;
@@ -68,6 +77,7 @@ const providerHttpMocks = vi.hoisted(() => ({
6877
sanitizeConfiguredModelProviderRequestMock: vi.fn(
6978
(request: SanitizeConfiguredModelProviderRequestParams) => request,
7079
),
80+
readProviderJsonResponseMock: vi.fn(),
7181
resolveProviderHttpRequestConfigMock: vi.fn((params: ResolveProviderHttpRequestConfigParams) => ({
7282
baseUrl: params.baseUrl ?? params.defaultBaseUrl,
7383
allowPrivateNetwork:
@@ -164,6 +174,14 @@ providerHttpMocks.fetchProviderOperationResponseMock.mockImplementation(
164174
},
165175
);
166176

177+
providerHttpMocks.readProviderJsonResponseMock.mockImplementation(async (response: Response) => {
178+
// Default mock mirrors the legacy raw `response.json()` so tests that
179+
// don't care about the bounded cap still pass. Tests that exercise the
180+
// bounded behavior install their own mock implementation (or use the
181+
// bounded-json test file directly against the production helper).
182+
return await response.json();
183+
});
184+
167185
providerHttpMocks.fetchProviderDownloadResponseMock.mockImplementation(
168186
async (params: FetchProviderDownloadResponseParams) => {
169187
const response = await providerHttpMocks.fetchWithTimeoutMock(
@@ -233,6 +251,7 @@ vi.mock("openclaw/plugin-sdk/provider-http", () => ({
233251
pollProviderOperationJson: providerHttpMocks.pollProviderOperationJsonMock,
234252
postJsonRequest: providerHttpMocks.postJsonRequestMock,
235253
postMultipartRequest: providerHttpMocks.postMultipartRequestMock,
254+
readProviderJsonResponse: providerHttpMocks.readProviderJsonResponseMock,
236255
providerOperationRetryConfig: (_stage: string) => true,
237256
resolveProviderOperationTimeoutMs: ({ defaultTimeoutMs }: { defaultTimeoutMs: number }) =>
238257
defaultTimeoutMs,
@@ -261,5 +280,6 @@ export function installProviderHttpMockCleanup(): void {
261280
providerHttpMocks.assertOkOrThrowProviderErrorMock.mockClear();
262281
providerHttpMocks.sanitizeConfiguredModelProviderRequestMock.mockClear();
263282
providerHttpMocks.resolveProviderHttpRequestConfigMock.mockClear();
283+
providerHttpMocks.readProviderJsonResponseMock.mockClear();
264284
});
265285
}

0 commit comments

Comments
 (0)