1+ import { spawnSync } from "node:child_process" ;
12import fs from "node:fs/promises" ;
23import path from "node:path" ;
34import { describe , expect , it } from "vitest" ;
45import { buildSandboxFsMounts , resolveSandboxFsPathWithMounts } from "./fs-paths.js" ;
6+ import { SANDBOX_PINNED_MUTATION_PYTHON } from "./fs-bridge-mutation-helper.js" ;
57import { createSandbox , withTempDir } from "./fs-bridge.test-helpers.js" ;
8+ import { createRemoteShellSandboxFsBridge } from "./remote-fs-bridge.js" ;
69
710describe ( "workspace skills bridge mount policy" , ( ) => {
811 it ( "resolves workspace skill roots as read-only" , async ( ) => {
@@ -28,4 +31,61 @@ describe("workspace skills bridge mount policy", () => {
2831 expect ( resolve ( "/workspace/skills/demo/SKILL.md" ) . writable ) . toBe ( false ) ;
2932 } ) ;
3033 } ) ;
34+
35+ it . runIf ( process . platform !== "win32" ) (
36+ "allows remote bridge writes under absent skill roots" ,
37+ async ( ) => {
38+ await withTempDir ( "openclaw-skills-remote-absent-" , async ( stateDir ) => {
39+ const workspaceDir = path . join ( stateDir , "workspace" ) ;
40+ await fs . mkdir ( workspaceDir , { recursive : true } ) ;
41+
42+ const bridge = createRemoteShellSandboxFsBridge ( {
43+ sandbox : createSandbox ( { workspaceDir, agentWorkspaceDir : workspaceDir } ) ,
44+ runtime : {
45+ remoteWorkspaceDir : workspaceDir ,
46+ remoteAgentWorkspaceDir : workspaceDir ,
47+ runRemoteShellScript : async ( command ) => {
48+ const result = command . script . includes ( 'python3 /dev/fd/3 "$@" 3<<' )
49+ ? spawnSync (
50+ "python3" ,
51+ [ "-c" , SANDBOX_PINNED_MUTATION_PYTHON , ...( command . args ?? [ ] ) ] ,
52+ {
53+ input : command . stdin ,
54+ encoding : "buffer" ,
55+ stdio : [ "pipe" , "pipe" , "pipe" ] ,
56+ } ,
57+ )
58+ : spawnSync ( "sh" , [ "-c" , command . script , "openclaw-test" , ...( command . args ?? [ ] ) ] , {
59+ input : command . stdin ,
60+ encoding : "buffer" ,
61+ stdio : [ "pipe" , "pipe" , "pipe" ] ,
62+ } ) ;
63+ const stdout = Buffer . isBuffer ( result . stdout )
64+ ? result . stdout
65+ : Buffer . from ( result . stdout ?? [ ] ) ;
66+ const stderr = Buffer . isBuffer ( result . stderr )
67+ ? result . stderr
68+ : Buffer . from ( result . stderr ?? [ ] ) ;
69+ const code = result . status ?? ( result . signal ? 128 : 1 ) ;
70+ if ( result . error ) {
71+ throw result . error ;
72+ }
73+ if ( code !== 0 && ! command . allowFailure ) {
74+ throw Object . assign (
75+ new Error ( stderr . toString ( "utf8" ) . trim ( ) || `shell exited with code ${ code } ` ) ,
76+ { code, stdout, stderr } ,
77+ ) ;
78+ }
79+ return { stdout, stderr, code } ;
80+ } ,
81+ } ,
82+ } ) ;
83+
84+ await bridge . writeFile ( { filePath : "skills/new.txt" , data : "created" } ) ;
85+ await expect ( fs . readFile ( path . join ( workspaceDir , "skills" , "new.txt" ) , "utf8" ) ) . resolves . toBe (
86+ "created" ,
87+ ) ;
88+ } ) ;
89+ } ,
90+ ) ;
3191} ) ;
0 commit comments