@@ -50,12 +50,14 @@ registerGetReplyRuntimeOverrides(mocks);
5050let getReplyFromConfig : typeof import ( "./get-reply.js" ) . getReplyFromConfig ;
5151let resolveDefaultModelMock : typeof import ( "./directive-handling.defaults.js" ) . resolveDefaultModel ;
5252let runPreparedReplyMock : typeof import ( "./get-reply-run.js" ) . runPreparedReply ;
53+ let stageSandboxMediaMock : typeof import ( "./stage-sandbox-media.runtime.js" ) . stageSandboxMedia ;
5354
5455async function loadGetReplyRuntimeForTest ( ) {
5556 ( { getReplyFromConfig } = await loadGetReplyModuleForTest ( { cacheKey : import . meta. url } ) ) ;
5657 ( { resolveDefaultModel : resolveDefaultModelMock } =
5758 await import ( "./directive-handling.defaults.js" ) ) ;
5859 ( { runPreparedReply : runPreparedReplyMock } = await import ( "./get-reply-run.js" ) ) ;
60+ ( { stageSandboxMedia : stageSandboxMediaMock } = await import ( "./stage-sandbox-media.runtime.js" ) ) ;
5961}
6062
6163function emptyAliasIndex ( ) {
@@ -101,6 +103,7 @@ describe("getReplyFromConfig message hooks", () => {
101103 mocks . initSessionState . mockReset ( ) ;
102104 vi . mocked ( resolveDefaultModelMock ) . mockReset ( ) ;
103105 vi . mocked ( runPreparedReplyMock ) . mockReset ( ) ;
106+ vi . mocked ( stageSandboxMediaMock ) . mockReset ( ) ;
104107 vi . mocked ( logVerbose ) . mockReset ( ) ;
105108
106109 mocks . applyMediaUnderstanding . mockImplementation ( async ( ...args : unknown [ ] ) => {
@@ -141,6 +144,7 @@ describe("getReplyFromConfig message hooks", () => {
141144 aliasIndex : emptyAliasIndex ( ) ,
142145 } ) ;
143146 vi . mocked ( runPreparedReplyMock ) . mockResolvedValue ( { text : "ok" } ) ;
147+ vi . mocked ( stageSandboxMediaMock ) . mockResolvedValue ( { staged : new Map ( ) } ) ;
144148 mocks . initSessionState . mockResolvedValue (
145149 createGetReplySessionState ( {
146150 sessionKey : "agent:main:telegram:-100123" ,
@@ -281,6 +285,70 @@ describe("getReplyFromConfig message hooks", () => {
281285 text : "a tiny dot image" ,
282286 } ) ,
283287 ] ) ;
288+ expect ( stageSandboxMediaMock ) . not . toHaveBeenCalled ( ) ;
289+ } ) ;
290+
291+ it ( "stages remote iMessage media before media understanding" , async ( ) => {
292+ const order : string [ ] = [ ] ;
293+ const remotePath = "/Users/demo/Library/Messages/Attachments/ab/cd/photo.jpg" ;
294+ const stagedPath = "/tmp/openclaw-remote-cache/photo.jpg" ;
295+ vi . mocked ( stageSandboxMediaMock ) . mockImplementationOnce ( async ( params ) => {
296+ order . push ( "stage" ) ;
297+ params . ctx . MediaPath = stagedPath ;
298+ params . ctx . MediaPaths = [ stagedPath ] ;
299+ params . ctx . MediaUrl = stagedPath ;
300+ params . ctx . MediaUrls = [ stagedPath ] ;
301+ params . sessionCtx . MediaPath = stagedPath ;
302+ params . sessionCtx . MediaPaths = [ stagedPath ] ;
303+ params . sessionCtx . MediaUrl = stagedPath ;
304+ params . sessionCtx . MediaUrls = [ stagedPath ] ;
305+ return { staged : new Map ( [ [ remotePath , stagedPath ] ] ) } ;
306+ } ) ;
307+ mocks . applyMediaUnderstanding . mockImplementationOnce ( async ( ...args : unknown [ ] ) => {
308+ order . push ( "understand" ) ;
309+ const { ctx } = args [ 0 ] as { ctx : MsgContext } ;
310+ expect ( ctx . MediaPath ) . toBe ( stagedPath ) ;
311+ expect ( ctx . MediaPaths ) . toEqual ( [ stagedPath ] ) ;
312+ expect ( ctx . MediaUrl ) . toBe ( stagedPath ) ;
313+ expect ( ctx . MediaUrls ) . toEqual ( [ stagedPath ] ) ;
314+ expect ( ctx . MediaStaged ) . toBe ( true ) ;
315+ } ) ;
316+
317+ await getReplyFromConfig (
318+ buildCtx ( {
319+ Provider : "imessage" ,
320+ Surface : "imessage" ,
321+ OriginatingChannel : "imessage" ,
322+ OriginatingTo : "imessage:chat:abc" ,
323+ ChatType : "direct" ,
324+ Body : "please describe this" ,
325+ BodyForAgent : "please describe this" ,
326+ RawBody : "please describe this" ,
327+ CommandBody : "please describe this" ,
328+ BodyForCommands : "please describe this" ,
329+ SessionKey : "agent:main:imessage:direct:user" ,
330+ From : "imessage:user" ,
331+ To : "imessage:chat:abc" ,
332+ MediaPath : remotePath ,
333+ MediaPaths : [ remotePath ] ,
334+ MediaUrl : remotePath ,
335+ MediaUrls : [ remotePath ] ,
336+ MediaType : "image/jpeg" ,
337+ MediaTypes : [ "image/jpeg" ] ,
338+ MediaRemoteHost : "user@gateway-host" ,
339+ } ) ,
340+ undefined ,
341+ withFastReplyConfig ( { } ) ,
342+ ) ;
343+
344+ expect ( order ) . toEqual ( [ "stage" , "understand" ] ) ;
345+ expect ( stageSandboxMediaMock ) . toHaveBeenCalledTimes ( 1 ) ;
346+ expect ( stageSandboxMediaMock ) . toHaveBeenCalledWith (
347+ expect . objectContaining ( {
348+ sessionKey : "agent:main:imessage:direct:user" ,
349+ workspaceDir : "/tmp/workspace" ,
350+ } ) ,
351+ ) ;
284352 } ) ;
285353
286354 it ( "emits only preprocessed when no transcript is produced" , async ( ) => {
0 commit comments