From 6687320f2177c1eddf110028add893404af14708 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 18 Jul 2025 13:24:55 +0000 Subject: [PATCH] fix: ignore dot files in .roo/rules/ directory to prevent Vim swap file crashes - Add check to exclude files starting with "." in shouldIncludeRuleFile function - Fixes issue where Vim .swp files (e.g., .filename.swp) crash rules loading - Add comprehensive test case for Vim swap files and other dot files - Resolves #4317 --- .../__tests__/custom-instructions.spec.ts | 78 +++++++++++++++++++ .../prompts/sections/custom-instructions.ts | 5 ++ 2 files changed, 83 insertions(+) diff --git a/src/core/prompts/sections/__tests__/custom-instructions.spec.ts b/src/core/prompts/sections/__tests__/custom-instructions.spec.ts index 9c8e003143..c12e3937b9 100644 --- a/src/core/prompts/sections/__tests__/custom-instructions.spec.ts +++ b/src/core/prompts/sections/__tests__/custom-instructions.spec.ts @@ -321,6 +321,84 @@ describe("loadRuleFiles", () => { } }) + it("should filter out Vim swap files and other dot files from .roo/rules/ directory", async () => { + // Simulate .roo/rules directory exists + statMock.mockResolvedValueOnce({ + isDirectory: vi.fn().mockReturnValue(true), + } as any) + + // Simulate listing files including Vim swap files and other dot files + readdirMock.mockResolvedValueOnce([ + { name: "rule1.txt", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" }, + { name: ".01-prettier-tree-sitter.md.swp", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" }, + { name: ".vimrc.swp", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" }, + { name: ".hidden-file", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" }, + { name: "rule2.md", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" }, + { name: ".gitignore", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" }, + ] as any) + + statMock.mockImplementation((path) => { + return Promise.resolve({ + isFile: vi.fn().mockReturnValue(true), + }) as any + }) + + readFileMock.mockImplementation((filePath: PathLike) => { + const pathStr = filePath.toString() + const normalizedPath = pathStr.replace(/\\/g, "/") + + // Only rule files should be read - dot files should be skipped + if (normalizedPath === "/fake/path/.roo/rules/rule1.txt") { + return Promise.resolve("rule 1 content") + } + if (normalizedPath === "/fake/path/.roo/rules/rule2.md") { + return Promise.resolve("rule 2 content") + } + + // Dot files should not be read due to filtering + // If they somehow are read, return recognizable content + if (normalizedPath === "/fake/path/.roo/rules/.01-prettier-tree-sitter.md.swp") { + return Promise.resolve("b0VIM 8.2") + } + if (normalizedPath === "/fake/path/.roo/rules/.vimrc.swp") { + return Promise.resolve("VIM_SWAP_CONTENT") + } + if (normalizedPath === "/fake/path/.roo/rules/.hidden-file") { + return Promise.resolve("HIDDEN_FILE_CONTENT") + } + if (normalizedPath === "/fake/path/.roo/rules/.gitignore") { + return Promise.resolve("GITIGNORE_CONTENT") + } + + return Promise.reject({ code: "ENOENT" }) + }) + + const result = await loadRuleFiles("/fake/path") + + // Should contain rule files + expect(result).toContain("rule 1 content") + expect(result).toContain("rule 2 content") + + // Should NOT contain dot file content - they should be filtered out + expect(result).not.toContain("b0VIM 8.2") + expect(result).not.toContain("VIM_SWAP_CONTENT") + expect(result).not.toContain("HIDDEN_FILE_CONTENT") + expect(result).not.toContain("GITIGNORE_CONTENT") + + // Verify dot files are not read at all + const expectedDotFiles = [ + "/fake/path/.roo/rules/.01-prettier-tree-sitter.md.swp", + "/fake/path/.roo/rules/.vimrc.swp", + "/fake/path/.roo/rules/.hidden-file", + "/fake/path/.roo/rules/.gitignore", + ] + + for (const dotFile of expectedDotFiles) { + const expectedPath = process.platform === "win32" ? dotFile.replace(/\//g, "\\") : dotFile + expect(readFileMock).not.toHaveBeenCalledWith(expectedPath, "utf-8") + } + }) + it("should fall back to .roorules when .roo/rules/ is empty", async () => { // Simulate .roo/rules directory exists statMock.mockResolvedValueOnce({ diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index 3c8558a57f..e621c4c5f4 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -309,6 +309,11 @@ ${joinedSections}` function shouldIncludeRuleFile(filename: string): boolean { const basename = path.basename(filename) + // Exclude files that start with . (hidden files, including Vim .swp files) + if (basename.startsWith(".")) { + return false + } + const cachePatterns = [ "*.DS_Store", "*.bak",