

























@@ -21,6 +21,58 @@ vi.mock("../../media/read-capability.js", () => ({
21212222import { createReplyMediaPathNormalizer } from "./reply-media-paths.js";
232324+type NormalizedReply = {
25+mediaUrl?: string;
26+mediaUrls?: string[];
27+text?: string;
28+};
29+30+function isRecord(value: unknown): value is Record<string, unknown> {
31+return typeof value === "object" && value !== null;
32+}
33+34+function requireRecord(value: unknown, label: string): Record<string, unknown> {
35+expect(isRecord(value)).toBe(true);
36+if (!isRecord(value)) {
37+throw new Error(`${label} was not an object`);
38+}
39+return value;
40+}
41+42+function expectMedia(result: NormalizedReply, mediaUrl: string, mediaUrls: string[]): void {
43+expect(result.mediaUrl).toBe(mediaUrl);
44+expect(result.mediaUrls).toEqual(mediaUrls);
45+}
46+47+function expectNoMedia(result: NormalizedReply): void {
48+expect(result.mediaUrl).toBeUndefined();
49+expect(result.mediaUrls).toBeUndefined();
50+}
51+52+function expectOutboundAttachmentCall(
53+index: number,
54+mediaUrl: string,
55+mediaMaxBytes: number,
56+): Record<string, unknown> {
57+const call = resolveOutboundAttachmentFromUrl.mock.calls[index] as unknown[] | undefined;
58+expect(call).toBeDefined();
59+if (!call) {
60+throw new Error(`missing outbound attachment call ${index + 1}`);
61+}
62+expect(call[0]).toBe(mediaUrl);
63+expect(call[1]).toBe(mediaMaxBytes);
64+return requireRecord(call[2], "outbound attachment options");
65+}
66+67+function expectAgentScopedMediaAccessCall(): Record<string, unknown> {
68+const call = resolveAgentScopedOutboundMediaAccess.mock.calls[0] as unknown[] | undefined;
69+expect(call).toBeDefined();
70+if (!call) {
71+throw new Error("missing agent scoped media access call");
72+}
73+return requireRecord(call[0], "agent scoped media access request");
74+}
75+2476describe("createReplyMediaPathNormalizer", () => {
2577beforeEach(() => {
2678ensureSandboxWorkspaceForSession.mockReset().mockResolvedValue(null);
@@ -48,19 +100,14 @@ describe("createReplyMediaPathNormalizer", () => {
48100mediaUrls: ["./out/photo.png"],
49101});
5010251-expect(result).toMatchObject({
52-mediaUrl: "/tmp/outbound-media/photo.png",
53-mediaUrls: ["/tmp/outbound-media/photo.png"],
54-});
55-expect(resolveOutboundAttachmentFromUrl).toHaveBeenCalledWith(
103+expectMedia(result, "/tmp/outbound-media/photo.png", ["/tmp/outbound-media/photo.png"]);
104+const options = expectOutboundAttachmentCall(
105+0,
56106path.join("/tmp/agent-workspace", "out", "photo.png"),
571075 * 1024 * 1024,
58-expect.objectContaining({
59-mediaAccess: expect.objectContaining({
60-workspaceDir: "/tmp/agent-workspace",
61-}),
62-}),
63108);
109+const mediaAccess = requireRecord(options.mediaAccess, "media access");
110+expect(mediaAccess.workspaceDir).toBe("/tmp/agent-workspace");
64111});
6511266113it("maps sandbox-relative media back to the host sandbox workspace before staging", async () => {
@@ -78,21 +125,19 @@ describe("createReplyMediaPathNormalizer", () => {
78125mediaUrls: ["./out/photo.png", "file:///workspace/screens/final.png"],
79126});
8012781-expect(result).toMatchObject({
82-mediaUrl: "/tmp/outbound-media/photo.png",
83-mediaUrls: ["/tmp/outbound-media/photo.png", "/tmp/outbound-media/final.png"],
84-});
85-expect(resolveOutboundAttachmentFromUrl).toHaveBeenNthCalledWith(
86-1,
128+expectMedia(result, "/tmp/outbound-media/photo.png", [
129+"/tmp/outbound-media/photo.png",
130+"/tmp/outbound-media/final.png",
131+]);
132+expectOutboundAttachmentCall(
133+0,
87134path.join("/tmp/sandboxes/session-1", "out", "photo.png"),
881355 * 1024 * 1024,
89-expect.any(Object),
90136);
91-expect(resolveOutboundAttachmentFromUrl).toHaveBeenNthCalledWith(
92-2,
137+expectOutboundAttachmentCall(
138+1,
93139path.join("/tmp/sandboxes/session-1", "screens", "final.png"),
941405 * 1024 * 1024,
95-expect.any(Object),
96141);
97142});
98143@@ -112,15 +157,12 @@ describe("createReplyMediaPathNormalizer", () => {
112157mediaUrls: ["./out/photo.png"],
113158});
114159115-expect(result).toMatchObject({
116-mediaUrl: undefined,
117-mediaUrls: undefined,
118-});
160+expectNoMedia(result);
119161expect(resolveOutboundAttachmentFromUrl).toHaveBeenCalledTimes(1);
120-expect(resolveOutboundAttachmentFromUrl).toHaveBeenCalledWith(
162+expectOutboundAttachmentCall(
163+0,
121164path.join("/tmp/sandboxes/session-1", "out", "photo.png"),
1221655 * 1024 * 1024,
123-expect.any(Object),
124166);
125167expect(result.text).toBe("⚠️ Media failed.");
126168});
@@ -136,10 +178,7 @@ describe("createReplyMediaPathNormalizer", () => {
136178mediaUrls: ["file:///Users/peter/Documents/report.pdf"],
137179});
138180139-expect(result).toMatchObject({
140-mediaUrl: undefined,
141-mediaUrls: undefined,
142-});
181+expectNoMedia(result);
143182expect(resolveOutboundAttachmentFromUrl).not.toHaveBeenCalled();
144183});
145184@@ -158,10 +197,7 @@ describe("createReplyMediaPathNormalizer", () => {
158197mediaUrls: ["file:///Users/peter/Documents/report.pdf"],
159198});
160199161-expect(result).toMatchObject({
162-mediaUrl: undefined,
163-mediaUrls: undefined,
164-});
200+expectNoMedia(result);
165201expect(resolveOutboundAttachmentFromUrl).not.toHaveBeenCalled();
166202});
167203@@ -180,10 +216,7 @@ describe("createReplyMediaPathNormalizer", () => {
180216mediaUrls: ["/Users/peter/Documents/report.pdf"],
181217});
182218183-expect(result).toMatchObject({
184-mediaUrl: undefined,
185-mediaUrls: undefined,
186-});
219+expectNoMedia(result);
187220expect(resolveOutboundAttachmentFromUrl).not.toHaveBeenCalled();
188221});
189222@@ -199,15 +232,8 @@ describe("createReplyMediaPathNormalizer", () => {
199232mediaUrls: [absolutePath],
200233});
201234202-expect(result).toMatchObject({
203-mediaUrl: "/tmp/outbound-media/chart.png",
204-mediaUrls: ["/tmp/outbound-media/chart.png"],
205-});
206-expect(resolveOutboundAttachmentFromUrl).toHaveBeenCalledWith(
207-absolutePath,
208-8 * 1024 * 1024,
209-expect.any(Object),
210-);
235+expectMedia(result, "/tmp/outbound-media/chart.png", ["/tmp/outbound-media/chart.png"]);
236+expectOutboundAttachmentCall(0, absolutePath, 8 * 1024 * 1024);
211237});
212238213239it("prefers channel account media limits when staging reply attachments", async () => {
@@ -236,11 +262,7 @@ describe("createReplyMediaPathNormalizer", () => {
236262mediaUrls: [absolutePath],
237263});
238264239-expect(resolveOutboundAttachmentFromUrl).toHaveBeenCalledWith(
240-absolutePath,
241-64 * 1024 * 1024,
242-expect.any(Object),
243-);
265+expectOutboundAttachmentCall(0, absolutePath, 64 * 1024 * 1024);
244266});
245267246268it("drops workspace-relative media paths that escape the agent workspace", async () => {
@@ -254,10 +276,7 @@ describe("createReplyMediaPathNormalizer", () => {
254276mediaUrls: ["../../etc/passwd"],
255277});
256278257-expect(result).toMatchObject({
258-mediaUrl: undefined,
259-mediaUrls: undefined,
260-});
279+expectNoMedia(result);
261280expect(resolveOutboundAttachmentFromUrl).not.toHaveBeenCalled();
262281});
263282@@ -276,10 +295,7 @@ describe("createReplyMediaPathNormalizer", () => {
276295mediaUrls: ["../../etc/passwd"],
277296});
278297279-expect(result).toMatchObject({
280-mediaUrl: undefined,
281-mediaUrls: undefined,
282-});
298+expectNoMedia(result);
283299expect(resolveOutboundAttachmentFromUrl).not.toHaveBeenCalled();
284300});
285301@@ -295,10 +311,9 @@ describe("createReplyMediaPathNormalizer", () => {
295311mediaUrls: ["/Users/peter/.openclaw/media/tool-image-generation/generated.png"],
296312});
297313298-expect(result).toMatchObject({
299-mediaUrl: "/Users/peter/.openclaw/media/tool-image-generation/generated.png",
300-mediaUrls: ["/Users/peter/.openclaw/media/tool-image-generation/generated.png"],
301-});
314+expectMedia(result, "/Users/peter/.openclaw/media/tool-image-generation/generated.png", [
315+"/Users/peter/.openclaw/media/tool-image-generation/generated.png",
316+]);
302317expect(resolveOutboundAttachmentFromUrl).not.toHaveBeenCalled();
303318});
304319@@ -318,10 +333,9 @@ describe("createReplyMediaPathNormalizer", () => {
318333mediaUrls: ["/Users/peter/.openclaw/media/outbound/generated.png"],
319334});
320335321-expect(result).toMatchObject({
322-mediaUrl: "/Users/peter/.openclaw/media/outbound/generated.png",
323-mediaUrls: ["/Users/peter/.openclaw/media/outbound/generated.png"],
324-});
336+expectMedia(result, "/Users/peter/.openclaw/media/outbound/generated.png", [
337+"/Users/peter/.openclaw/media/outbound/generated.png",
338+]);
325339expect(resolveOutboundAttachmentFromUrl).not.toHaveBeenCalled();
326340});
327341@@ -348,10 +362,7 @@ describe("createReplyMediaPathNormalizer", () => {
348362mediaUrls: [symlinkPath],
349363});
350364351-expect(result).toMatchObject({
352-mediaUrl: undefined,
353-mediaUrls: undefined,
354-});
365+expectNoMedia(result);
355366expect(resolveOutboundAttachmentFromUrl).not.toHaveBeenCalled();
356367} finally {
357368await fs.rm(symlinkPath, { force: true });
@@ -374,10 +385,7 @@ describe("createReplyMediaPathNormalizer", () => {
374385mediaUrls: ["/Users/peter/secrets/photo.png"],
375386});
376387377-expect(result).toMatchObject({
378-mediaUrl: undefined,
379-mediaUrls: undefined,
380-});
388+expectNoMedia(result);
381389});
382390383391it("keeps reply text and appends a warning when all reply media is dropped", async () => {
@@ -393,11 +401,8 @@ describe("createReplyMediaPathNormalizer", () => {
393401mediaUrls: ["./out/missing.png"],
394402});
395403396-expect(result).toMatchObject({
397-text: "WA_MEDIA_DM_07\n⚠️ Media failed.",
398-mediaUrl: undefined,
399-mediaUrls: undefined,
400-});
404+expect(result.text).toBe("WA_MEDIA_DM_07\n⚠️ Media failed.");
405+expectNoMedia(result);
401406});
402407403408it("returns a warning-only text reply when media-only output is dropped upstream", async () => {
@@ -412,11 +417,8 @@ describe("createReplyMediaPathNormalizer", () => {
412417mediaUrls: ["./out/missing.png"],
413418});
414419415-expect(result).toMatchObject({
416-text: "⚠️ Media failed.",
417-mediaUrl: undefined,
418-mediaUrls: undefined,
419-});
420+expect(result.text).toBe("⚠️ Media failed.");
421+expectNoMedia(result);
420422});
421423422424it("threads requester context into shared outbound media access", async () => {
@@ -439,7 +441,8 @@ describe("createReplyMediaPathNormalizer", () => {
439441mediaUrls: ["./out/photo.png"],
440442});
441443442-expect(resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledWith({
444+expect(resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledTimes(1);
445+expect(expectAgentScopedMediaAccessCall()).toEqual({
443446cfg: {},
444447agentId: undefined,
445448workspaceDir: "/tmp/agent-workspace",
@@ -469,9 +472,12 @@ describe("createReplyMediaPathNormalizer", () => {
469472mediaUrls: [absolutePath],
470473});
471474472-expect(resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledWith({
475+expect(resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledTimes(1);
476+const accessRequest = expectAgentScopedMediaAccessCall();
477+expect(typeof accessRequest.agentId).toBe("string");
478+expect({ ...accessRequest, agentId: undefined }).toEqual({
473479cfg: { tools: { fs: { workspaceOnly: false } } },
474-agentId: expect.any(String),
480+agentId: undefined,
475481workspaceDir: "/tmp/agent-workspace",
476482mediaSources: [absolutePath],
477483sessionKey: "session-key",
@@ -499,13 +505,13 @@ describe("createReplyMediaPathNormalizer", () => {
499505mediaUrls: [homeRelativePath],
500506});
501507502-expect(result).toMatchObject({
503- mediaUrl: "/tmp/outbound-media/chart.png",
504- mediaUrls: ["/tmp/outbound-media/chart.png"],
505-});
506-expect(resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledWith({
508+expectMedia(result, "/tmp/outbound-media/chart.png", ["/tmp/outbound-media/chart.png"]);
509+expect(resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledTimes(1);
510+const accessRequest = expectAgentScopedMediaAccessCall();
511+expect(typeof accessRequest.agentId).toBe("string");
512+expect({ ...accessRequest, agentId: undefined }).toEqual({
507513cfg: { tools: { fs: { workspaceOnly: false } } },
508-agentId: expect.any(String),
514+agentId: undefined,
509515workspaceDir: "/tmp/agent-workspace",
510516mediaSources: [homeRelativePath],
511517sessionKey: "session-key",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。