@@ -321,6 +321,84 @@ describe("loadRuleFiles", () => {
321321 }
322322 } )
323323
324+ it ( "should filter out Vim swap files and other dot files from .roo/rules/ directory" , async ( ) => {
325+ // Simulate .roo/rules directory exists
326+ statMock . mockResolvedValueOnce ( {
327+ isDirectory : vi . fn ( ) . mockReturnValue ( true ) ,
328+ } as any )
329+
330+ // Simulate listing files including Vim swap files and other dot files
331+ readdirMock . mockResolvedValueOnce ( [
332+ { name : "rule1.txt" , isFile : ( ) => true , isSymbolicLink : ( ) => false , parentPath : "/fake/path/.roo/rules" } ,
333+ { name : ".01-prettier-tree-sitter.md.swp" , isFile : ( ) => true , isSymbolicLink : ( ) => false , parentPath : "/fake/path/.roo/rules" } ,
334+ { name : ".vimrc.swp" , isFile : ( ) => true , isSymbolicLink : ( ) => false , parentPath : "/fake/path/.roo/rules" } ,
335+ { name : ".hidden-file" , isFile : ( ) => true , isSymbolicLink : ( ) => false , parentPath : "/fake/path/.roo/rules" } ,
336+ { name : "rule2.md" , isFile : ( ) => true , isSymbolicLink : ( ) => false , parentPath : "/fake/path/.roo/rules" } ,
337+ { name : ".gitignore" , isFile : ( ) => true , isSymbolicLink : ( ) => false , parentPath : "/fake/path/.roo/rules" } ,
338+ ] as any )
339+
340+ statMock . mockImplementation ( ( path ) => {
341+ return Promise . resolve ( {
342+ isFile : vi . fn ( ) . mockReturnValue ( true ) ,
343+ } ) as any
344+ } )
345+
346+ readFileMock . mockImplementation ( ( filePath : PathLike ) => {
347+ const pathStr = filePath . toString ( )
348+ const normalizedPath = pathStr . replace ( / \\ / g, "/" )
349+
350+ // Only rule files should be read - dot files should be skipped
351+ if ( normalizedPath === "/fake/path/.roo/rules/rule1.txt" ) {
352+ return Promise . resolve ( "rule 1 content" )
353+ }
354+ if ( normalizedPath === "/fake/path/.roo/rules/rule2.md" ) {
355+ return Promise . resolve ( "rule 2 content" )
356+ }
357+
358+ // Dot files should not be read due to filtering
359+ // If they somehow are read, return recognizable content
360+ if ( normalizedPath === "/fake/path/.roo/rules/.01-prettier-tree-sitter.md.swp" ) {
361+ return Promise . resolve ( "b0VIM 8.2" )
362+ }
363+ if ( normalizedPath === "/fake/path/.roo/rules/.vimrc.swp" ) {
364+ return Promise . resolve ( "VIM_SWAP_CONTENT" )
365+ }
366+ if ( normalizedPath === "/fake/path/.roo/rules/.hidden-file" ) {
367+ return Promise . resolve ( "HIDDEN_FILE_CONTENT" )
368+ }
369+ if ( normalizedPath === "/fake/path/.roo/rules/.gitignore" ) {
370+ return Promise . resolve ( "GITIGNORE_CONTENT" )
371+ }
372+
373+ return Promise . reject ( { code : "ENOENT" } )
374+ } )
375+
376+ const result = await loadRuleFiles ( "/fake/path" )
377+
378+ // Should contain rule files
379+ expect ( result ) . toContain ( "rule 1 content" )
380+ expect ( result ) . toContain ( "rule 2 content" )
381+
382+ // Should NOT contain dot file content - they should be filtered out
383+ expect ( result ) . not . toContain ( "b0VIM 8.2" )
384+ expect ( result ) . not . toContain ( "VIM_SWAP_CONTENT" )
385+ expect ( result ) . not . toContain ( "HIDDEN_FILE_CONTENT" )
386+ expect ( result ) . not . toContain ( "GITIGNORE_CONTENT" )
387+
388+ // Verify dot files are not read at all
389+ const expectedDotFiles = [
390+ "/fake/path/.roo/rules/.01-prettier-tree-sitter.md.swp" ,
391+ "/fake/path/.roo/rules/.vimrc.swp" ,
392+ "/fake/path/.roo/rules/.hidden-file" ,
393+ "/fake/path/.roo/rules/.gitignore" ,
394+ ]
395+
396+ for ( const dotFile of expectedDotFiles ) {
397+ const expectedPath = process . platform === "win32" ? dotFile . replace ( / \/ / g, "\\" ) : dotFile
398+ expect ( readFileMock ) . not . toHaveBeenCalledWith ( expectedPath , "utf-8" )
399+ }
400+ } )
401+
324402 it ( "should fall back to .roorules when .roo/rules/ is empty" , async ( ) => {
325403 // Simulate .roo/rules directory exists
326404 statMock . mockResolvedValueOnce ( {
0 commit comments