@@ -234,6 +234,7 @@ describe("package-openclaw-for-docker", () => {
234234 ) } \n`;
235235 const installedAiPath = path . join ( sourceDir , "node_modules" , "@openclaw" , "ai" ) ;
236236 fs . mkdirSync ( path . join ( sourceDir , "packages" , "ai" ) , { recursive : true } ) ;
237+ fs . writeFileSync ( path . join ( sourceDir , "packages" , "ai" , "package.json" ) , "{}\n" ) ;
237238 fs . mkdirSync ( installedAiPath , { recursive : true } ) ;
238239 fs . writeFileSync ( path . join ( installedAiPath , "original-marker" ) , "workspace package" ) ;
239240 fs . writeFileSync ( packageJsonPath , originalPackageJson ) ;
@@ -291,6 +292,67 @@ describe("package-openclaw-for-docker", () => {
291292 }
292293 } ) ;
293294
295+ it ( "leaves pre-AI-workspace package sources unchanged" , async ( ) => {
296+ const sourceDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-docker-legacy-source-" ) ) ;
297+ const outputDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-docker-legacy-output-" ) ) ;
298+ const packageJsonPath = path . join ( sourceDir , "package.json" ) ;
299+ const originalPackageJson = `${ JSON . stringify ( {
300+ dependencies : { "dep-a" : "1.2.3" } ,
301+ name : "openclaw" ,
302+ version : "2026.7.1" ,
303+ } ) } \n`;
304+ fs . writeFileSync ( packageJsonPath , originalPackageJson ) ;
305+ const runCapture = vi . fn ( ) ;
306+
307+ try {
308+ const cleanup = await prepareBundledAiRuntimePackage ( sourceDir , outputDir , runCapture ) ;
309+
310+ expect ( runCapture ) . not . toHaveBeenCalled ( ) ;
311+ expect ( fs . readFileSync ( packageJsonPath , "utf8" ) ) . toBe ( originalPackageJson ) ;
312+ await cleanup ( ) ;
313+ } finally {
314+ fs . rmSync ( sourceDir , { recursive : true , force : true } ) ;
315+ fs . rmSync ( outputDir , { recursive : true , force : true } ) ;
316+ }
317+ } ) ;
318+
319+ it ( "rejects incomplete AI workspace package sources" , async ( ) => {
320+ const cases = [
321+ {
322+ dependencies : { "@openclaw/ai" : "workspace:*" } ,
323+ expected : "@openclaw/ai dependency requires the packages/ai workspace" ,
324+ withWorkspace : false ,
325+ } ,
326+ {
327+ dependencies : { } ,
328+ expected : "root package.json must declare @openclaw/ai as a dependency" ,
329+ withWorkspace : true ,
330+ } ,
331+ ] ;
332+
333+ for ( const testCase of cases ) {
334+ const sourceDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-docker-invalid-source-" ) ) ;
335+ const outputDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-docker-invalid-output-" ) ) ;
336+ fs . writeFileSync (
337+ path . join ( sourceDir , "package.json" ) ,
338+ `${ JSON . stringify ( { dependencies : testCase . dependencies , name : "openclaw" } ) } \n` ,
339+ ) ;
340+ if ( testCase . withWorkspace ) {
341+ fs . mkdirSync ( path . join ( sourceDir , "packages" , "ai" ) , { recursive : true } ) ;
342+ fs . writeFileSync ( path . join ( sourceDir , "packages" , "ai" , "package.json" ) , "{}\n" ) ;
343+ }
344+
345+ try {
346+ await expect ( prepareBundledAiRuntimePackage ( sourceDir , outputDir , vi . fn ( ) ) ) . rejects . toThrow (
347+ testCase . expected ,
348+ ) ;
349+ } finally {
350+ fs . rmSync ( sourceDir , { recursive : true , force : true } ) ;
351+ fs . rmSync ( outputDir , { recursive : true , force : true } ) ;
352+ }
353+ }
354+ } ) ;
355+
294356 it ( "trims and restores the changelog around ignore-scripts package artifacts" , async ( ) => {
295357 const calls : string [ ] = [ ] ;
296358 const tarball = await packOpenClawPackageForDocker ( "/repo" , "/out" , {
0 commit comments