@@ -15,7 +15,6 @@ import { S3FileSystem } from "./s3-filesystem.js";
1515
1616const BUCKET = "secure-exec-vfs-test" ;
1717
18- // Connect to MinIO (S3-compatible) running on localhost
1918const client = new S3Client ( {
2019 endpoint : "http://localhost:9000" ,
2120 region : "us-east-1" ,
@@ -26,7 +25,6 @@ const client = new S3Client({
2625 forcePathStyle : true ,
2726} ) ;
2827
29- // Create test bucket
3028try {
3129 await client . send ( new CreateBucketCommand ( { Bucket : BUCKET } ) ) ;
3230} catch ( err : unknown ) {
@@ -49,72 +47,257 @@ const runtime = new NodeRuntime({
4947 runtimeDriverFactory : createNodeRuntimeDriverFactory ( ) ,
5048} ) ;
5149
52- try {
53- // Sandbox writes files — stored in MinIO via S3 API
54- const writeResult = await runtime . exec ( `
55- const fs = require("node:fs");
56- fs.mkdirSync("/workspace", { recursive: true });
57- fs.writeFileSync("/workspace/hello.txt", "hello from sandbox via S3");
58- fs.writeFileSync("/workspace/data.json", JSON.stringify({ count: 42 }));
59- console.log("files written");
60- ` ) ;
61-
62- if ( writeResult . code !== 0 ) {
63- throw new Error ( `Write step failed: ${ writeResult . errorMessage } ` ) ;
50+ const failures : string [ ] = [ ] ;
51+
52+ function assert ( condition : boolean , message : string ) {
53+ if ( ! condition ) {
54+ failures . push ( message ) ;
55+ console . error ( `FAIL: ${ message } ` ) ;
6456 }
57+ }
6558
66- // Sandbox reads files back
67- let readOutput = "" ;
68- const readResult = await runtime . exec (
69- `
70- const fs = require("node:fs");
71- const text = fs.readFileSync("/workspace/hello.txt", "utf8");
72- const data = JSON.parse(fs.readFileSync("/workspace/data.json", "utf8"));
73- const entries = fs.readdirSync("/workspace");
74- console.log(JSON.stringify({ text, count: data.count, entries }));
75- ` ,
76- {
77- onStdio : ( event ) => {
78- if ( event . channel === "stdout" ) readOutput += event . message ;
79- } ,
59+ async function exec (
60+ code : string ,
61+ ) : Promise < { code : number ; stdout : string ; stderr : string } > {
62+ let stdout = "" ;
63+ let stderr = "" ;
64+ const result = await runtime . exec ( code , {
65+ onStdio : ( event ) => {
66+ if ( event . channel === "stdout" ) stdout += event . message ;
67+ else stderr += event . message ;
8068 } ,
81- ) ;
69+ } ) ;
70+ return { code : result . code , stdout, stderr } ;
71+ }
72+
73+ try {
74+ // --- writeFile + readFile + readDir ---
75+ {
76+ const r = await exec ( `
77+ const fs = require("node:fs");
78+ fs.mkdirSync("/workspace", { recursive: true });
79+ fs.writeFileSync("/workspace/hello.txt", "hello from sandbox via S3");
80+ fs.writeFileSync("/workspace/data.json", JSON.stringify({ count: 42 }));
81+ const text = fs.readFileSync("/workspace/hello.txt", "utf8");
82+ const data = JSON.parse(fs.readFileSync("/workspace/data.json", "utf8"));
83+ const entries = fs.readdirSync("/workspace");
84+ console.log(JSON.stringify({ text, count: data.count, entries }));
85+ ` ) ;
86+ assert ( r . code === 0 , `writeFile+readFile exec failed: ${ r . stderr } ` ) ;
87+ const parsed = JSON . parse ( r . stdout . trim ( ) ) ;
88+ assert (
89+ parsed . text === "hello from sandbox via S3" ,
90+ "readFile content mismatch" ,
91+ ) ;
92+ assert ( parsed . count === 42 , "readFile JSON data mismatch" ) ;
93+ assert (
94+ parsed . entries . includes ( "hello.txt" ) ,
95+ "readDir missing hello.txt" ,
96+ ) ;
97+ assert (
98+ parsed . entries . includes ( "data.json" ) ,
99+ "readDir missing data.json" ,
100+ ) ;
101+ const hostContent = await filesystem . readTextFile (
102+ "/workspace/hello.txt" ,
103+ ) ;
104+ assert (
105+ hostContent === "hello from sandbox via S3" ,
106+ "host readTextFile mismatch" ,
107+ ) ;
108+ }
109+
110+ // --- stat ---
111+ {
112+ const r = await exec ( `
113+ const fs = require("node:fs");
114+ const fileStat = fs.statSync("/workspace/hello.txt");
115+ const dirStat = fs.statSync("/workspace");
116+ console.log(JSON.stringify({
117+ fileIsDir: fileStat.isDirectory(),
118+ fileSize: fileStat.size,
119+ dirIsDir: dirStat.isDirectory(),
120+ }));
121+ ` ) ;
122+ assert ( r . code === 0 , `stat exec failed: ${ r . stderr } ` ) ;
123+ const parsed = JSON . parse ( r . stdout . trim ( ) ) ;
124+ assert ( ! parsed . fileIsDir , "stat: file should not be directory" ) ;
125+ assert ( parsed . fileSize > 0 , "stat: file size should be > 0" ) ;
126+ assert ( parsed . dirIsDir , "stat: dir should be directory" ) ;
127+ }
82128
83- if ( readResult . code !== 0 ) {
84- throw new Error ( `Read step failed: ${ readResult . errorMessage } ` ) ;
129+ // --- exists ---
130+ {
131+ const r = await exec ( `
132+ const fs = require("node:fs");
133+ console.log(JSON.stringify({
134+ yes: fs.existsSync("/workspace/hello.txt"),
135+ no: fs.existsSync("/nonexistent-file.xyz"),
136+ }));
137+ ` ) ;
138+ assert ( r . code === 0 , `exists exec failed: ${ r . stderr } ` ) ;
139+ const parsed = JSON . parse ( r . stdout . trim ( ) ) ;
140+ assert ( parsed . yes === true , "existsSync true for existing file" ) ;
141+ assert ( parsed . no === false , "existsSync false for missing file" ) ;
85142 }
86143
87- // Verify from host side via S3 API
88- const hostContent = await filesystem . readTextFile ( "/workspace/hello.txt" ) ;
89- const parsed = JSON . parse ( readOutput . trim ( ) ) ;
144+ // --- rename file ---
145+ {
146+ const r = await exec ( `
147+ const fs = require("node:fs");
148+ fs.writeFileSync("/workspace/old.txt", "rename-me");
149+ fs.renameSync("/workspace/old.txt", "/workspace/new.txt");
150+ const content = fs.readFileSync("/workspace/new.txt", "utf8");
151+ const oldExists = fs.existsSync("/workspace/old.txt");
152+ console.log(JSON.stringify({ content, oldExists }));
153+ ` ) ;
154+ assert ( r . code === 0 , `rename exec failed: ${ r . stderr } ` ) ;
155+ const parsed = JSON . parse ( r . stdout . trim ( ) ) ;
156+ assert ( parsed . content === "rename-me" , "rename: content mismatch" ) ;
157+ assert ( ! parsed . oldExists , "rename: old path should not exist" ) ;
158+ }
90159
91- const ok =
92- hostContent === "hello from sandbox via S3" &&
93- parsed . text === "hello from sandbox via S3" &&
94- parsed . count === 42 &&
95- parsed . entries . includes ( "hello.txt" ) &&
96- parsed . entries . includes ( "data.json" ) ;
160+ // --- rename directory with children ---
161+ {
162+ const r = await exec ( `
163+ const fs = require("node:fs");
164+ fs.mkdirSync("/workspace/dirA/sub", { recursive: true });
165+ fs.writeFileSync("/workspace/dirA/sub/file.txt", "child-content");
166+ fs.renameSync("/workspace/dirA", "/workspace/dirB");
167+ const content = fs.readFileSync("/workspace/dirB/sub/file.txt", "utf8");
168+ const oldExists = fs.existsSync("/workspace/dirA");
169+ console.log(JSON.stringify({ content, oldExists }));
170+ ` ) ;
171+ assert ( r . code === 0 , `rename dir exec failed: ${ r . stderr } ` ) ;
172+ const parsed = JSON . parse ( r . stdout . trim ( ) ) ;
173+ assert (
174+ parsed . content === "child-content" ,
175+ "rename dir: child content lost" ,
176+ ) ;
177+ assert ( ! parsed . oldExists , "rename dir: old path should not exist" ) ;
178+ }
97179
180+ // --- removeFile ---
181+ {
182+ const r = await exec ( `
183+ const fs = require("node:fs");
184+ fs.writeFileSync("/workspace/delete-me.txt", "gone");
185+ fs.unlinkSync("/workspace/delete-me.txt");
186+ console.log(JSON.stringify({ exists: fs.existsSync("/workspace/delete-me.txt") }));
187+ ` ) ;
188+ assert ( r . code === 0 , `removeFile exec failed: ${ r . stderr } ` ) ;
189+ assert (
190+ JSON . parse ( r . stdout . trim ( ) ) . exists === false ,
191+ "removeFile: file should be gone" ,
192+ ) ;
193+ }
194+
195+ // --- removeDir ---
196+ {
197+ const r = await exec ( `
198+ const fs = require("node:fs");
199+ fs.mkdirSync("/workspace/empty-dir");
200+ fs.rmdirSync("/workspace/empty-dir");
201+ console.log(JSON.stringify({ exists: fs.existsSync("/workspace/empty-dir") }));
202+ ` ) ;
203+ assert ( r . code === 0 , `removeDir exec failed: ${ r . stderr } ` ) ;
204+ assert (
205+ JSON . parse ( r . stdout . trim ( ) ) . exists === false ,
206+ "removeDir: dir should be gone" ,
207+ ) ;
208+ }
209+
210+ // --- truncate ---
211+ {
212+ const r = await exec ( `
213+ const fs = require("node:fs");
214+ fs.writeFileSync("/workspace/trunc.txt", "hello world");
215+ fs.truncateSync("/workspace/trunc.txt", 5);
216+ const content = fs.readFileSync("/workspace/trunc.txt", "utf8");
217+ console.log(JSON.stringify({ content }));
218+ ` ) ;
219+ assert ( r . code === 0 , `truncate exec failed: ${ r . stderr } ` ) ;
220+ assert (
221+ JSON . parse ( r . stdout . trim ( ) ) . content === "hello" ,
222+ "truncate: should be 'hello'" ,
223+ ) ;
224+ }
225+
226+ // --- error: symlink (should throw ENOSYS) ---
227+ {
228+ const r = await exec ( `
229+ const fs = require("node:fs");
230+ try { fs.symlinkSync("/workspace/hello.txt", "/workspace/link"); console.log("NO_ERROR"); }
231+ catch (e) { console.log(e.message.includes("ENOSYS") ? "ENOSYS" : e.message); }
232+ ` ) ;
233+ assert ( r . code === 0 , `symlink error exec failed: ${ r . stderr } ` ) ;
234+ assert (
235+ r . stdout . trim ( ) === "ENOSYS" ,
236+ "symlink should throw ENOSYS on S3" ,
237+ ) ;
238+ }
239+
240+ // --- error: read nonexistent ---
241+ {
242+ const r = await exec ( `
243+ const fs = require("node:fs");
244+ try { fs.readFileSync("/nonexistent"); console.log("NO_ERROR"); }
245+ catch (e) { console.log(e.message.includes("ENOENT") ? "ENOENT" : e.message); }
246+ ` ) ;
247+ assert ( r . code === 0 , `error read exec failed: ${ r . stderr } ` ) ;
248+ assert (
249+ r . stdout . trim ( ) === "ENOENT" ,
250+ "read missing file should throw ENOENT" ,
251+ ) ;
252+ }
253+
254+ // --- error: rmdir non-empty ---
255+ {
256+ const r = await exec ( `
257+ const fs = require("node:fs");
258+ try { fs.rmdirSync("/workspace"); console.log("NO_ERROR"); }
259+ catch (e) { console.log(e.message.includes("ENOTEMPTY") || e.message.includes("not empty") ? "ENOTEMPTY" : e.message); }
260+ ` ) ;
261+ assert ( r . code === 0 , `error rmdir exec failed: ${ r . stderr } ` ) ;
262+ assert (
263+ r . stdout . trim ( ) === "ENOTEMPTY" ,
264+ "rmdir non-empty should throw ENOTEMPTY" ,
265+ ) ;
266+ }
267+
268+ // --- result ---
269+ const ok = failures . length === 0 ;
98270 console . log (
99271 JSON . stringify ( {
100272 ok,
101- hostContent,
102- sandboxResult : parsed ,
103- summary :
104- "S3-backed VFS: sandbox wrote files to MinIO, read them back, host verified via S3 API" ,
273+ passed : 12 - failures . length ,
274+ total : 12 ,
275+ failures,
276+ summary : ok
277+ ? "S3 VFS: 12/12 tests passed (write, read, stat, exists, rename, rename-dir, remove, rmdir, truncate, err-symlink, err-read, err-rmdir)"
278+ : `${ failures . length } of 12 tests failed` ,
105279 } ) ,
106280 ) ;
281+
282+ if ( ! ok ) process . exit ( 1 ) ;
107283} finally {
108284 runtime . dispose ( ) ;
109285
110- // Clean up: delete all objects and the bucket
111- const list = await client . send (
112- new ListObjectsV2Command ( { Bucket : BUCKET } ) ,
113- ) ;
114- for ( const obj of list . Contents ?? [ ] ) {
115- await client . send (
116- new DeleteObjectCommand ( { Bucket : BUCKET , Key : obj . Key ! } ) ,
286+ // Clean up: paginated delete of all objects, then bucket
287+ let token : string | undefined ;
288+ do {
289+ const list = await client . send (
290+ new ListObjectsV2Command ( {
291+ Bucket : BUCKET ,
292+ ContinuationToken : token ,
293+ } ) ,
117294 ) ;
118- }
295+ for ( const obj of list . Contents ?? [ ] ) {
296+ await client . send (
297+ new DeleteObjectCommand ( { Bucket : BUCKET , Key : obj . Key ! } ) ,
298+ ) ;
299+ }
300+ token = list . NextContinuationToken ;
301+ } while ( token ) ;
119302 await client . send ( new DeleteBucketCommand ( { Bucket : BUCKET } ) ) ;
120303}
0 commit comments