Skip to content

Commit 6687320

Browse files
committed
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
1 parent 38d8edf commit 6687320

File tree

2 files changed

+83
-0
lines changed

2 files changed

+83
-0
lines changed

src/core/prompts/sections/__tests__/custom-instructions.spec.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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({

src/core/prompts/sections/custom-instructions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,11 @@ ${joinedSections}`
309309
function shouldIncludeRuleFile(filename: string): boolean {
310310
const basename = path.basename(filename)
311311

312+
// Exclude files that start with . (hidden files, including Vim .swp files)
313+
if (basename.startsWith(".")) {
314+
return false
315+
}
316+
312317
const cachePatterns = [
313318
"*.DS_Store",
314319
"*.bak",

0 commit comments

Comments
 (0)