





















@@ -68,6 +68,30 @@ function createCacheFetchMock(params: { name: string; expireTime: string }) {
6868);
6969}
707071+// Builds a 200-OK response whose body streams more than 1 MiB with no
72+// Content-Length, mirroring a buggy/hostile Google cachedContents endpoint.
73+// The shared byte-cap reader must cancel the body before fully buffering it.
74+function createOversizedJsonResponse(): { response: Response; cancel: ReturnType<typeof vi.fn> } {
75+const cancel = vi.fn(async () => undefined);
76+let pullCount = 0;
77+const response = new Response(
78+new ReadableStream<Uint8Array>({
79+pull(controller) {
80+pullCount += 1;
81+// First chunk already exceeds the 1 MiB cap so the reader truncates
82+// and cancels instead of waiting for the (never-ending) rest.
83+controller.enqueue(new Uint8Array(pullCount === 1 ? 1024 * 1024 + 1 : 1));
84+},
85+ cancel,
86+}),
87+{
88+status: 200,
89+headers: { "content-type": "application/json" },
90+},
91+);
92+return { response, cancel };
93+}
94+7195function createCapturingStreamFn(result = "stream") {
7296// The wrapper mutates payloads through onPayload before calling the real
7397// stream; capture that final payload instead of mocking Google responses.
@@ -552,4 +576,91 @@ describe("google prompt cache", () => {
552576expect(wrapped).toBeUndefined();
553577expect(fetchMock).not.toHaveBeenCalled();
554578});
579+580+it("bounds an oversized cache-creation response body instead of buffering it", async () => {
581+const now = 4_000_000;
582+const { response, cancel } = createOversizedJsonResponse();
583+const fetchMock = vi.fn(async () => response);
584+const sessionManager = makeSessionManager();
585+const innerStreamFn = vi.fn(() => "stream" as never);
586+587+const wrapped = await preparePromptCacheStream({
588+ fetchMock,
589+ now,
590+ sessionManager,
591+streamFn: innerStreamFn,
592+});
593+594+await expect(
595+Promise.resolve(
596+wrapped?.(
597+makeGoogleModel(),
598+{ systemPrompt: "Follow policy.", messages: [] } as never,
599+{} as never,
600+),
601+),
602+).rejects.toThrow(/Google prompt cache response too large: \d+ bytes/);
603+604+expect(fetchMock).toHaveBeenCalledTimes(1);
605+expect(callArg(fetchMock, 0, 0)).toBe(
606+"https://generativelanguage.googleapis.com/v1beta/cachedContents",
607+);
608+expect(cancel).toHaveBeenCalledOnce();
609+});
610+611+it("bounds an oversized cache-refresh response body instead of buffering it", async () => {
612+const now = 4_500_000;
613+const expireSoon = new Date(now + 60_000).toISOString();
614+const systemPromptDigest = crypto.createHash("sha256").update("Follow policy.").digest("hex");
615+const sessionManager = makeSessionManager([
616+{
617+id: "entry-1",
618+parentId: null,
619+timestamp: new Date(now - 5_000).toISOString(),
620+type: "custom",
621+customType: "openclaw.google-prompt-cache",
622+data: {
623+status: "ready",
624+timestamp: now - 5_000,
625+provider: "google",
626+modelId: "gemini-3.1-pro-preview",
627+modelApi: "google-generative-ai",
628+baseUrl: "https://generativelanguage.googleapis.com/v1beta",
629+ systemPromptDigest,
630+cacheRetention: "long",
631+cachedContent: "cachedContents/system-cache-overflow",
632+expireTime: expireSoon,
633+},
634+},
635+]);
636+const { response, cancel } = createOversizedJsonResponse();
637+const fetchMock = vi.fn(async () => response);
638+const { streamFn: innerStreamFn, getCapturedPayload } = createCapturingStreamFn();
639+640+const wrapped = await preparePromptCacheStream({
641+ fetchMock,
642+ now,
643+ sessionManager,
644+streamFn: innerStreamFn,
645+});
646+647+// The TTL-refresh read swallows errors (.catch(() => null)) and falls back
648+// to the still-valid cached content, so the oversized body must be cancelled
649+// by the byte cap rather than fully buffered.
650+await Promise.resolve(
651+wrapped?.(
652+makeGoogleModel(),
653+{ systemPrompt: "Follow policy.", messages: [] } as never,
654+{} as never,
655+),
656+);
657+658+expect(fetchMock).toHaveBeenCalledTimes(1);
659+expect(fetchUrl(fetchMock)).toBe(
660+"https://generativelanguage.googleapis.com/v1beta/cachedContents/system-cache-overflow?updateMask=ttl",
661+);
662+expect(fetchInit(fetchMock).method).toBe("PATCH");
663+expect(cancel).toHaveBeenCalledOnce();
664+expect(getCapturedPayload()?.cachedContent).toBe("cachedContents/system-cache-overflow");
665+});
555666});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。