























@@ -59,6 +59,40 @@ function cancelTrackedResponse(
5959};
6060}
616162+function streamedJsonResponse(params: { chunkCount: number; chunkSize: number }): {
63+response: Response;
64+getReadCount: () => number;
65+wasCanceled: () => boolean;
66+} {
67+// Multi-chunk fixture: proves the bounded read stops pulling chunks before
68+// the whole (here syntactically broken / unbounded) body is buffered, and
69+// that the stream is cancelled on overflow.
70+let reads = 0;
71+let canceled = false;
72+const encoder = new TextEncoder();
73+const stream = new ReadableStream<Uint8Array>({
74+pull(controller) {
75+if (reads >= params.chunkCount) {
76+controller.close();
77+return;
78+}
79+reads += 1;
80+controller.enqueue(encoder.encode("a".repeat(params.chunkSize)));
81+},
82+cancel() {
83+canceled = true;
84+},
85+});
86+return {
87+response: new Response(stream, {
88+status: 200,
89+headers: { "Content-Type": "application/json" },
90+}),
91+getReadCount: () => reads,
92+wasCanceled: () => canceled,
93+};
94+}
95+6296import { testing } from "../test-api.js";
6397import { createParallelWebSearchProvider as createContractParallelWebSearchProvider } from "../web-search-contract-api.js";
6498import { createParallelWebSearchProvider } from "./parallel-web-search-provider.js";
@@ -583,6 +617,65 @@ describe("parallel web search provider", () => {
583617expect(textSpy).not.toHaveBeenCalled();
584618});
585619620+it("bounds successful Parallel JSON bodies instead of buffering the whole response", async () => {
621+// 200-chunk x 1 MiB body (~200 MiB) caps at 16 MiB: the bounded reader must
622+// stop pulling chunks and cancel the stream well before draining it, then
623+// surface a bounded error rather than buffering the whole payload.
624+const streamed = streamedJsonResponse({ chunkCount: 200, chunkSize: 1024 * 1024 });
625+endpointMockState.responses.push(streamed.response);
626+const provider = createParallelWebSearchProvider();
627+const tool = provider.createTool({
628+config: {},
629+searchConfig: { parallel: { apiKey: "par-secret" } },
630+});
631+if (!tool) {
632+throw new Error("Expected tool definition");
633+}
634+635+const error = await tool
636+.execute({
637+objective: `parallel-success-body-${Date.now()}-${Math.random()}`,
638+search_queries: ["openclaw"],
639+})
640+.catch((cause: unknown) => cause);
641+642+expect(error).toBeInstanceOf(Error);
643+expect((error as Error).message).toMatch(
644+new RegExp(
645+`Parallel API: JSON response exceeds ${testing.PARALLEL_SEARCH_RESPONSE_LIMIT_BYTES} bytes`,
646+),
647+);
648+// Stopped well before draining all 200 chunks, and cancelled the stream.
649+expect(streamed.getReadCount()).toBeLessThan(200);
650+expect(streamed.wasCanceled()).toBe(true);
651+});
652+653+it("parses a well-formed Parallel JSON body under the byte cap", async () => {
654+endpointMockState.responses.push(
655+new Response(
656+JSON.stringify({
657+search_id: "ok",
658+session_id: "ok-session",
659+results: [{ url: "https://example.com/a", title: "A", excerpts: ["alpha"] }],
660+}),
661+{ status: 200, headers: { "Content-Type": "application/json" } },
662+),
663+);
664+const provider = createParallelWebSearchProvider();
665+const tool = provider.createTool({
666+config: {},
667+searchConfig: { parallel: { apiKey: "par-secret" } },
668+});
669+if (!tool) {
670+throw new Error("Expected tool definition");
671+}
672+const result = (await tool.execute({
673+objective: `parallel-success-ok-${Date.now()}-${Math.random()}`,
674+search_queries: ["openclaw"],
675+})) as { provider?: string; searchId?: string; count?: number };
676+expect(result).toMatchObject({ provider: "parallel", searchId: "ok", count: 1 });
677+});
678+586679it("does not surface a Parallel-generated sessionId on a cache hit", async () => {
587680// Unique objective so this test does not collide with the SDK's
588681// module-level web-search cache across other cases.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。