



















@@ -107,9 +107,6 @@ const mockState = vi.hoisted(() => ({
107107// stageSandboxMedia silently skips over-cap files.
108108unstagedSources: null as string[] | null,
109109deleteMediaBufferCalls: [] as Array<{ id: string; subdir?: string }>,
110-// Paths the mocked resolveInboundMediaReference treats as managed inbound
111-// media-store entries (drives the #90097 managed-PDF staging-fallback tests).
112-managedInboundPaths: null as string[] | null,
113110}));
114111115112function readTranscriptJsonLines(transcriptPath: string): Array<Record<string, unknown>> {
@@ -431,22 +428,6 @@ vi.mock("../../media/store.js", async () => {
431428};
432429});
433430434-vi.mock("../../media/media-reference.js", async () => {
435-const original = await vi.importActual<typeof import("../../media/media-reference.js")>(
436-"../../media/media-reference.js",
437-);
438-return {
439- ...original,
440-// Treat only test-declared paths as managed inbound media, so the #90097
441-// managed-PDF fallback can be exercised without a real on-disk media store.
442-resolveInboundMediaReference: vi.fn(async (source: string) =>
443-mockState.managedInboundPaths?.includes(source)
444- ? { id: source, normalizedSource: source, physicalPath: source, sourceType: "path" }
445- : null,
446-),
447-};
448-});
449-450431const { chatHandlers } = await import("./chat.js");
451432452433async function waitForAssertion(assertion: () => void, timeoutMs = 1000, stepMs = 2) {
@@ -861,7 +842,6 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
861842mockState.stagedRelativePaths = null;
862843mockState.unstagedSources = null;
863844mockState.deleteMediaBufferCalls = [];
864-mockState.managedInboundPaths = null;
865845mockState.hasBeforeAgentRunHooks = false;
866846mockState.beforeMessageWriteBlock = false;
867847mockState.beforeMessageWriteContent = null;
@@ -5593,149 +5573,11 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
55935573]);
55945574});
559555755596-it("falls back to the managed inbound PDF path when sandbox staging is incomplete (#90097)", async () => {
5597-createTranscriptFixture("openclaw-chat-send-managed-pdf-incomplete-");
5598-mockState.finalText = "ok";
5599-mockState.sessionEntry = { modelProvider: "test-provider", model: "vision-model" };
5600-mockState.modelCatalog = [
5601-{
5602-provider: "test-provider",
5603-id: "vision-model",
5604-name: "Vision model",
5605-input: ["text", "image"],
5606-},
5607-];
5608-const managedPath = "/home/user/.openclaw/media/inbound/report.pdf";
5609-mockState.savedMediaResults = [{ path: managedPath, contentType: "application/pdf" }];
5610-mockState.sandboxWorkspace = { workspaceDir: "/sandbox/workspace" };
5611-// Staging returns an incomplete map (the file silently fell out).
5612-mockState.stagedRelativePaths = ["media/inbound/report.pdf"];
5613-mockState.unstagedSources = [managedPath];
5614-mockState.managedInboundPaths = [managedPath];
5615-const respond = vi.fn();
5616-const context = createChatContext();
5617-const pdf = Buffer.from("%PDF-1.4\n").toString("base64");
5618-5619-await runNonStreamingChatSend({
5620- context,
5621- respond,
5622-idempotencyKey: "idem-managed-pdf-incomplete",
5623-message: "read this",
5624-requestParams: {
5625-attachments: [
5626-{ type: "file", mimeType: "application/pdf", fileName: "report.pdf", content: pdf },
5627-],
5628-},
5629-expectBroadcast: false,
5630-});
5631-5632-// Send proceeds with the absolute managed path; buffers are NOT deleted.
5633-expect(mockState.deleteMediaBufferCalls).toEqual([]);
5634-expect(mockState.lastDispatchCtx?.MediaPaths).toEqual([managedPath]);
5635-expect(mockState.lastDispatchCtx?.MediaPath).toBe(managedPath);
5636-expect(mockState.lastDispatchCtx?.MediaTypes).toEqual(["application/pdf"]);
5637-// Host-side readable: no sandbox workspace dir is carried for the fallback.
5638-expect(mockState.lastDispatchCtx?.MediaWorkspaceDir).toBeUndefined();
5639-// Marker still blocks the dispatch pipeline from re-running staging.
5640-expect(mockState.lastDispatchCtx?.MediaStaged).toBe(true);
5641-});
5642-5643-it("falls back to the managed inbound PDF path when sandbox staging throws (#90097)", async () => {
5644-createTranscriptFixture("openclaw-chat-send-managed-pdf-throw-");
5645-mockState.finalText = "ok";
5646-mockState.sessionEntry = { modelProvider: "test-provider", model: "vision-model" };
5647-mockState.modelCatalog = [
5648-{
5649-provider: "test-provider",
5650-id: "vision-model",
5651-name: "Vision model",
5652-input: ["text", "image"],
5653-},
5654-];
5655-const managedPath = "/home/user/.openclaw/media/inbound/report.pdf";
5656-mockState.savedMediaResults = [{ path: managedPath, contentType: "application/pdf" }];
5657-mockState.sandboxWorkspace = { workspaceDir: "/sandbox/workspace" };
5658-mockState.stageSandboxMediaError = new Error("ENOSPC: no space left on device");
5659-mockState.managedInboundPaths = [managedPath];
5660-const respond = vi.fn();
5661-const context = createChatContext();
5662-const pdf = Buffer.from("%PDF-1.4\n").toString("base64");
5663-5664-await runNonStreamingChatSend({
5665- context,
5666- respond,
5667-idempotencyKey: "idem-managed-pdf-throw",
5668-message: "read this",
5669-requestParams: {
5670-attachments: [
5671-{ type: "file", mimeType: "application/pdf", fileName: "report.pdf", content: pdf },
5672-],
5673-},
5674-expectBroadcast: false,
5675-});
5676-5677-expect(mockState.deleteMediaBufferCalls).toEqual([]);
5678-expect(mockState.lastDispatchCtx?.MediaPaths).toEqual([managedPath]);
5679-expect(mockState.lastDispatchCtx?.MediaWorkspaceDir).toBeUndefined();
5680-expect(mockState.lastDispatchCtx?.MediaStaged).toBe(true);
5681-});
5682-5683-it("does not fall back when not every offloaded ref is a managed PDF (#90097)", async () => {
5684-createTranscriptFixture("openclaw-chat-send-managed-pdf-mixed-");
5685-mockState.finalText = "ok";
5686-mockState.sessionEntry = { modelProvider: "test-provider", model: "vision-model" };
5687-mockState.modelCatalog = [
5688-{
5689-provider: "test-provider",
5690-id: "vision-model",
5691-name: "Vision model",
5692-input: ["text", "image"],
5693-},
5694-];
5695-const managedPdf = "/home/user/.openclaw/media/inbound/report.pdf";
5696-const managedZip = "/home/user/.openclaw/media/inbound/data.zip";
5697-mockState.savedMediaResults = [
5698-{ path: managedPdf, contentType: "application/pdf" },
5699-{ path: managedZip, contentType: "application/zip" },
5700-];
5701-mockState.sandboxWorkspace = { workspaceDir: "/sandbox/workspace" };
5702-mockState.stagedRelativePaths = ["media/inbound/report.pdf", "media/inbound/data.zip"];
5703-mockState.unstagedSources = [managedPdf];
5704-// Both managed, but the non-PDF ref must block the all-or-nothing fallback.
5705-mockState.managedInboundPaths = [managedPdf, managedZip];
5706-const respond = vi.fn();
5707-const context = createChatContext();
5708-const pdf = Buffer.from("%PDF-1.4\n").toString("base64");
5709-const zip = Buffer.from("PKdata").toString("base64");
5710-5711-await runNonStreamingChatSend({
5712- context,
5713- respond,
5714-idempotencyKey: "idem-managed-pdf-mixed",
5715-message: "read these",
5716-requestParams: {
5717-attachments: [
5718-{ type: "file", mimeType: "application/pdf", fileName: "report.pdf", content: pdf },
5719-{ type: "file", mimeType: "application/zip", fileName: "data.zip", content: zip },
5720-],
5721-},
5722-expectBroadcast: false,
5723-waitFor: "none",
5724-});
5725-5726-// Mixed batch keeps the existing delete + 5xx behavior (no silent passthrough).
5727-expect(mockState.lastDispatchCtx).toBeUndefined();
5728-const [ok, , error] = lastRespondCall(respond) ?? [];
5729-expect(ok).toBe(false);
5730-expect(error?.code).toBe(ErrorCodes.UNAVAILABLE);
5731-expect(mockState.deleteMediaBufferCalls.map((c) => c.id).toSorted()).toEqual([
5732-"saved-media",
5733-"saved-media",
5734-]);
5735-});
5736-5737-it("passes sandbox-oversized managed inbound PDFs through instead of rejecting them (#90097)", async () => {
5738-createTranscriptFixture("openclaw-chat-send-managed-pdf-oversize-");
5576+it("passes already-managed oversized inbound PDFs through staging instead of rejecting", async () => {
5577+// #90097: a managed inbound PDF above the sandbox staging cap is read
5578+// host-side (media-understanding) rather than copied into the sandbox, so
5579+// it must reach dispatch with its managed media path instead of a 4xx.
5580+createTranscriptFixture("openclaw-chat-send-managed-pdf-pass-through-");
57395581mockState.finalText = "ok";
57405582mockState.sessionEntry = {
57415583modelProvider: "test-provider",
@@ -5749,19 +5591,20 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
57495591input: ["text", "image"],
57505592},
57515593];
5752-const managedPath = "/home/user/.openclaw/media/inbound/huge.pdf";
5753-mockState.savedMediaResults = [{ path: managedPath, contentType: "application/pdf" }];
5594+mockState.savedMediaResults = [
5595+{ path: "/home/user/.openclaw/media/inbound/huge.pdf", contentType: "application/pdf" },
5596+];
57545597mockState.sandboxWorkspace = { workspaceDir: "/sandbox/workspace" };
5755-mockState.managedInboundPaths = [managedPath];
57565598const respond = vi.fn();
57575599const context = createChatContext();
5600+// 6MB PDF — above STAGED_MEDIA_MAX_BYTES (5MB) but below the 20MB parse cap.
57585601const oversized = Buffer.alloc(6 * 1024 * 1024);
57595602oversized.set(Buffer.from("%PDF-1.4\n"), 0);
5760560357615604await runNonStreamingChatSend({
57625605 context,
57635606 respond,
5764-idempotencyKey: "idem-managed-pdf-oversize",
5607+idempotencyKey: "idem-managed-pdf-pass-through",
57655608message: "read this",
57665609requestParams: {
57675610attachments: [
@@ -5776,13 +5619,19 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
57765619expectBroadcast: false,
57775620});
577856215779-expect(mockState.deleteMediaBufferCalls).toEqual([]);
5780-expect(mockState.lastDispatchCtx?.MediaPath).toBe(managedPath);
5781-expect(mockState.lastDispatchCtx?.MediaPaths).toEqual([managedPath]);
5622+// Reaches dispatch with the managed media path; not staged into the sandbox,
5623+// so no workspace dir, and the media-store entry is kept (not cleaned up).
5624+expect(mockState.lastDispatchCtx?.MediaPath).toBe(
5625+"/home/user/.openclaw/media/inbound/huge.pdf",
5626+);
5627+expect(mockState.lastDispatchCtx?.MediaPaths).toEqual([
5628+"/home/user/.openclaw/media/inbound/huge.pdf",
5629+]);
57825630expect(mockState.lastDispatchCtx?.MediaType).toBe("application/pdf");
57835631expect(mockState.lastDispatchCtx?.MediaTypes).toEqual(["application/pdf"]);
57845632expect(mockState.lastDispatchCtx?.MediaWorkspaceDir).toBeUndefined();
57855633expect(mockState.lastDispatchCtx?.MediaStaged).toBe(true);
5634+expect(mockState.deleteMediaBufferCalls).toEqual([]);
57865635});
5787563657885637it("rejects sandbox-oversized non-image attachments as 4xx before staging", async () => {
@@ -5791,7 +5640,8 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
57915640// silently drops oversize files. Without a pre-check, a sandbox session
57925641// accepting a 5-20MB non-image would fail staging and surface as a
57935642// retryable 5xx UNAVAILABLE, misleading clients into retrying a
5794-// deterministically broken request.
5643+// deterministically broken request. Managed PDFs pass through (see above);
5644+// other oversized non-image files must still be rejected.
57955645createTranscriptFixture("openclaw-chat-send-sandbox-oversize-");
57965646mockState.finalText = "ok";
57975647mockState.sessionEntry = {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。