@@ -3,13 +3,16 @@ import os from "node:os";
33import path from "node:path" ;
44import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
55import type { OpenClawConfig } from "../config/config.js" ;
6+ import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js" ;
67import { resolveCliNoOutputTimeoutMs } from "./cli-runner/helpers.js" ;
78import type { EmbeddedContextFile } from "./pi-embedded-helpers.js" ;
89import type { WorkspaceBootstrapFile } from "./workspace.js" ;
910
1011const supervisorSpawnMock = vi . fn ( ) ;
1112const enqueueSystemEventMock = vi . fn ( ) ;
1213const requestHeartbeatNowMock = vi . fn ( ) ;
14+ const SMALL_PNG_BASE64 =
15+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=" ;
1316const hoisted = vi . hoisted ( ( ) => {
1417 type BootstrapContext = {
1518 bootstrapFiles : WorkspaceBootstrapFile [ ] ;
@@ -325,6 +328,135 @@ describe("runCliAgent with process supervisor", () => {
325328 expect ( promptCarrier ) . toContain ( "hi" ) ;
326329 } ) ;
327330
331+ it ( "hydrates prompt media refs into CLI image args" , async ( ) => {
332+ supervisorSpawnMock . mockResolvedValueOnce (
333+ createManagedRun ( {
334+ reason : "exit" ,
335+ exitCode : 0 ,
336+ exitSignal : null ,
337+ durationMs : 50 ,
338+ stdout : "ok" ,
339+ stderr : "" ,
340+ timedOut : false ,
341+ noOutputTimedOut : false ,
342+ } ) ,
343+ ) ;
344+
345+ const tempDir = await fs . mkdtemp (
346+ path . join ( resolvePreferredOpenClawTmpDir ( ) , "openclaw-cli-prompt-image-" ) ,
347+ ) ;
348+ const sourceImage = path . join ( tempDir , "bb-image.png" ) ;
349+ await fs . writeFile ( sourceImage , Buffer . from ( SMALL_PNG_BASE64 , "base64" ) ) ;
350+
351+ try {
352+ await runCliAgent ( {
353+ sessionId : "s1" ,
354+ sessionFile : "/tmp/session.jsonl" ,
355+ workspaceDir : tempDir ,
356+ prompt : `[media attached: ${ sourceImage } (image/png)]\n\n<media:image>` ,
357+ provider : "codex-cli" ,
358+ model : "gpt-5.2-codex" ,
359+ timeoutMs : 1_000 ,
360+ runId : "run-prompt-image" ,
361+ } ) ;
362+ } finally {
363+ await fs . rm ( tempDir , { recursive : true , force : true } ) ;
364+ }
365+
366+ const input = supervisorSpawnMock . mock . calls [ 0 ] ?. [ 0 ] as { argv ?: string [ ] } ;
367+ const argv = input . argv ?? [ ] ;
368+ const imageArgIndex = argv . indexOf ( "--image" ) ;
369+ expect ( imageArgIndex ) . toBeGreaterThanOrEqual ( 0 ) ;
370+ expect ( argv [ imageArgIndex + 1 ] ) . toContain ( "openclaw-cli-images-" ) ;
371+ expect ( argv [ imageArgIndex + 1 ] ) . not . toBe ( sourceImage ) ;
372+ } ) ;
373+
374+ it ( "appends hydrated prompt media refs to generic backend prompts" , async ( ) => {
375+ supervisorSpawnMock . mockResolvedValueOnce (
376+ createManagedRun ( {
377+ reason : "exit" ,
378+ exitCode : 0 ,
379+ exitSignal : null ,
380+ durationMs : 50 ,
381+ stdout : "ok" ,
382+ stderr : "" ,
383+ timedOut : false ,
384+ noOutputTimedOut : false ,
385+ } ) ,
386+ ) ;
387+
388+ const tempDir = await fs . mkdtemp (
389+ path . join ( resolvePreferredOpenClawTmpDir ( ) , "openclaw-cli-prompt-image-generic-" ) ,
390+ ) ;
391+ const sourceImage = path . join ( tempDir , "claude-image.png" ) ;
392+ await fs . writeFile ( sourceImage , Buffer . from ( SMALL_PNG_BASE64 , "base64" ) ) ;
393+
394+ try {
395+ await runCliAgent ( {
396+ sessionId : "s1" ,
397+ sessionFile : "/tmp/session.jsonl" ,
398+ workspaceDir : tempDir ,
399+ prompt : `[media attached: ${ sourceImage } (image/png)]\n\n<media:image>` ,
400+ provider : "claude-cli" ,
401+ model : "claude-opus-4-1" ,
402+ timeoutMs : 1_000 ,
403+ runId : "run-prompt-image-generic" ,
404+ } ) ;
405+ } finally {
406+ await fs . rm ( tempDir , { recursive : true , force : true } ) ;
407+ }
408+
409+ const input = supervisorSpawnMock . mock . calls [ 0 ] ?. [ 0 ] as { argv ?: string [ ] ; input ?: string } ;
410+ const argv = input . argv ?? [ ] ;
411+ expect ( argv ) . not . toContain ( "--image" ) ;
412+ const promptCarrier = [ input . input ?? "" , ...argv ] . join ( "\n" ) ;
413+ const appendedPath = argv . find ( ( value ) => value . includes ( "openclaw-cli-images-" ) ) ;
414+ expect ( appendedPath ) . toBeDefined ( ) ;
415+ expect ( appendedPath ) . not . toBe ( sourceImage ) ;
416+ expect ( promptCarrier ) . toContain ( appendedPath ?? "" ) ;
417+ } ) ;
418+
419+ it ( "prefers explicit images over prompt refs" , async ( ) => {
420+ supervisorSpawnMock . mockResolvedValueOnce (
421+ createManagedRun ( {
422+ reason : "exit" ,
423+ exitCode : 0 ,
424+ exitSignal : null ,
425+ durationMs : 50 ,
426+ stdout : "ok" ,
427+ stderr : "" ,
428+ timedOut : false ,
429+ noOutputTimedOut : false ,
430+ } ) ,
431+ ) ;
432+
433+ const tempDir = await fs . mkdtemp (
434+ path . join ( resolvePreferredOpenClawTmpDir ( ) , "openclaw-cli-explicit-images-" ) ,
435+ ) ;
436+ const sourceImage = path . join ( tempDir , "ignored-prompt-image.png" ) ;
437+ await fs . writeFile ( sourceImage , Buffer . from ( SMALL_PNG_BASE64 , "base64" ) ) ;
438+
439+ try {
440+ await runCliAgent ( {
441+ sessionId : "s1" ,
442+ sessionFile : "/tmp/session.jsonl" ,
443+ workspaceDir : tempDir ,
444+ prompt : `[media attached: ${ sourceImage } (image/png)]\n\n<media:image>` ,
445+ images : [ { type : "image" , data : SMALL_PNG_BASE64 , mimeType : "image/png" } ] ,
446+ provider : "codex-cli" ,
447+ model : "gpt-5.2-codex" ,
448+ timeoutMs : 1_000 ,
449+ runId : "run-explicit-image-precedence" ,
450+ } ) ;
451+ } finally {
452+ await fs . rm ( tempDir , { recursive : true , force : true } ) ;
453+ }
454+
455+ const input = supervisorSpawnMock . mock . calls [ 0 ] ?. [ 0 ] as { argv ?: string [ ] } ;
456+ const argv = input . argv ?? [ ] ;
457+ expect ( argv . filter ( ( arg ) => arg === "--image" ) ) . toHaveLength ( 1 ) ;
458+ } ) ;
459+
328460 it ( "fails with timeout when no-output watchdog trips" , async ( ) => {
329461 supervisorSpawnMock . mockResolvedValueOnce (
330462 createManagedRun ( {
0 commit comments