1
- import { describe , it , expect , vi } from "vitest"
1
+ import { vi , describe , it , expect , beforeEach } from "vitest"
2
+ import * as path from "path"
2
3
import { listFiles } from "../list-files"
4
+ import * as childProcess from "child_process"
3
5
4
6
vi . mock ( "../list-files" , async ( ) => {
5
7
const actual = await vi . importActual ( "../list-files" )
@@ -16,3 +18,201 @@ describe("listFiles", () => {
16
18
expect ( result ) . toEqual ( [ [ ] , false ] )
17
19
} )
18
20
} )
21
+
22
+ // Mock ripgrep to avoid filesystem dependencies
23
+ vi . mock ( "../../ripgrep" , ( ) => ( {
24
+ getBinPath : vi . fn ( ) . mockResolvedValue ( "/mock/path/to/rg" ) ,
25
+ } ) )
26
+
27
+ // Mock vscode
28
+ vi . mock ( "vscode" , ( ) => ( {
29
+ env : {
30
+ appRoot : "/mock/app/root" ,
31
+ } ,
32
+ } ) )
33
+
34
+ // Mock filesystem operations
35
+ vi . mock ( "fs" , ( ) => ( {
36
+ promises : {
37
+ access : vi . fn ( ) . mockRejectedValue ( new Error ( "Not found" ) ) ,
38
+ readFile : vi . fn ( ) . mockResolvedValue ( "" ) ,
39
+ readdir : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
40
+ } ,
41
+ } ) )
42
+
43
+ // Import fs to set up mocks
44
+ import * as fs from "fs"
45
+
46
+ vi . mock ( "child_process" , ( ) => ( {
47
+ spawn : vi . fn ( ) ,
48
+ } ) )
49
+
50
+ vi . mock ( "../../path" , ( ) => ( {
51
+ arePathsEqual : vi . fn ( ) . mockReturnValue ( false ) ,
52
+ } ) )
53
+
54
+ describe ( "list-files symlink support" , ( ) => {
55
+ beforeEach ( ( ) => {
56
+ vi . clearAllMocks ( )
57
+ } )
58
+
59
+ it ( "should include --follow flag in ripgrep arguments" , async ( ) => {
60
+ const mockSpawn = vi . mocked ( childProcess . spawn )
61
+ const mockProcess = {
62
+ stdout : {
63
+ on : vi . fn ( ( event , callback ) => {
64
+ if ( event === "data" ) {
65
+ // Simulate some output to complete the process
66
+ setTimeout ( ( ) => callback ( "test-file.txt\n" ) , 10 )
67
+ }
68
+ } ) ,
69
+ } ,
70
+ stderr : {
71
+ on : vi . fn ( ) ,
72
+ } ,
73
+ on : vi . fn ( ( event , callback ) => {
74
+ if ( event === "close" ) {
75
+ setTimeout ( ( ) => callback ( 0 ) , 20 )
76
+ }
77
+ if ( event === "error" ) {
78
+ // No error simulation
79
+ }
80
+ } ) ,
81
+ kill : vi . fn ( ) ,
82
+ }
83
+
84
+ mockSpawn . mockReturnValue ( mockProcess as any )
85
+
86
+ // Call listFiles to trigger ripgrep execution
87
+ await listFiles ( "/test/dir" , false , 100 )
88
+
89
+ // Verify that spawn was called with --follow flag (the critical fix)
90
+ const [ rgPath , args ] = mockSpawn . mock . calls [ 0 ]
91
+ expect ( rgPath ) . toBe ( "/mock/path/to/rg" )
92
+ expect ( args ) . toContain ( "--files" )
93
+ expect ( args ) . toContain ( "--hidden" )
94
+ expect ( args ) . toContain ( "--follow" ) // This is the critical assertion - the fix should add this flag
95
+
96
+ // Platform-agnostic path check - verify the last argument is the resolved path
97
+ const expectedPath = path . resolve ( "/test/dir" )
98
+ expect ( args [ args . length - 1 ] ) . toBe ( expectedPath )
99
+ } )
100
+
101
+ it ( "should include --follow flag for recursive listings too" , async ( ) => {
102
+ const mockSpawn = vi . mocked ( childProcess . spawn )
103
+ const mockProcess = {
104
+ stdout : {
105
+ on : vi . fn ( ( event , callback ) => {
106
+ if ( event === "data" ) {
107
+ setTimeout ( ( ) => callback ( "test-file.txt\n" ) , 10 )
108
+ }
109
+ } ) ,
110
+ } ,
111
+ stderr : {
112
+ on : vi . fn ( ) ,
113
+ } ,
114
+ on : vi . fn ( ( event , callback ) => {
115
+ if ( event === "close" ) {
116
+ setTimeout ( ( ) => callback ( 0 ) , 20 )
117
+ }
118
+ if ( event === "error" ) {
119
+ // No error simulation
120
+ }
121
+ } ) ,
122
+ kill : vi . fn ( ) ,
123
+ }
124
+
125
+ mockSpawn . mockReturnValue ( mockProcess as any )
126
+
127
+ // Call listFiles with recursive=true
128
+ await listFiles ( "/test/dir" , true , 100 )
129
+
130
+ // Verify that spawn was called with --follow flag (the critical fix)
131
+ const [ rgPath , args ] = mockSpawn . mock . calls [ 0 ]
132
+ expect ( rgPath ) . toBe ( "/mock/path/to/rg" )
133
+ expect ( args ) . toContain ( "--files" )
134
+ expect ( args ) . toContain ( "--hidden" )
135
+ expect ( args ) . toContain ( "--follow" ) // This should be present in recursive mode too
136
+
137
+ // Platform-agnostic path check - verify the last argument is the resolved path
138
+ const expectedPath = path . resolve ( "/test/dir" )
139
+ expect ( args [ args . length - 1 ] ) . toBe ( expectedPath )
140
+ } )
141
+
142
+ it ( "should ensure first-level directories are included when limit is reached" , async ( ) => {
143
+ // Mock fs.promises.readdir to simulate a directory structure
144
+ const mockReaddir = vi . mocked ( fs . promises . readdir )
145
+
146
+ // Root directory with first-level directories
147
+ mockReaddir . mockResolvedValueOnce ( [
148
+ { name : "a_dir" , isDirectory : ( ) => true , isSymbolicLink : ( ) => false , isFile : ( ) => false } as any ,
149
+ { name : "b_dir" , isDirectory : ( ) => true , isSymbolicLink : ( ) => false , isFile : ( ) => false } as any ,
150
+ { name : "c_dir" , isDirectory : ( ) => true , isSymbolicLink : ( ) => false , isFile : ( ) => false } as any ,
151
+ { name : "file1.txt" , isDirectory : ( ) => false , isSymbolicLink : ( ) => false , isFile : ( ) => true } as any ,
152
+ { name : "file2.txt" , isDirectory : ( ) => false , isSymbolicLink : ( ) => false , isFile : ( ) => true } as any ,
153
+ ] )
154
+
155
+ // Mock ripgrep to return many files (simulating hitting the limit)
156
+ const mockSpawn = vi . mocked ( childProcess . spawn )
157
+ const mockProcess = {
158
+ stdout : {
159
+ on : vi . fn ( ( event , callback ) => {
160
+ if ( event === "data" ) {
161
+ // Return many file paths to trigger the limit
162
+ const paths =
163
+ [
164
+ "/test/dir/a_dir/" ,
165
+ "/test/dir/a_dir/subdir1/" ,
166
+ "/test/dir/a_dir/subdir1/file1.txt" ,
167
+ "/test/dir/a_dir/subdir1/file2.txt" ,
168
+ "/test/dir/a_dir/subdir2/" ,
169
+ "/test/dir/a_dir/subdir2/file3.txt" ,
170
+ "/test/dir/a_dir/file4.txt" ,
171
+ "/test/dir/a_dir/file5.txt" ,
172
+ "/test/dir/file1.txt" ,
173
+ "/test/dir/file2.txt" ,
174
+ // Note: b_dir and c_dir are missing from ripgrep output
175
+ ] . join ( "\n" ) + "\n"
176
+ setTimeout ( ( ) => callback ( paths ) , 10 )
177
+ }
178
+ } ) ,
179
+ } ,
180
+ stderr : {
181
+ on : vi . fn ( ) ,
182
+ } ,
183
+ on : vi . fn ( ( event , callback ) => {
184
+ if ( event === "close" ) {
185
+ setTimeout ( ( ) => callback ( 0 ) , 20 )
186
+ }
187
+ } ) ,
188
+ kill : vi . fn ( ) ,
189
+ }
190
+ mockSpawn . mockReturnValue ( mockProcess as any )
191
+
192
+ // Mock fs.promises.access to simulate .gitignore doesn't exist
193
+ vi . mocked ( fs . promises . access ) . mockRejectedValue ( new Error ( "File not found" ) )
194
+
195
+ // Call listFiles with recursive=true and a small limit
196
+ const [ results , limitReached ] = await listFiles ( "/test/dir" , true , 10 )
197
+
198
+ // Verify that we got results and hit the limit
199
+ expect ( results . length ) . toBe ( 10 )
200
+ expect ( limitReached ) . toBe ( true )
201
+
202
+ // Count directories in results
203
+ const directories = results . filter ( ( r ) => r . endsWith ( "/" ) )
204
+
205
+ // We should have at least the 3 first-level directories
206
+ // even if ripgrep didn't return all of them
207
+ expect ( directories . length ) . toBeGreaterThanOrEqual ( 3 )
208
+
209
+ // Verify all first-level directories are included
210
+ const hasADir = results . some ( ( r ) => r . endsWith ( "a_dir/" ) )
211
+ const hasBDir = results . some ( ( r ) => r . endsWith ( "b_dir/" ) )
212
+ const hasCDir = results . some ( ( r ) => r . endsWith ( "c_dir/" ) )
213
+
214
+ expect ( hasADir ) . toBe ( true )
215
+ expect ( hasBDir ) . toBe ( true )
216
+ expect ( hasCDir ) . toBe ( true )
217
+ } )
218
+ } )
0 commit comments