

























11// Byteplus tests cover video generation provider plugin behavior.
2-import {
3-getProviderHttpMocks,
4-installProviderHttpMockCleanup,
5-} from "openclaw/plugin-sdk/provider-http-test-mocks";
62import { expectExplicitVideoGenerationCapabilities } from "openclaw/plugin-sdk/provider-test-contracts";
7-import { beforeAll, describe, expect, it, vi } from "vitest";
8-9-const { postJsonRequestMock, fetchWithTimeoutMock } = getProviderHttpMocks();
3+import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
4+5+// Submit/poll transport is mocked locally so each test can inject the BytePlus task JSON
6+// bodies, while readProviderJsonResponse is kept REAL (via importActual) so the byte-bounded
7+// reader actually streams and cancels oversized bodies under test instead of a stub.
8+const { postJsonRequestMock, fetchWithTimeoutMock, resolveApiKeyForProviderMock } = vi.hoisted(
9+() => ({
10+postJsonRequestMock: vi.fn(),
11+fetchWithTimeoutMock: vi.fn(),
12+resolveApiKeyForProviderMock: vi.fn(async () => ({ apiKey: "provider-key" })),
13+}),
14+);
15+16+vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => ({
17+resolveApiKeyForProvider: resolveApiKeyForProviderMock,
18+}));
19+20+vi.mock("openclaw/plugin-sdk/provider-http", async (importActual) => {
21+const actual = await importActual<typeof import("openclaw/plugin-sdk/provider-http")>();
22+const resolveTimeoutMs = (timeoutMs: unknown): number =>
23+typeof timeoutMs === "function" ? (timeoutMs() as number) : ((timeoutMs as number) ?? 60_000);
24+return {
25+// REAL byte-bounded JSON reader under test — not stubbed.
26+readProviderJsonResponse: actual.readProviderJsonResponse,
27+postJsonRequest: postJsonRequestMock,
28+fetchProviderOperationResponse: async (params: {
29+url: string;
30+init?: RequestInit;
31+timeoutMs?: unknown;
32+fetchFn: typeof fetch;
33+}) => fetchWithTimeoutMock(params.url, params.init ?? {}, resolveTimeoutMs(params.timeoutMs)),
34+fetchProviderDownloadResponse: async (params: {
35+url: string;
36+init?: RequestInit;
37+timeoutMs?: unknown;
38+fetchFn: typeof fetch;
39+}) => fetchWithTimeoutMock(params.url, params.init ?? {}, resolveTimeoutMs(params.timeoutMs)),
40+assertOkOrThrowHttpError: async () => {},
41+createProviderOperationDeadline: ({
42+ label,
43+ timeoutMs,
44+}: {
45+label: string;
46+timeoutMs?: number;
47+}) => ({ label, timeoutMs }),
48+createProviderOperationTimeoutResolver:
49+({ defaultTimeoutMs }: { defaultTimeoutMs: number }) =>
50+() =>
51+defaultTimeoutMs,
52+resolveProviderOperationTimeoutMs: ({ defaultTimeoutMs }: { defaultTimeoutMs: number }) =>
53+defaultTimeoutMs,
54+resolveProviderHttpRequestConfig: (params: {
55+baseUrl?: string;
56+defaultBaseUrl: string;
57+allowPrivateNetwork?: boolean;
58+defaultHeaders?: Record<string, string>;
59+}) => ({
60+baseUrl: params.baseUrl ?? params.defaultBaseUrl,
61+allowPrivateNetwork: params.allowPrivateNetwork === true,
62+headers: new Headers(params.defaultHeaders),
63+dispatcherPolicy: undefined,
64+}),
65+waitProviderOperationPollInterval: async () => {},
66+};
67+});
10681169let buildBytePlusVideoGenerationProvider: typeof import("./video-generation-provider.js").buildBytePlusVideoGenerationProvider;
12701371beforeAll(async () => {
1472({ buildBytePlusVideoGenerationProvider } = await import("./video-generation-provider.js"));
1573});
167417-installProviderHttpMockCleanup();
75+afterEach(() => {
76+postJsonRequestMock.mockReset();
77+fetchWithTimeoutMock.mockReset();
78+resolveApiKeyForProviderMock.mockClear();
79+});
18801981function mockSuccessfulBytePlusTask(params?: { model?: string }) {
2082postJsonRequestMock.mockResolvedValue({
21-response: {
22-json: async () => ({
23-id: "task_123",
24-}),
25-},
83+response: streamedJsonResponse({
84+id: "task_123",
85+}),
2686release: vi.fn(async () => {}),
2787});
2888fetchWithTimeoutMock
29-.mockResolvedValueOnce({
30-json: async () => ({
89+.mockResolvedValueOnce(
90+streamedJsonResponse({
3191id: "task_123",
3292status: "succeeded",
3393content: {
3494video_url: "https://example.com/byteplus.mp4",
3595},
3696model: params?.model ?? "seedance-1-0-lite-t2v-250428",
3797}),
38-})
98+)
3999.mockResolvedValueOnce({
40100headers: new Headers({ "content-type": "video/webm" }),
41101arrayBuffer: async () => Buffer.from("webm-bytes"),
@@ -77,6 +137,53 @@ function streamedVideoResponse(bytes: string): Response {
77137);
78138}
79139140+// BytePlus submit/poll task JSON is now read through the byte-bounded reader, so the
141+// mocked responses must expose a real readable body (not just a json() shortcut).
142+function streamedJsonResponse(payload: unknown): Response {
143+return new Response(
144+new ReadableStream({
145+start(controller) {
146+controller.enqueue(new TextEncoder().encode(JSON.stringify(payload)));
147+controller.close();
148+},
149+}),
150+{ status: 200, headers: { "content-type": "application/json" } },
151+);
152+}
153+154+// Builds a JSON body larger than the shared 16 MiB readProviderJsonResponse cap so the
155+// bounded reader cancels the stream mid-flight; if the cap were removed the reader would
156+// buffer the whole advertised payload before parsing. Tracks how many bytes were pulled
157+// and whether the stream was canceled so callers can assert the body was not fully read.
158+function makeOversizedJsonStream(): {
159+body: ReadableStream<Uint8Array>;
160+maxBytes: number;
161+totalBytes: number;
162+state: { bytesPulled: number; canceled: boolean };
163+} {
164+const maxBytes = 16 * 1024 * 1024; // matches PROVIDER_JSON_RESPONSE_MAX_BYTES.
165+const ONE_MIB = 1024 * 1024;
166+const TOTAL_CHUNKS = 32; // 32 MiB advertised body, double the cap.
167+const chunk = new Uint8Array(ONE_MIB);
168+const state = { bytesPulled: 0, canceled: false };
169+let pulled = 0;
170+const body = new ReadableStream<Uint8Array>({
171+pull(controller) {
172+if (pulled >= TOTAL_CHUNKS) {
173+controller.close();
174+return;
175+}
176+pulled += 1;
177+state.bytesPulled += chunk.length;
178+controller.enqueue(chunk);
179+},
180+cancel() {
181+state.canceled = true;
182+},
183+});
184+return { body, maxBytes, totalBytes: TOTAL_CHUNKS * ONE_MIB, state };
185+}
186+80187describe("byteplus video generation provider", () => {
81188it("declares explicit mode capabilities", () => {
82189expectExplicitVideoGenerationCapabilities(buildBytePlusVideoGenerationProvider());
@@ -110,21 +217,19 @@ describe("byteplus video generation provider", () => {
110217111218it("rejects generated video downloads that exceed the configured media cap", async () => {
112219postJsonRequestMock.mockResolvedValue({
113-response: {
114-json: async () => ({ id: "task_too_large" }),
115-},
220+response: streamedJsonResponse({ id: "task_too_large" }),
116221release: vi.fn(async () => {}),
117222});
118223fetchWithTimeoutMock
119-.mockResolvedValueOnce({
120-json: async () => ({
224+.mockResolvedValueOnce(
225+streamedJsonResponse({
121226id: "task_too_large",
122227status: "succeeded",
123228content: {
124229video_url: "https://example.com/too-large.mp4",
125230},
126231}),
127-})
232+)
128233.mockResolvedValueOnce(streamedVideoResponse("too-large"));
129234130235const provider = buildBytePlusVideoGenerationProvider();
@@ -222,24 +327,22 @@ describe("byteplus video generation provider", () => {
222327223328it("drops malformed response duration metadata", async () => {
224329postJsonRequestMock.mockResolvedValue({
225-response: {
226-json: async () => ({
227-id: "task_123",
228-}),
229-},
330+response: streamedJsonResponse({
331+id: "task_123",
332+}),
230333release: vi.fn(async () => {}),
231334});
232335fetchWithTimeoutMock
233-.mockResolvedValueOnce({
234-json: async () => ({
336+.mockResolvedValueOnce(
337+streamedJsonResponse({
235338id: "task_123",
236339status: "succeeded",
237340content: {
238341video_url: "https://example.com/byteplus.mp4",
239342},
240343duration: 1.5,
241344}),
242-})
345+)
243346.mockResolvedValueOnce({
244347headers: new Headers({ "content-type": "video/mp4" }),
245348arrayBuffer: async () => Buffer.from("mp4-bytes"),
@@ -259,11 +362,15 @@ describe("byteplus video generation provider", () => {
259362it("reports malformed create JSON with a provider-owned error", async () => {
260363const release = vi.fn(async () => {});
261364postJsonRequestMock.mockResolvedValue({
262-response: {
263-json: async () => {
264-throw new SyntaxError("bad json");
265-},
266-},
365+response: new Response(
366+new ReadableStream({
367+start(controller) {
368+controller.enqueue(new TextEncoder().encode("{ not valid json"));
369+controller.close();
370+},
371+}),
372+{ status: 200, headers: { "content-type": "application/json" } },
373+),
267374 release,
268375});
269376@@ -281,19 +388,17 @@ describe("byteplus video generation provider", () => {
281388282389it("rejects status responses missing a task status", async () => {
283390postJsonRequestMock.mockResolvedValue({
284-response: {
285-json: async () => ({ id: "task_missing_status" }),
286-},
391+response: streamedJsonResponse({ id: "task_missing_status" }),
287392release: vi.fn(async () => {}),
288393});
289-fetchWithTimeoutMock.mockResolvedValueOnce({
290-json: async () => ({
394+fetchWithTimeoutMock.mockResolvedValueOnce(
395+streamedJsonResponse({
291396id: "task_missing_status",
292397content: {
293398video_url: "https://example.com/byteplus.mp4",
294399},
295400}),
296-});
401+);
297402298403const provider = buildBytePlusVideoGenerationProvider();
299404await expect(
@@ -308,18 +413,16 @@ describe("byteplus video generation provider", () => {
308413309414it("rejects malformed completed content", async () => {
310415postJsonRequestMock.mockResolvedValue({
311-response: {
312-json: async () => ({ id: "task_malformed_content" }),
313-},
416+response: streamedJsonResponse({ id: "task_malformed_content" }),
314417release: vi.fn(async () => {}),
315418});
316-fetchWithTimeoutMock.mockResolvedValueOnce({
317-json: async () => ({
419+fetchWithTimeoutMock.mockResolvedValueOnce(
420+streamedJsonResponse({
318421id: "task_malformed_content",
319422status: "succeeded",
320423content: ["https://example.com/byteplus.mp4"],
321424}),
322-});
425+);
323426324427const provider = buildBytePlusVideoGenerationProvider();
325428await expect(
@@ -331,4 +434,61 @@ describe("byteplus video generation provider", () => {
331434}),
332435).rejects.toThrow("BytePlus video generation completed with malformed content");
333436});
437+438+it("bounds the submit task JSON body and cancels an oversized stream", async () => {
439+const stream = makeOversizedJsonStream();
440+const release = vi.fn(async () => {});
441+postJsonRequestMock.mockResolvedValue({
442+response: new Response(stream.body, {
443+status: 200,
444+headers: { "content-type": "application/json" },
445+}),
446+ release,
447+});
448+449+const provider = buildBytePlusVideoGenerationProvider();
450+await expect(
451+provider.generateVideo({
452+provider: "byteplus",
453+model: "seedance-1-0-lite-t2v-250428",
454+prompt: "oversized submit response",
455+cfg: {},
456+}),
457+).rejects.toThrow(
458+`BytePlus video generation failed: JSON response exceeds ${stream.maxBytes} bytes`,
459+);
460+expect(stream.state.canceled).toBe(true);
461+// Only the bounded prefix is pulled, never the full advertised stream.
462+expect(stream.state.bytesPulled).toBeLessThan(stream.totalBytes);
463+// The submit request must still be released even though the body overflowed.
464+expect(release).toHaveBeenCalledOnce();
465+});
466+467+it("bounds the poll status JSON body and cancels an oversized stream", async () => {
468+postJsonRequestMock.mockResolvedValue({
469+response: streamedJsonResponse({ id: "task_oversized_poll" }),
470+release: vi.fn(async () => {}),
471+});
472+const stream = makeOversizedJsonStream();
473+fetchWithTimeoutMock.mockResolvedValueOnce(
474+new Response(stream.body, {
475+status: 200,
476+headers: { "content-type": "application/json" },
477+}),
478+);
479+480+const provider = buildBytePlusVideoGenerationProvider();
481+await expect(
482+provider.generateVideo({
483+provider: "byteplus",
484+model: "seedance-1-0-lite-t2v-250428",
485+prompt: "oversized poll response",
486+cfg: {},
487+}),
488+).rejects.toThrow(
489+`BytePlus video status request failed: JSON response exceeds ${stream.maxBytes} bytes`,
490+);
491+expect(stream.state.canceled).toBe(true);
492+expect(stream.state.bytesPulled).toBeLessThan(stream.totalBytes);
493+});
334494});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。