@@ -15,15 +15,13 @@ describe("Local Folder Cleanup Safety", () => {
1515 let sourceFile : string ;
1616
1717 beforeEach ( async ( ) => {
18- // Create test directories
1918 testDir = path . join ( tmpdir ( ) , `agentic-safety-test-${ Date . now ( ) } ` ) ;
2019 sourceDir = path . join ( testDir , "source" ) ;
2120 targetDir = path . join ( testDir , "target" ) ;
2221
2322 await fs . mkdir ( sourceDir , { recursive : true } ) ;
2423 await fs . mkdir ( targetDir , { recursive : true } ) ;
2524
26- // Create a source directory with actual files
2725 const actualSourceFolder = path . join ( sourceDir , "src" ) ;
2826 await fs . mkdir ( actualSourceFolder , { recursive : true } ) ;
2927 sourceFile = path . join ( actualSourceFolder , "important-file.js" ) ;
@@ -35,34 +33,26 @@ describe("Local Folder Cleanup Safety", () => {
3533 } ) ;
3634
3735 it ( "CRITICAL: should NOT delete source files when removing symlinks" , async ( ) => {
38- // Create symlink to source directory
3936 await createSymlinks ( [ "src" ] , targetDir , sourceDir ) ;
4037
41- // Verify symlink was created
42- const symlinkPath = path . join ( targetDir , "src" ) ;
43- const linkStat = await fs . lstat ( symlinkPath ) ;
44- expect ( linkStat . isSymbolicLink ( ) ) . toBe ( true ) ;
45-
46- // Verify we can access the source file through the symlink
47- const fileViaSymlink = path . join ( symlinkPath , "important-file.js" ) ;
48- const content = await fs . readFile ( fileViaSymlink , "utf-8" ) ;
49- expect ( content ) . toBe ( "CRITICAL DATA - DO NOT DELETE" ) ;
38+ const symlinkPath = path . join ( targetDir , "important-file.js" ) ;
39+ expect ( ( await fs . lstat ( symlinkPath ) ) . isSymbolicLink ( ) ) . toBe ( true ) ;
40+ expect ( await fs . readFile ( symlinkPath , "utf-8" ) ) . toBe (
41+ "CRITICAL DATA - DO NOT DELETE" ,
42+ ) ;
5043
51- // Remove symlinks
5244 await removeSymlinks ( targetDir ) ;
5345
54- // CRITICAL: Source file must still exist!
46+ // CRITICAL: removing the symlink must never touch the source
5547 const stillExists = await fs
5648 . access ( sourceFile )
5749 . then ( ( ) => true )
5850 . catch ( ( ) => false ) ;
5951 expect ( stillExists ) . toBe ( true ) ;
52+ expect ( await fs . readFile ( sourceFile , "utf-8" ) ) . toBe (
53+ "CRITICAL DATA - DO NOT DELETE" ,
54+ ) ;
6055
61- // Verify content is unchanged
62- const originalContent = await fs . readFile ( sourceFile , "utf-8" ) ;
63- expect ( originalContent ) . toBe ( "CRITICAL DATA - DO NOT DELETE" ) ;
64-
65- // Symlink should be gone
6656 const symlinkGone = await fs
6757 . lstat ( symlinkPath )
6858 . then ( ( ) => false )
@@ -71,107 +61,88 @@ describe("Local Folder Cleanup Safety", () => {
7161 } ) ;
7262
7363 it ( "CRITICAL: should NOT delete source files when clearing target directory" , async ( ) => {
74- // Create symlink
7564 await createSymlinks ( [ "src" ] , targetDir , sourceDir ) ;
7665
77- // Verify source file exists
78- expect ( await fs . readFile ( sourceFile , "utf-8" ) ) . toBe (
79- "CRITICAL DATA - DO NOT DELETE" ,
80- ) ;
81-
82- // Simulate clearing target directory (what --force does)
83- // This is the DANGEROUS operation we need to test
66+ // Simulate --force: delete the whole docset directory
8467 await fs . rm ( targetDir , { recursive : true , force : true } ) ;
8568
86- // CRITICAL: Source file must STILL exist after removing target!
69+ // CRITICAL: fs.rm must not follow symlinks into the source
8770 const stillExists = await fs
8871 . access ( sourceFile )
8972 . then ( ( ) => true )
9073 . catch ( ( ) => false ) ;
91-
9274 expect ( stillExists ) . toBe (
9375 true ,
9476 "CRITICAL FAILURE: Source file was deleted!" ,
9577 ) ;
9678
9779 if ( stillExists ) {
98- const content = await fs . readFile ( sourceFile , "utf-8" ) ;
99- expect ( content ) . toBe ( "CRITICAL DATA - DO NOT DELETE" ) ;
80+ expect ( await fs . readFile ( sourceFile , "utf-8" ) ) . toBe (
81+ "CRITICAL DATA - DO NOT DELETE" ,
82+ ) ;
10083 }
10184 } ) ;
10285
10386 it ( "CRITICAL: should handle nested symlinks safely" , async ( ) => {
104- // Create nested structure in source
10587 const nestedDir = path . join ( sourceDir , "src" , "nested" ) ;
10688 await fs . mkdir ( nestedDir , { recursive : true } ) ;
10789 const nestedFile = path . join ( nestedDir , "nested-file.js" ) ;
10890 await fs . writeFile ( nestedFile , "NESTED CRITICAL DATA" ) ;
10991
110- // Create symlink
11192 await createSymlinks ( [ "src" ] , targetDir , sourceDir ) ;
112-
113- // Clear target directory
11493 await fs . rm ( targetDir , { recursive : true , force : true } ) ;
11594
116- // CRITICAL: All source files must still exist
117- const sourceExists = await fs
118- . access ( sourceFile )
119- . then ( ( ) => true )
120- . catch ( ( ) => false ) ;
121- const nestedExists = await fs
122- . access ( nestedFile )
123- . then ( ( ) => true )
124- . catch ( ( ) => false ) ;
125-
126- expect ( sourceExists ) . toBe ( true , "Source file was deleted!" ) ;
127- expect ( nestedExists ) . toBe ( true , "Nested source file was deleted!" ) ;
95+ // CRITICAL: All source files must survive target removal
96+ expect (
97+ await fs
98+ . access ( sourceFile )
99+ . then ( ( ) => true )
100+ . catch ( ( ) => false ) ,
101+ ) . toBe ( true , "Source file was deleted!" ) ;
102+ expect (
103+ await fs
104+ . access ( nestedFile )
105+ . then ( ( ) => true )
106+ . catch ( ( ) => false ) ,
107+ ) . toBe ( true , "Nested source file was deleted!" ) ;
128108 } ) ;
129109
130110 it ( "should safely handle mixed content (symlinks and regular files)" , async ( ) => {
131- // Create symlink
132111 await createSymlinks ( [ "src" ] , targetDir , sourceDir ) ;
133112
134- // Add a regular file to target directory
135113 const regularFile = path . join ( targetDir , "regular-file.txt" ) ;
136114 await fs . writeFile ( regularFile , "This can be deleted" ) ;
137115
138- // Clear target directory
139116 await fs . rm ( targetDir , { recursive : true , force : true } ) ;
140117
141- // Source file must still exist
142- const sourceExists = await fs
143- . access ( sourceFile )
144- . then ( ( ) => true )
145- . catch ( ( ) => false ) ;
146- expect ( sourceExists ) . toBe ( true ) ;
147-
148- // Target directory should be gone
149- const targetExists = await fs
150- . access ( targetDir )
151- . then ( ( ) => true )
152- . catch ( ( ) => false ) ;
153- expect ( targetExists ) . toBe ( false ) ;
118+ expect (
119+ await fs
120+ . access ( sourceFile )
121+ . then ( ( ) => true )
122+ . catch ( ( ) => false ) ,
123+ ) . toBe ( true ) ;
124+ expect (
125+ await fs
126+ . access ( targetDir )
127+ . then ( ( ) => true )
128+ . catch ( ( ) => false ) ,
129+ ) . toBe ( false ) ;
154130 } ) ;
155131
156132 it ( "should document Node.js symlink behavior" , async ( ) => {
157- // This test documents how Node.js handles symlinks with fs.rm
158- // According to Node.js docs, fs.rm should NOT follow symlinks
159-
133+ // fs.rm with recursive:true must NOT follow symlinks — this test pins that contract.
160134 await createSymlinks ( [ "src" ] , targetDir , sourceDir ) ;
161- const symlinkPath = path . join ( targetDir , "src " ) ;
135+ const symlinkPath = path . join ( targetDir , "important-file.js " ) ;
162136
163- // Verify it's a symlink
164- const stats = await fs . lstat ( symlinkPath ) ;
165- expect ( stats . isSymbolicLink ( ) ) . toBe ( true ) ;
137+ expect ( ( await fs . lstat ( symlinkPath ) ) . isSymbolicLink ( ) ) . toBe ( true ) ;
166138
167- // Remove just the symlink using fs.unlink
168139 await fs . unlink ( symlinkPath ) ;
169140
170- // Source should still exist
171- const sourceExists = await fs
172- . access ( sourceFile )
173- . then ( ( ) => true )
174- . catch ( ( ) => false ) ;
175- expect ( sourceExists ) . toBe ( true ) ;
141+ expect (
142+ await fs
143+ . access ( sourceFile )
144+ . then ( ( ) => true )
145+ . catch ( ( ) => false ) ,
146+ ) . toBe ( true ) ;
176147 } ) ;
177148} ) ;
0 commit comments