77 mkdtempSync ,
88 readFileSync ,
99 rmSync ,
10+ symlinkSync ,
1011 writeFileSync ,
1112} from "node:fs" ;
1213import { tmpdir } from "node:os" ;
@@ -237,9 +238,14 @@ function extractEnsureLocalUpdateDistImportClosure(): string {
237238 return match [ 1 ] ;
238239}
239240
240- function runRestoreLocalDistFixture ( options : { failAiSwap ?: boolean } = { } ) {
241+ type RestorePathEscape = "packages" | "ai" ;
242+
243+ function runRestoreLocalDistFixture (
244+ options : { failAiSwap ?: boolean ; symlinkEscape ?: RestorePathEscape } = { } ,
245+ ) {
241246 const fixtureRoot = tempDirs . make ( "openclaw-install-restore-root-" ) ;
242247 const imageRoot = tempDirs . make ( "openclaw-install-restore-image-" ) ;
248+ let externalSentinel = "" ;
243249 for ( const [ relativePath , contents ] of [
244250 [ "dist/root.txt" , "old-root" ] ,
245251 [ "packages/ai/dist/ai.txt" , "old-ai" ] ,
@@ -258,6 +264,23 @@ function runRestoreLocalDistFixture(options: { failAiSwap?: boolean } = {}) {
258264 writeFileSync ( target , contents ) ;
259265 }
260266
267+ if ( options . symlinkEscape ) {
268+ const escapeRoot = tempDirs . make ( "openclaw-install-restore-escape-" ) ;
269+ const externalAiRoot =
270+ options . symlinkEscape === "packages" ? join ( escapeRoot , "packages" , "ai" ) : escapeRoot ;
271+ externalSentinel = join ( externalAiRoot , "dist" , "ai.txt" ) ;
272+ mkdirSync ( path . dirname ( externalSentinel ) , { recursive : true } ) ;
273+ writeFileSync ( join ( externalAiRoot , "package.json" ) , "{}" ) ;
274+ writeFileSync ( externalSentinel , "external-ai" ) ;
275+ if ( options . symlinkEscape === "packages" ) {
276+ rmSync ( join ( fixtureRoot , "packages" ) , { force : true , recursive : true } ) ;
277+ symlinkSync ( join ( escapeRoot , "packages" ) , join ( fixtureRoot , "packages" ) , "dir" ) ;
278+ } else {
279+ rmSync ( join ( fixtureRoot , "packages" , "ai" ) , { force : true , recursive : true } ) ;
280+ symlinkSync ( externalAiRoot , join ( fixtureRoot , "packages" , "ai" ) , "dir" ) ;
281+ }
282+ }
283+
261284 return spawnSync (
262285 "bash" ,
263286 [
@@ -269,6 +292,7 @@ REPO_ROOT="$FIXTURE_REPO"
269292ROOT_DIR="$FIXTURE_ROOT"
270293IMAGE_ROOT="$FIXTURE_IMAGE"
271294docker_e2e_docker_cmd() {
295+ printf 'docker-call=%s\\n' "$1" >&2
272296 case "$1" in
273297 create)
274298 printf "fixture"
@@ -285,7 +309,7 @@ docker_e2e_docker_cmd() {
285309 esac
286310}
287311mv() {
288- if [[ "$FAIL_AI_SWAP" == "1" && "$1" == */ai-dist && "$2" == "$ROOT_DIR /packages/ai/dist" ]]; then
312+ if [[ "$FAIL_AI_SWAP" == "1" && "$1" == */ai-dist && "$2" == * /packages/ai/dist ]]; then
289313 return 1
290314 fi
291315 command mv "$@"
@@ -296,13 +320,17 @@ docker_e2e_restore_package_dist_from_image fixture-image || status=$?
296320printf 'status=%s\\n' "$status"
297321printf 'root=%s\\n' "$(cat "$ROOT_DIR/dist/root.txt")"
298322printf 'ai=%s\\n' "$(cat "$ROOT_DIR/packages/ai/dist/ai.txt")"
323+ if [[ -n "$EXTERNAL_SENTINEL" ]]; then
324+ printf 'external=%s\\n' "$(cat "$EXTERNAL_SENTINEL")"
325+ fi
299326` ,
300327 ] ,
301328 {
302329 encoding : "utf8" ,
303330 env : {
304331 ...process . env ,
305332 FAIL_AI_SWAP : options . failAiSwap ? "1" : "0" ,
333+ EXTERNAL_SENTINEL : externalSentinel ,
306334 FIXTURE_IMAGE : imageRoot ,
307335 FIXTURE_REPO : process . cwd ( ) ,
308336 FIXTURE_ROOT : fixtureRoot ,
@@ -457,12 +485,12 @@ describe("test-install-sh-docker", () => {
457485 ) ;
458486 expect ( packageHelper ) . toContain ( '"${container_id}:/app/node_modules/@openclaw/ai/dist"' ) ;
459487 expect ( packageHelper ) . toContain ( '"$temp_dir/ai-dist"' ) ;
460- expect ( packageHelper ) . toContain ( 'mv "$temp_dir/ai-dist" "$ROOT_DIR/packages/ai/dist "' ) ;
488+ expect ( packageHelper ) . toContain ( 'mv "$temp_dir/ai-dist" "$ai_dist_dir "' ) ;
461489 expect ( packageHelper ) . toContain ( "cleanup_restore_package_dist() {" ) ;
462- expect ( packageHelper ) . toContain ( 'mv "$ROOT_DIR /dist" "$backup_dir"' ) ;
463- expect ( packageHelper ) . toContain ( 'mv "$temp_dir/dist" "$ROOT_DIR /dist"' ) ;
464- expect ( packageHelper ) . toContain ( 'rm -rf "$ROOT_DIR /dist" >/dev/null 2>&1 || true' ) ;
465- expect ( packageHelper ) . toContain ( 'mv "$backup_dir" "$ROOT_DIR /dist"' ) ;
490+ expect ( packageHelper ) . toContain ( 'mv "$restore_root /dist" "$backup_dir"' ) ;
491+ expect ( packageHelper ) . toContain ( 'mv "$temp_dir/dist" "$restore_root /dist"' ) ;
492+ expect ( packageHelper ) . toContain ( 'rm -rf "$restore_root /dist" >/dev/null 2>&1 || true' ) ;
493+ expect ( packageHelper ) . toContain ( 'mv "$backup_dir" "$restore_root /dist"' ) ;
466494 expect ( packageHelper ) . toContain ( 'docker_e2e_docker_cmd rm -f "$container_id"' ) ;
467495 expect ( script ) . not . toContain ( 'container_id="$(docker create "$image")"' ) ;
468496 expect ( script ) . not . toContain ( 'docker cp "${container_id}:/app/dist" "$ROOT_DIR/dist"' ) ;
@@ -499,6 +527,21 @@ describe("test-install-sh-docker", () => {
499527 expect ( result . stdout ) . toContain ( "ai=old-ai" ) ;
500528 } ) ;
501529
530+ it . each ( [ "packages" , "ai" ] as const ) (
531+ "rejects a symlinked %s path before restoring artifacts" ,
532+ ( symlinkEscape ) => {
533+ const result = runRestoreLocalDistFixture ( { symlinkEscape } ) ;
534+
535+ expect ( result . status , result . stderr ) . toBe ( 0 ) ;
536+ expect ( result . stdout ) . toContain ( "status=1" ) ;
537+ expect ( result . stdout ) . toContain ( "root=old-root" ) ;
538+ expect ( result . stdout ) . toContain ( "ai=external-ai" ) ;
539+ expect ( result . stdout ) . toContain ( "external=external-ai" ) ;
540+ expect ( result . stderr ) . not . toContain ( "docker-call=" ) ;
541+ expect ( result . stderr ) . toContain ( "refusing package artifact restore through a symlinked" ) ;
542+ } ,
543+ ) ;
544+
502545 it ( "fails closed when exact image artifacts fail import closure" , ( ) => {
503546 const result = spawnSync (
504547 "bash" ,
@@ -1069,13 +1112,13 @@ describe("bun global install smoke", () => {
10691112 ) ;
10701113 expect ( packageHelper ) . toContain ( '"${container_id}:/app/node_modules/@openclaw/ai/dist"' ) ;
10711114 expect ( packageHelper ) . toContain ( '"$temp_dir/ai-dist"' ) ;
1072- expect ( packageHelper ) . toContain ( 'mv "$temp_dir/ai-dist" "$ROOT_DIR/packages/ai/dist "' ) ;
1115+ expect ( packageHelper ) . toContain ( 'mv "$temp_dir/ai-dist" "$ai_dist_dir "' ) ;
10731116 expect ( packageHelper ) . toContain ( "cleanup_restore_package_dist() {" ) ;
1074- expect ( packageHelper ) . toContain ( 'mv "$ROOT_DIR /dist" "$backup_dir"' ) ;
1075- expect ( packageHelper ) . toContain ( 'mv "$temp_dir/dist" "$ROOT_DIR /dist"' ) ;
1076- expect ( packageHelper ) . toContain ( 'mktemp -d "$ROOT_DIR /.package-dist.XXXXXX"' ) ;
1077- expect ( packageHelper ) . toContain ( 'rm -rf "$ROOT_DIR /dist" >/dev/null 2>&1 || true' ) ;
1078- expect ( packageHelper ) . toContain ( 'mv "$backup_dir" "$ROOT_DIR /dist"' ) ;
1117+ expect ( packageHelper ) . toContain ( 'mv "$restore_root /dist" "$backup_dir"' ) ;
1118+ expect ( packageHelper ) . toContain ( 'mv "$temp_dir/dist" "$restore_root /dist"' ) ;
1119+ expect ( packageHelper ) . toContain ( 'mktemp -d "$restore_root /.package-dist.XXXXXX"' ) ;
1120+ expect ( packageHelper ) . toContain ( 'rm -rf "$restore_root /dist" >/dev/null 2>&1 || true' ) ;
1121+ expect ( packageHelper ) . toContain ( 'mv "$backup_dir" "$restore_root /dist"' ) ;
10791122 expect ( packageHelper ) . toContain ( 'docker_e2e_docker_cmd rm -f "$container_id"' ) ;
10801123 expect ( script ) . not . toContain ( 'container_id="$(docker create "$image")"' ) ;
10811124 expect ( script ) . not . toContain ( 'docker cp "${container_id}:/app/dist" "$ROOT_DIR/dist"' ) ;
0 commit comments