




















@@ -10,6 +10,7 @@ import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plug
10101111let LocalMediaAccessError: typeof import("./web-media.js").LocalMediaAccessError;
1212let loadWebMedia: typeof import("./web-media.js").loadWebMedia;
13+let loadWebMediaRaw: typeof import("./web-media.js").loadWebMediaRaw;
1314let optimizeImageToJpeg: typeof import("./web-media.js").optimizeImageToJpeg;
14151516const TINY_PNG_BASE64 =
@@ -39,7 +40,8 @@ function installCanvasMediaResolver() {
3940}
40414142beforeAll(async () => {
42-({ LocalMediaAccessError, loadWebMedia, optimizeImageToJpeg } = await import("./web-media.js"));
43+({ LocalMediaAccessError, loadWebMedia, loadWebMediaRaw, optimizeImageToJpeg } =
44+await import("./web-media.js"));
4345fixtureRoot = await fs.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "web-media-core-"));
4446tinyPngFile = path.join(fixtureRoot, "tiny.png");
4547await fs.writeFile(tinyPngFile, Buffer.from(TINY_PNG_BASE64, "base64"));
@@ -75,6 +77,47 @@ afterAll(async () => {
7577});
76787779describe("loadWebMedia", () => {
80+function makeStallingFetch(firstChunk: Uint8Array) {
81+return vi.fn(
82+async () =>
83+new Response(
84+new ReadableStream<Uint8Array>({
85+start(controller) {
86+controller.enqueue(firstChunk);
87+},
88+}),
89+{
90+status: 200,
91+headers: { "content-type": "application/pdf" },
92+},
93+),
94+);
95+}
96+97+async function expectWebMediaIdleTimeout(
98+createLoadPromise: () => Promise<unknown>,
99+idleTimeoutMs: number,
100+) {
101+vi.useFakeTimers();
102+try {
103+const outcome = createLoadPromise().then(
104+() => ({ status: "resolved" as const }),
105+(error: unknown) => ({ status: "rejected" as const, error }),
106+);
107+await vi.advanceTimersByTimeAsync(idleTimeoutMs + 5);
108+await expect(
109+Promise.race([outcome, Promise.resolve({ status: "pending" as const })]),
110+).resolves.toMatchObject({ status: "rejected" });
111+const result = await outcome;
112+expect(result.status).toBe("rejected");
113+if (result.status === "rejected") {
114+expect(String(result.error)).toMatch(/stalled|no data received/i);
115+}
116+} finally {
117+vi.useRealTimers();
118+}
119+}
120+78121function createLocalWebMediaOptions() {
79122return {
80123maxBytes: 1024 * 1024,
@@ -689,6 +732,43 @@ describe("loadWebMedia", () => {
689732}
690733});
691734735+it("applies the shared remote read idle timeout for raw web media loads", async () => {
736+const readIdleTimeoutMs = 20;
737+const fetchImpl = makeStallingFetch(new Uint8Array([0x25, 0x50, 0x44, 0x46]));
738+739+await expectWebMediaIdleTimeout(
740+() =>
741+loadWebMediaRaw("https://example.test/stalled.pdf", {
742+maxBytes: 1024 * 1024,
743+ fetchImpl,
744+ readIdleTimeoutMs,
745+ssrfPolicy: { allowedHostnames: ["example.test"] },
746+}),
747+readIdleTimeoutMs,
748+);
749+});
750+751+it("loads a valid remote PDF when the raw web media read stays active", async () => {
752+const fetchImpl = vi.fn(
753+async () =>
754+new Response(Buffer.from("%PDF-1.4\n%%EOF"), {
755+status: 200,
756+headers: { "content-type": "application/pdf" },
757+}),
758+);
759+760+const result = await loadWebMediaRaw("https://example.test/ok.pdf", {
761+maxBytes: 1024 * 1024,
762+ fetchImpl,
763+readIdleTimeoutMs: 20,
764+ssrfPolicy: { allowedHostnames: ["example.test"] },
765+});
766+767+expect(result.kind).toBe("document");
768+expect(result.contentType).toBe("application/pdf");
769+expect(result.buffer.toString()).toContain("%PDF-1.4");
770+});
771+692772it("rejects unsupported media store URI locations", async () => {
693773await expectLoadWebMediaErrorCode(
694774loadWebMedia("media://outbound/tiny.png"),
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。