Skip to content

Commit 830467b

Browse files
fix(imessage): stage remote media for plugin claims (#91803)
1 parent d1b9171 commit 830467b

8 files changed

Lines changed: 581 additions & 57 deletions

extensions/codex/src/conversation-turn-input.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,31 @@ describe("codex conversation turn input", () => {
2424
]);
2525
});
2626

27+
it("uses staged remote-cache paths for remote iMessage image attachments", () => {
28+
const rawPath = "/Users/demo/Library/Messages/Attachments/ab/cd/photo.jpg";
29+
const stagedPath = "/tmp/openclaw-proof/.openclaw/media/remote-cache/imessage/photo.jpg";
30+
31+
const input = buildCodexConversationTurnInput({
32+
prompt: "what is this?",
33+
event: {
34+
content: "what is this?",
35+
channel: "imessage",
36+
isGroup: false,
37+
metadata: {
38+
mediaPaths: [stagedPath],
39+
mediaTypes: ["image/jpeg"],
40+
originalMediaPaths: [rawPath],
41+
},
42+
},
43+
});
44+
45+
expect(input).toEqual([
46+
{ type: "text", text: "what is this?", text_elements: [] },
47+
{ type: "localImage", path: stagedPath },
48+
]);
49+
expect(input).not.toContainEqual({ type: "localImage", path: rawPath });
50+
});
51+
2752
it("uses remote image urls when no local path is available", () => {
2853
expect(
2954
buildCodexConversationTurnInput({

src/auto-reply/reply.stage-sandbox-media.scp-remote-path.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/** Tests sandbox media staging for SCP remote-path inputs. */
2+
import { EventEmitter } from "node:events";
23
import fs from "node:fs/promises";
34
import { basename, join } from "node:path";
45
import { afterEach, describe, expect, it, vi } from "vitest";
@@ -151,4 +152,115 @@ describe("stageSandboxMedia scp remote paths", () => {
151152
}
152153
});
153154
});
155+
156+
it("rewrites remote iMessage attachment metadata to the staged local cache path", async () => {
157+
await withSandboxMediaTempHome("openclaw-triggers-", async (home) => {
158+
const { cfg, workspaceDir, sessionKey } = createRemoteStageParams(home);
159+
const remotePath = "/Users/demo/Library/Messages/Attachments/ab/cd/photo.jpg";
160+
const { ctx, sessionCtx } = createRemoteContexts(remotePath);
161+
ctx.MediaPaths = [remotePath];
162+
sessionCtx.MediaPaths = [remotePath];
163+
childProcessMocks.spawn.mockImplementation((_command, argsUnknown) => {
164+
const args = argsUnknown as string[];
165+
const localPath = args[args.length - 1];
166+
const child = new EventEmitter() as EventEmitter & {
167+
stderr: EventEmitter & { setEncoding: (_encoding: string) => void };
168+
};
169+
child.stderr = Object.assign(new EventEmitter(), {
170+
setEncoding: () => undefined,
171+
});
172+
queueMicrotask(() => {
173+
void fs.writeFile(localPath, "staged-image-bytes").then(() => {
174+
child.emit("exit", 0);
175+
});
176+
});
177+
return child;
178+
});
179+
180+
const result = await stageSandboxMedia({
181+
ctx,
182+
sessionCtx,
183+
cfg,
184+
sessionKey,
185+
workspaceDir,
186+
});
187+
188+
const stagedPath = join(
189+
CONFIG_DIR,
190+
"media",
191+
"remote-cache",
192+
slugifySessionKey(sessionKey),
193+
basename(remotePath),
194+
);
195+
expect(result.staged.get(remotePath)).toBe(stagedPath);
196+
expect(ctx.MediaPath).toBe(stagedPath);
197+
expect(ctx.MediaPaths).toEqual([stagedPath]);
198+
expect(ctx.MediaUrl).toBe(stagedPath);
199+
expect(sessionCtx.MediaPath).toBe(stagedPath);
200+
expect(sessionCtx.MediaPaths).toEqual([stagedPath]);
201+
expect(sessionCtx.MediaUrl).toBe(stagedPath);
202+
expect(await fs.readFile(stagedPath, "utf8")).toBe("staged-image-bytes");
203+
await fs.rm(join(CONFIG_DIR, "media", "remote-cache", slugifySessionKey(sessionKey)), {
204+
recursive: true,
205+
force: true,
206+
});
207+
});
208+
});
209+
210+
it("uses absolute remote cache paths in cache mode even when sandbox staging is available", async () => {
211+
await withSandboxMediaTempHome("openclaw-triggers-", async (home) => {
212+
const { cfg, workspaceDir, sessionKey } = createRemoteStageParams(home);
213+
const sandboxWorkspace = join(home, "sandbox-workspace");
214+
vi.mocked(sandboxMocks.ensureSandboxWorkspaceForSession).mockResolvedValue({
215+
workspaceDir: sandboxWorkspace,
216+
workspaceAccess: "workspace-write",
217+
});
218+
const remotePath = "/Users/demo/Library/Messages/Attachments/ab/cd/photo.jpg";
219+
const { ctx, sessionCtx } = createRemoteContexts(remotePath);
220+
ctx.MediaPaths = [remotePath];
221+
sessionCtx.MediaPaths = [remotePath];
222+
childProcessMocks.spawn.mockImplementation((_command, argsUnknown) => {
223+
const args = argsUnknown as string[];
224+
const localPath = args[args.length - 1];
225+
const child = new EventEmitter() as EventEmitter & {
226+
stderr: EventEmitter & { setEncoding: (_encoding: string) => void };
227+
};
228+
child.stderr = Object.assign(new EventEmitter(), {
229+
setEncoding: () => undefined,
230+
});
231+
queueMicrotask(() => {
232+
void fs.writeFile(localPath, "staged-image-bytes").then(() => {
233+
child.emit("exit", 0);
234+
});
235+
});
236+
return child;
237+
});
238+
239+
const result = await stageSandboxMedia({
240+
ctx,
241+
sessionCtx,
242+
cfg,
243+
sessionKey,
244+
workspaceDir,
245+
remoteMediaMode: "cache",
246+
});
247+
248+
const stagedPath = join(
249+
CONFIG_DIR,
250+
"media",
251+
"remote-cache",
252+
slugifySessionKey(sessionKey),
253+
basename(remotePath),
254+
);
255+
expect(result.staged.get(remotePath)).toBe(stagedPath);
256+
expect(ctx.MediaPath).toBe(stagedPath);
257+
expect(ctx.MediaPaths).toEqual([stagedPath]);
258+
await expectPathMissing(join(sandboxWorkspace, "media", "inbound", basename(remotePath)));
259+
expect(await fs.readFile(stagedPath, "utf8")).toBe("staged-image-bytes");
260+
await fs.rm(join(CONFIG_DIR, "media", "remote-cache", slugifySessionKey(sessionKey)), {
261+
recursive: true,
262+
force: true,
263+
});
264+
});
265+
});
154266
});

0 commit comments

Comments
 (0)