


























@@ -17,7 +17,7 @@ const enqueueFollowupRunMock = vi.fn();
1717const scheduleFollowupDrainMock = vi.fn();
1818const refreshQueuedFollowupSessionMock = vi.fn();
1919const resolveOutboundAttachmentFromUrlMock = vi.fn();
20-const createReplyMediaPathNormalizerRuntimeMock = vi.fn();
20+const createReplyMediaContextRuntimeMock = vi.fn();
21212222vi.mock("../../agents/model-fallback.js", () => ({
2323runWithModelFallback: (params: {
@@ -54,14 +54,15 @@ vi.mock("../../media/outbound-attachment.js", () => ({
5454}));
55555656// Spy on the .runtime import path used by agent-runner-execution.ts so we can assert
57-// that the fix prevents a second normalizer from being created inside runAgentTurnWithFallback.
57+// that the fix prevents a second media context from being created inside runAgentTurnWithFallback.
5858vi.mock("./reply-media-paths.runtime.js", async (importOriginal) => {
5959const mod = await importOriginal<typeof import("./reply-media-paths.runtime.js")>();
6060return {
61-createReplyMediaPathNormalizer: (...args: Parameters<typeof mod.createReplyMediaPathNormalizer>) => {
62-createReplyMediaPathNormalizerRuntimeMock(...args);
63-return mod.createReplyMediaPathNormalizer(...args);
61+createReplyMediaContext: (...args: Parameters<typeof mod.createReplyMediaContext>) => {
62+createReplyMediaContextRuntimeMock(...args);
63+return mod.createReplyMediaContext(...args);
6464},
65+createReplyMediaPathNormalizer: mod.createReplyMediaPathNormalizer,
6566};
6667});
6768@@ -86,7 +87,7 @@ describe("runReplyAgent media path normalization", () => {
8687scheduleFollowupDrainMock.mockReset();
8788refreshQueuedFollowupSessionMock.mockReset();
8889resolveOutboundAttachmentFromUrlMock.mockReset();
89-createReplyMediaPathNormalizerRuntimeMock.mockReset();
90+createReplyMediaContextRuntimeMock.mockReset();
9091vi.stubEnv("OPENCLAW_TEST_FAST", "1");
9192resolveOutboundAttachmentFromUrlMock.mockImplementation(async (mediaUrl: string) => ({
9293path: path.join("/tmp/outbound-media", path.basename(mediaUrl)),
@@ -175,15 +176,94 @@ describe("runReplyAgent media path normalization", () => {
175176);
176177});
177178178-it("does not create a second normalizer inside runAgentTurnWithFallback when onBlockReply is provided", async () => {
179+it("shares one media cache between direct block media and final payload filtering", async () => {
180+let stagedIndex = 0;
181+resolveOutboundAttachmentFromUrlMock.mockImplementation(async (mediaUrl: string) => {
182+stagedIndex += 1;
183+return {
184+path: path.join("/tmp/outbound-media", `${stagedIndex}-${path.basename(mediaUrl)}`),
185+};
186+});
187+const onBlockReply = vi.fn();
188+runEmbeddedPiAgentMock.mockImplementation(
189+async (params: {
190+onBlockReply?: (payload: { text?: string; mediaUrls?: string[] }) => Promise<void>;
191+}) => {
192+await params.onBlockReply?.({
193+text: "here is the chart\nMEDIA:./out/chart.png",
194+});
195+return {
196+payloads: [{ text: "here is the chart\nMEDIA:./out/chart.png" }],
197+meta: {
198+agentMeta: {
199+sessionId: "session",
200+provider: "anthropic",
201+model: "claude",
202+},
203+},
204+};
205+},
206+);
207+208+const result = await runReplyAgent({
209+commandBody: "generate chart",
210+followupRun: createMockFollowupRun({
211+prompt: "generate chart",
212+run: {
213+agentId: "main",
214+agentDir: "/tmp/agent",
215+messageProvider: "whatsapp",
216+workspaceDir: "/tmp/workspace",
217+},
218+}) as unknown as FollowupRun,
219+queueKey: "main",
220+resolvedQueue: { mode: "interrupt" } as QueueSettings,
221+shouldSteer: false,
222+shouldFollowup: false,
223+isActive: false,
224+isStreaming: false,
225+typing: createMockTypingController(),
226+sessionCtx: {
227+Provider: "whatsapp",
228+Surface: "whatsapp",
229+To: "chat-1",
230+OriginatingTo: "chat-1",
231+AccountId: "default",
232+MessageSid: "msg-1",
233+} as unknown as TemplateContext,
234+defaultModel: "anthropic/claude",
235+resolvedVerboseLevel: "off",
236+isNewSession: false,
237+blockStreamingEnabled: false,
238+resolvedBlockStreamingBreak: "message_end",
239+shouldInjectGroupIntro: false,
240+typingMode: "instant",
241+opts: {
242+ onBlockReply,
243+},
244+});
245+246+expect(result).toBeUndefined();
247+expect(resolveOutboundAttachmentFromUrlMock).toHaveBeenCalledTimes(1);
248+expect(onBlockReply).toHaveBeenCalledTimes(1);
249+expect(onBlockReply).toHaveBeenCalledWith({
250+text: undefined,
251+mediaUrl: "/tmp/outbound-media/1-chart.png",
252+mediaUrls: ["/tmp/outbound-media/1-chart.png"],
253+replyToCurrent: false,
254+replyToId: "msg-1",
255+replyToTag: false,
256+audioAsVoice: false,
257+});
258+});
259+260+it("does not create a second media context inside runAgentTurnWithFallback when onBlockReply is provided", async () => {
179261// Regression test for openclaw/openclaw#68056.
180-// Before the fix, runAgentTurnWithFallback always called createReplyMediaPathNormalizer
181-// from reply-media-paths.runtime.js to build its own normalizer instance — separate from
182-// the one agent-runner.ts created and passed to buildReplyPayloads. Two separate
183-// persistedMediaBySource caches meant the same source could be persisted twice (two UUID
184-// outbound files, two WhatsApp sends).
262+// Before the fix, runAgentTurnWithFallback created its own media context, separate from
263+// the one agent-runner.ts created and passed to buildReplyPayloads. Two separate caches
264+// meant the same source could be persisted twice (two UUID outbound files, two sends).
185265//
186-// After the fix, agent-runner.ts passes its normalizer into runAgentTurnWithFallback, so
266+// After the fix, agent-runner.ts passes its media context into runAgentTurnWithFallback, so
187267// the .runtime import path is never called from inside that function.
188268runEmbeddedPiAgentMock.mockResolvedValue({
189269payloads: [],
@@ -234,9 +314,9 @@ describe("runReplyAgent media path normalization", () => {
234314},
235315});
236316237-// The .runtime import is only used by agent-runner-execution.ts. After the fix,
238-// runAgentTurnWithFallback receives the normalizer from the caller and never
239-// calls createReplyMediaPathNormalizer itself.
240-expect(createReplyMediaPathNormalizerRuntimeMock).not.toHaveBeenCalled();
317+// The .runtime import is only used by agent-runner-execution.ts. After the fix,
318+// runAgentTurnWithFallback receives the context from the caller and never
319+// creates its own.
320+expect(createReplyMediaContextRuntimeMock).not.toHaveBeenCalled();
241321});
242322});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。