





















@@ -107,6 +107,9 @@ 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,
110113}));
111114112115function readTranscriptJsonLines(transcriptPath: string): Array<Record<string, unknown>> {
@@ -428,6 +431,22 @@ vi.mock("../../media/store.js", async () => {
428431};
429432});
430433434+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+431450const { chatHandlers } = await import("./chat.js");
432451433452async function waitForAssertion(assertion: () => void, timeoutMs = 1000, stepMs = 2) {
@@ -842,6 +861,7 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
842861mockState.stagedRelativePaths = null;
843862mockState.unstagedSources = null;
844863mockState.deleteMediaBufferCalls = [];
864+mockState.managedInboundPaths = null;
845865mockState.hasBeforeAgentRunHooks = false;
846866mockState.beforeMessageWriteBlock = false;
847867mockState.beforeMessageWriteContent = null;
@@ -5573,6 +5593,198 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
55735593]);
55745594});
557555955596+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-");
5739+mockState.finalText = "ok";
5740+mockState.sessionEntry = {
5741+modelProvider: "test-provider",
5742+model: "vision-model",
5743+};
5744+mockState.modelCatalog = [
5745+{
5746+provider: "test-provider",
5747+id: "vision-model",
5748+name: "Vision model",
5749+input: ["text", "image"],
5750+},
5751+];
5752+const managedPath = "/home/user/.openclaw/media/inbound/huge.pdf";
5753+mockState.savedMediaResults = [{ path: managedPath, contentType: "application/pdf" }];
5754+mockState.sandboxWorkspace = { workspaceDir: "/sandbox/workspace" };
5755+mockState.managedInboundPaths = [managedPath];
5756+const respond = vi.fn();
5757+const context = createChatContext();
5758+const oversized = Buffer.alloc(6 * 1024 * 1024);
5759+oversized.set(Buffer.from("%PDF-1.4\n"), 0);
5760+5761+await runNonStreamingChatSend({
5762+ context,
5763+ respond,
5764+idempotencyKey: "idem-managed-pdf-oversize",
5765+message: "read this",
5766+requestParams: {
5767+attachments: [
5768+{
5769+type: "file",
5770+mimeType: "application/pdf",
5771+fileName: "huge.pdf",
5772+content: oversized.toString("base64"),
5773+},
5774+],
5775+},
5776+expectBroadcast: false,
5777+});
5778+5779+expect(mockState.deleteMediaBufferCalls).toEqual([]);
5780+expect(mockState.lastDispatchCtx?.MediaPath).toBe(managedPath);
5781+expect(mockState.lastDispatchCtx?.MediaPaths).toEqual([managedPath]);
5782+expect(mockState.lastDispatchCtx?.MediaType).toBe("application/pdf");
5783+expect(mockState.lastDispatchCtx?.MediaTypes).toEqual(["application/pdf"]);
5784+expect(mockState.lastDispatchCtx?.MediaWorkspaceDir).toBeUndefined();
5785+expect(mockState.lastDispatchCtx?.MediaStaged).toBe(true);
5786+});
5787+55765788it("rejects sandbox-oversized non-image attachments as 4xx before staging", async () => {
55775789// Regression: resolveChatAttachmentMaxBytes defaults to 20MB, but
55785790// stageSandboxMedia caps each file at STAGED_MEDIA_MAX_BYTES (5MB) and
@@ -5595,15 +5807,18 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
55955807},
55965808];
55975809mockState.savedMediaResults = [
5598-{ path: "/home/user/.openclaw/media/inbound/huge.pdf", contentType: "application/pdf" },
5810+{
5811+path: "/home/user/.openclaw/media/inbound/huge.bin",
5812+contentType: "application/octet-stream",
5813+},
55995814];
56005815mockState.sandboxWorkspace = { workspaceDir: "/sandbox/workspace" };
56015816const respond = vi.fn();
56025817const context = createChatContext();
56035818// 6MB buffer — above STAGED_MEDIA_MAX_BYTES (5MB) but below the 20MB parse cap.
56045819const oversized = Buffer.alloc(6 * 1024 * 1024);
5605-oversized.set(Buffer.from("%PDF-1.4\n"), 0);
5606-const pdf = oversized.toString("base64");
5820+oversized.set(Buffer.from("OPENCLAW-BINARY\n"), 0);
5821+const oversizedPayload = oversized.toString("base64");
5607582256085823await runNonStreamingChatSend({
56095824 context,
@@ -5612,7 +5827,12 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
56125827message: "read this",
56135828requestParams: {
56145829attachments: [
5615-{ type: "file", mimeType: "application/pdf", fileName: "huge.pdf", content: pdf },
5830+{
5831+type: "file",
5832+mimeType: "application/octet-stream",
5833+fileName: "huge.bin",
5834+content: oversizedPayload,
5835+},
56165836],
56175837},
56185838expectBroadcast: false,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。