@@ -94,6 +94,39 @@ function fetchInputUrl(fetchMock: ReturnType<typeof vi.fn>, index: number): stri
|
94 | 94 | return input.url; |
95 | 95 | } |
96 | 96 | |
| 97 | +function oversizedJsonResponse(params: { chunkCount: number; chunkSize: number }): { |
| 98 | +response: Response; |
| 99 | +getReadCount: () => number; |
| 100 | +wasCanceled: () => boolean; |
| 101 | +} { |
| 102 | +const chunk = new Uint8Array(params.chunkSize); |
| 103 | +let readCount = 0; |
| 104 | +let canceled = false; |
| 105 | +return { |
| 106 | +response: new Response( |
| 107 | +new ReadableStream<Uint8Array>({ |
| 108 | +pull(controller) { |
| 109 | +if (readCount >= params.chunkCount) { |
| 110 | +controller.close(); |
| 111 | +return; |
| 112 | +} |
| 113 | +readCount += 1; |
| 114 | +controller.enqueue(chunk); |
| 115 | +}, |
| 116 | +cancel() { |
| 117 | +canceled = true; |
| 118 | +}, |
| 119 | +}), |
| 120 | +{ |
| 121 | +status: 200, |
| 122 | +headers: { "Content-Type": "application/json" }, |
| 123 | +}, |
| 124 | +), |
| 125 | +getReadCount: () => readCount, |
| 126 | +wasCanceled: () => canceled, |
| 127 | +}; |
| 128 | +} |
| 129 | + |
97 | 130 | let ssrfMock: { mockRestore: () => void } | undefined; |
98 | 131 | |
99 | 132 | describe("google video generation provider", () => { |
@@ -486,6 +519,33 @@ describe("google video generation provider", () => {
|
486 | 519 | expect(result.videos[0]?.buffer).toEqual(Buffer.from("rest-video")); |
487 | 520 | }); |
488 | 521 | |
| 522 | +it("bounds successful Google REST operation JSON bodies instead of buffering the whole response", async () => { |
| 523 | +vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({ |
| 524 | +apiKey: "google-key", |
| 525 | +source: "env", |
| 526 | +mode: "api-key", |
| 527 | +}); |
| 528 | +generateVideosMock.mockRejectedValue(Object.assign(new Error("sdk 404"), { status: 404 })); |
| 529 | +const streamed = oversizedJsonResponse({ chunkCount: 64, chunkSize: 1024 * 1024 }); |
| 530 | +const fetchMock = vi.fn(async () => streamed.response); |
| 531 | +vi.stubGlobal("fetch", fetchMock); |
| 532 | + |
| 533 | +const provider = buildGoogleVideoGenerationProvider(); |
| 534 | +await expect( |
| 535 | +provider.generateVideo({ |
| 536 | +provider: "google", |
| 537 | +model: "veo-3.1-fast-generate-preview", |
| 538 | +prompt: "A tiny robot watering a windowsill garden", |
| 539 | +cfg: {}, |
| 540 | +durationSeconds: 3, |
| 541 | +}), |
| 542 | +).rejects.toThrow("Google video operation response exceeds 16777216 bytes"); |
| 543 | + |
| 544 | +expect(fetchMock).toHaveBeenCalledTimes(1); |
| 545 | +expect(streamed.getReadCount()).toBeLessThan(64); |
| 546 | +expect(streamed.wasCanceled()).toBe(true); |
| 547 | +}); |
| 548 | + |
489 | 549 | it("retries transient Google REST poll failures with empty bodies", async () => { |
490 | 550 | vi.useFakeTimers(); |
491 | 551 | vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({ |
|