Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions src/core/prompts/sections/__tests__/custom-instructions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,106 @@ describe("loadRuleFiles", () => {
expect(readFileMock).toHaveBeenCalledWith(expectedFile2Path, "utf-8")
})

it("should filter out cache files from .roo/rules/ directory", async () => {
// Simulate .roo/rules directory exists
statMock.mockResolvedValueOnce({
isDirectory: vi.fn().mockReturnValue(true),
} as any)

// Simulate listing files including cache files
readdirMock.mockResolvedValueOnce([
{ name: "rule1.txt", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" },
{ name: ".DS_Store", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" },
{ name: "Thumbs.db", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" },
{ name: "rule2.md", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" },
{ name: "cache.log", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" },
{
name: "backup.bak",
isFile: () => true,
isSymbolicLink: () => false,
parentPath: "/fake/path/.roo/rules",
},
{ name: "temp.tmp", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" },
{
name: "script.pyc",
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 - cache 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")
}

// Cache files should not be read due to filtering
// If they somehow are read, return recognizable content
if (normalizedPath === "/fake/path/.roo/rules/.DS_Store") {
return Promise.resolve("DS_STORE_BINARY_CONTENT")
}
if (normalizedPath === "/fake/path/.roo/rules/Thumbs.db") {
return Promise.resolve("THUMBS_DB_CONTENT")
}
if (normalizedPath === "/fake/path/.roo/rules/backup.bak") {
return Promise.resolve("BACKUP_CONTENT")
}
if (normalizedPath === "/fake/path/.roo/rules/cache.log") {
return Promise.resolve("LOG_CONTENT")
}
if (normalizedPath === "/fake/path/.roo/rules/temp.tmp") {
return Promise.resolve("TEMP_CONTENT")
}
if (normalizedPath === "/fake/path/.roo/rules/script.pyc") {
return Promise.resolve("PYTHON_BYTECODE")
}

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 cache file content - they should be filtered out
expect(result).not.toContain("DS_STORE_BINARY_CONTENT")
expect(result).not.toContain("THUMBS_DB_CONTENT")
expect(result).not.toContain("BACKUP_CONTENT")
expect(result).not.toContain("LOG_CONTENT")
expect(result).not.toContain("TEMP_CONTENT")
expect(result).not.toContain("PYTHON_BYTECODE")

// Verify cache files are not read at all
const expectedCacheFiles = [
"/fake/path/.roo/rules/.DS_Store",
"/fake/path/.roo/rules/Thumbs.db",
"/fake/path/.roo/rules/backup.bak",
"/fake/path/.roo/rules/cache.log",
"/fake/path/.roo/rules/temp.tmp",
"/fake/path/.roo/rules/script.pyc",
]

for (const cacheFile of expectedCacheFiles) {
const expectedPath = process.platform === "win32" ? cacheFile.replace(/\//g, "\\") : cacheFile
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({
Expand Down
47 changes: 46 additions & 1 deletion src/core/prompts/sections/custom-instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ async function readTextFilesFromDirectory(dirPath: string): Promise<Array<{ file
// Check if it's a file (not a directory)
const stats = await fs.stat(file)
if (stats.isFile()) {
// Filter out cache files and system files that shouldn't be in rules
if (!shouldIncludeRuleFile(file)) {
return null
}
const content = await safeReadFile(file)
return { filename: file, content }
}
Expand All @@ -133,7 +137,7 @@ async function readTextFilesFromDirectory(dirPath: string): Promise<Array<{ file
}),
)

// Filter out null values (directories or failed reads)
// Filter out null values (directories, failed reads, or excluded files)
return fileContents.filter((item): item is { filename: string; content: string } => item !== null)
} catch (err) {
return []
Expand Down Expand Up @@ -297,3 +301,44 @@ The following additional instructions are provided by the user, and should be fo
${joinedSections}`
: ""
}

/**
* Check if a file should be included in rule compilation.
* Excludes cache files and system files that shouldn't be processed as rules.
*/
function shouldIncludeRuleFile(filename: string): boolean {
const basename = path.basename(filename)

const cachePatterns = [
"*.DS_Store",
"*.bak",
"*.cache",
"*.crdownload",
"*.db",
"*.dmp",
"*.dump",
"*.eslintcache",
"*.lock",
"*.log",
"*.old",
"*.part",
"*.partial",
"*.pyc",
"*.pyo",
"*.stackdump",
"*.swo",
"*.swp",
"*.temp",
"*.tmp",
"Thumbs.db",
]

return !cachePatterns.some((pattern) => {
if (pattern.startsWith("*.")) {
const extension = pattern.slice(1)
return basename.endsWith(extension)
} else {
return basename === pattern
}
})
}
Loading