@@ -999,6 +999,81 @@ export function defineVfsConformanceTests(config: VfsConformanceConfig): void {
999999 const text = await fs . readTextFile ( path ) ;
10001000 expect ( text ) . toBe ( "deep" ) ;
10011001 } ) ;
1002+
1003+ test . skipIf ( ! capabilities . pwrite ) (
1004+ "pwrite on nonexistent file throws ENOENT" ,
1005+ async ( ) => {
1006+ const err = await fs
1007+ . pwrite ( "/no-such-file.txt" , 0 , new TextEncoder ( ) . encode ( "x" ) )
1008+ . catch ( ( e ) => e ) ;
1009+ expectErrorCode ( err , "ENOENT" ) ;
1010+ } ,
1011+ ) ;
1012+
1013+ test . skipIf ( ! capabilities . truncate ) (
1014+ "truncate on nonexistent file throws ENOENT" ,
1015+ async ( ) => {
1016+ const err = await fs . truncate ( "/no-such-file.txt" , 0 ) . catch ( ( e ) => e ) ;
1017+ expectErrorCode ( err , "ENOENT" ) ;
1018+ } ,
1019+ ) ;
1020+
1021+ test ( "stat on root directory '/' returns isDirectory: true" , async ( ) => {
1022+ const s = await fs . stat ( "/" ) ;
1023+ expect ( s . isDirectory ) . toBe ( true ) ;
1024+ } ) ;
1025+ } ) ;
1026+
1027+ // ---------------------------------------------------------------
1028+ // Relative symlink tests (gated)
1029+ // ---------------------------------------------------------------
1030+
1031+ describe . skipIf ( ! capabilities . symlinks ) ( "relative symlinks" , ( ) => {
1032+ test ( "relative symlink resolution" , async ( ) => {
1033+ // Create /dir/real.txt, then /dir/link.txt -> real.txt (relative)
1034+ await fs . writeFile ( "/dir/real.txt" , "real content" ) ;
1035+ await fs . symlink ( "real.txt" , "/dir/link.txt" ) ;
1036+ const text = await fs . readTextFile ( "/dir/link.txt" ) ;
1037+ expect ( text ) . toBe ( "real content" ) ;
1038+ } ) ;
1039+
1040+ test ( "symlink-to-directory traversal" , async ( ) => {
1041+ // Create /real-dir/file.txt, then /a -> /real-dir
1042+ // Reading /a/file.txt should resolve to /real-dir/file.txt
1043+ await fs . writeFile ( "/real-dir/file.txt" , "traversed" ) ;
1044+ await fs . symlink ( "/real-dir" , "/a" ) ;
1045+ const text = await fs . readTextFile ( "/a/file.txt" ) ;
1046+ expect ( text ) . toBe ( "traversed" ) ;
1047+ } ) ;
1048+ } ) ;
1049+
1050+ // ---------------------------------------------------------------
1051+ // Concurrent rename + readFile (gated on pwrite for concurrency)
1052+ // ---------------------------------------------------------------
1053+
1054+ describe . skipIf ( ! capabilities . pwrite ) ( "concurrent rename + readFile" , ( ) => {
1055+ test ( "concurrent rename + readFile does not crash or corrupt" , async ( ) => {
1056+ await fs . writeFile ( "/conc-rename.txt" , "data before rename" ) ;
1057+ // Run rename and readFile concurrently. Neither should crash.
1058+ // readFile may see the file at old or new path depending on ordering.
1059+ const results = await Promise . allSettled ( [
1060+ fs . rename ( "/conc-rename.txt" , "/conc-renamed.txt" ) ,
1061+ fs . readFile ( "/conc-rename.txt" ) ,
1062+ ] ) ;
1063+ // Rename should succeed.
1064+ expect ( results [ 0 ] ! . status ) . toBe ( "fulfilled" ) ;
1065+ // readFile may succeed (read before rename) or fail with ENOENT (read after rename).
1066+ if ( results [ 1 ] ! . status === "fulfilled" ) {
1067+ const data = ( results [ 1 ] as PromiseFulfilledResult < Uint8Array > ) . value ;
1068+ expect ( new TextDecoder ( ) . decode ( data ) ) . toBe ( "data before rename" ) ;
1069+ } else {
1070+ const err = ( results [ 1 ] as PromiseRejectedResult ) . reason ;
1071+ expectErrorCode ( err , "ENOENT" ) ;
1072+ }
1073+ // The file should exist at the new path.
1074+ const text = await fs . readTextFile ( "/conc-renamed.txt" ) ;
1075+ expect ( text ) . toBe ( "data before rename" ) ;
1076+ } ) ;
10021077 } ) ;
10031078 } ) ;
10041079}
0 commit comments