33import { join } from "path"
44import fs from "fs/promises"
55import { fileExistsAtPath } from "../../../utils/fs"
6- import { getExcludePatterns } from "../excludes"
6+ import { getExcludePatterns , getExcludePatternsWithStats } from "../excludes"
7+ import { executeRipgrep } from "../../search/file-search"
78
89// Mock fs/promises
910vi . mock ( "fs/promises" , ( ) => ( {
1011 default : {
1112 readFile : vi . fn ( ) ,
13+ stat : vi . fn ( ) ,
1214 } ,
1315} ) )
1416
@@ -17,6 +19,12 @@ vi.mock("../../../utils/fs", () => ({
1719 fileExistsAtPath : vi . fn ( ) ,
1820} ) )
1921
22+ // Mock executeRipgrep
23+ vi . mock ( "../../search/file-search" , ( ) => ( {
24+ executeRipgrep : vi . fn ( ) ,
25+ executeRipgrepForFiles : vi . fn ( ) ,
26+ } ) )
27+
2028describe ( "getExcludePatterns" , ( ) => {
2129 const testWorkspacePath = "/test/workspace"
2230
@@ -152,4 +160,108 @@ readme.md text
152160 expect ( excludePatterns ) . toContain ( "*.log" ) // log
153161 } )
154162 } )
163+
164+ describe ( "getLargeFileAutoExcludePatterns with LFS pre-filtering" , ( ) => {
165+ it ( "should pre-filter git-lfs patterns when scanning for large files" , async ( ) => {
166+ // Mock .gitattributes file exists with LFS patterns
167+ vi . mocked ( fileExistsAtPath ) . mockResolvedValue ( true )
168+ const gitAttributesContent = `*.psd filter=lfs diff=lfs merge=lfs -text
169+ *.zip filter=lfs diff=lfs merge=lfs -text
170+ *.mp4 filter=lfs diff=lfs merge=lfs -text
171+ `
172+ vi . mocked ( fs . readFile ) . mockResolvedValue ( gitAttributesContent )
173+
174+ // Mock executeRipgrep to return some files
175+ vi . mocked ( executeRipgrep ) . mockResolvedValue ( [
176+ { path : "file1.txt" , type : "file" , label : "file1.txt" } ,
177+ { path : "large.bin" , type : "file" , label : "large.bin" } ,
178+ { path : "code.js" , type : "file" , label : "code.js" } ,
179+ ] )
180+
181+ // Mock file stats
182+ vi . mocked ( fs . stat ) . mockImplementation ( async ( path ) => {
183+ const pathStr = path . toString ( )
184+ if ( pathStr . includes ( "large.bin" ) ) {
185+ return { size : 20 * 1024 * 1024 } as any // 20MB
186+ }
187+ return { size : 1024 } as any // 1KB
188+ } )
189+
190+ // Get exclude patterns with stats
191+ const result = await getExcludePatternsWithStats ( testWorkspacePath )
192+
193+ // Verify executeRipgrep was called with LFS patterns as exclusions
194+ expect ( executeRipgrep ) . toHaveBeenCalledWith (
195+ expect . objectContaining ( {
196+ args : expect . arrayContaining ( [ "-g" , "!*.psd" , "-g" , "!*.zip" , "-g" , "!*.mp4" ] ) ,
197+ workspacePath : testWorkspacePath ,
198+ } ) ,
199+ )
200+
201+ // Verify large.bin was detected and included
202+ expect ( result . stats . largeFilesExcluded ) . toBe ( 1 )
203+ expect ( result . stats . sample ) . toContain ( "large.bin" )
204+ } )
205+
206+ it ( "should handle empty LFS patterns gracefully" , async ( ) => {
207+ // Mock no .gitattributes file
208+ vi . mocked ( fileExistsAtPath ) . mockResolvedValue ( false )
209+
210+ // Mock executeRipgrep to return some files
211+ vi . mocked ( executeRipgrep ) . mockResolvedValue ( [
212+ { path : "file1.txt" , type : "file" , label : "file1.txt" } ,
213+ { path : "large.bin" , type : "file" , label : "large.bin" } ,
214+ ] )
215+
216+ // Mock file stats
217+ vi . mocked ( fs . stat ) . mockImplementation ( async ( path ) => {
218+ const pathStr = path . toString ( )
219+ if ( pathStr . includes ( "large.bin" ) ) {
220+ return { size : 20 * 1024 * 1024 } as any // 20MB
221+ }
222+ return { size : 1024 } as any // 1KB
223+ } )
224+
225+ // Get exclude patterns with stats
226+ const result = await getExcludePatternsWithStats ( testWorkspacePath )
227+
228+ // Verify executeRipgrep was called without LFS patterns
229+ expect ( executeRipgrep ) . toHaveBeenCalledWith (
230+ expect . objectContaining ( {
231+ args : expect . not . arrayContaining ( [ "-g" , "!*.psd" , "-g" , "!*.zip" , "-g" , "!*.mp4" ] ) ,
232+ workspacePath : testWorkspacePath ,
233+ } ) ,
234+ )
235+
236+ // Verify large file was still detected
237+ expect ( result . stats . largeFilesExcluded ) . toBe ( 1 )
238+ expect ( result . stats . sample ) . toContain ( "large.bin" )
239+ } )
240+
241+ it ( "should not exclude code files even if they are large" , async ( ) => {
242+ // Mock no .gitattributes file
243+ vi . mocked ( fileExistsAtPath ) . mockResolvedValue ( false )
244+
245+ // Mock executeRipgrep to return some files including large code files
246+ vi . mocked ( executeRipgrep ) . mockResolvedValue ( [
247+ { path : "huge.js" , type : "file" , label : "huge.js" } ,
248+ { path : "large.bin" , type : "file" , label : "large.bin" } ,
249+ { path : "big.ts" , type : "file" , label : "big.ts" } ,
250+ ] )
251+
252+ // Mock file stats - all files are large
253+ vi . mocked ( fs . stat ) . mockImplementation ( async ( ) => {
254+ return { size : 20 * 1024 * 1024 } as any // 20MB
255+ } )
256+
257+ // Get exclude patterns with stats
258+ const result = await getExcludePatternsWithStats ( testWorkspacePath )
259+
260+ // Verify only non-code file was excluded
261+ expect ( result . stats . largeFilesExcluded ) . toBe ( 1 )
262+ expect ( result . stats . sample ) . toContain ( "large.bin" )
263+ expect ( result . stats . sample ) . not . toContain ( "huge.js" )
264+ expect ( result . stats . sample ) . not . toContain ( "big.ts" )
265+ } )
266+ } )
155267} )
0 commit comments