Skip to content

Commit 47f7845

Browse files
committed
fix: sort symlinked rules files alphabetically
- Add alphabetical sorting to readTextFilesFromDirectory function - Sort by basename of filename (case-insensitive) for consistent order - Fixes issue where symlinked rules were read in random order - Add test case to verify alphabetical sorting behavior Fixes #4131
1 parent a6e16e8 commit 47f7845

File tree

2 files changed

+66
-1
lines changed

2 files changed

+66
-1
lines changed

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,63 @@ describe("Rules directory reading", () => {
10331033
expect(result).toContain("content of file3")
10341034
})
10351035

1036+
it("should return files in alphabetical order by filename", async () => {
1037+
// Simulate .roo/rules directory exists
1038+
statMock.mockResolvedValueOnce({
1039+
isDirectory: vi.fn().mockReturnValue(true),
1040+
} as any)
1041+
1042+
// Simulate listing files in non-alphabetical order to test sorting
1043+
readdirMock.mockResolvedValueOnce([
1044+
{ name: "zebra.txt", isFile: () => true, parentPath: "/fake/path/.roo/rules" },
1045+
{ name: "alpha.txt", isFile: () => true, parentPath: "/fake/path/.roo/rules" },
1046+
{ name: "Beta.txt", isFile: () => true, parentPath: "/fake/path/.roo/rules" }, // Test case-insensitive sorting
1047+
] as any)
1048+
1049+
statMock.mockImplementation((path) => {
1050+
return Promise.resolve({
1051+
isFile: vi.fn().mockReturnValue(true),
1052+
}) as any
1053+
})
1054+
1055+
readFileMock.mockImplementation((filePath: PathLike) => {
1056+
const pathStr = filePath.toString()
1057+
const normalizedPath = pathStr.replace(/\\/g, "/")
1058+
if (normalizedPath === "/fake/path/.roo/rules/zebra.txt") {
1059+
return Promise.resolve("zebra content")
1060+
}
1061+
if (normalizedPath === "/fake/path/.roo/rules/alpha.txt") {
1062+
return Promise.resolve("alpha content")
1063+
}
1064+
if (normalizedPath === "/fake/path/.roo/rules/Beta.txt") {
1065+
return Promise.resolve("beta content")
1066+
}
1067+
return Promise.reject({ code: "ENOENT" })
1068+
})
1069+
1070+
const result = await loadRuleFiles("/fake/path")
1071+
1072+
// Files should appear in alphabetical order: alpha.txt, Beta.txt, zebra.txt
1073+
const alphaIndex = result.indexOf("alpha content")
1074+
const betaIndex = result.indexOf("beta content")
1075+
const zebraIndex = result.indexOf("zebra content")
1076+
1077+
expect(alphaIndex).toBeLessThan(betaIndex)
1078+
expect(betaIndex).toBeLessThan(zebraIndex)
1079+
1080+
// Verify the expected file paths are in the result
1081+
const expectedAlphaPath =
1082+
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\alpha.txt" : "/fake/path/.roo/rules/alpha.txt"
1083+
const expectedBetaPath =
1084+
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\Beta.txt" : "/fake/path/.roo/rules/Beta.txt"
1085+
const expectedZebraPath =
1086+
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\zebra.txt" : "/fake/path/.roo/rules/zebra.txt"
1087+
1088+
expect(result).toContain(`# Rules from ${expectedAlphaPath}:`)
1089+
expect(result).toContain(`# Rules from ${expectedBetaPath}:`)
1090+
expect(result).toContain(`# Rules from ${expectedZebraPath}:`)
1091+
})
1092+
10361093
it("should handle empty file list gracefully", async () => {
10371094
// Simulate .roo/rules directory exists
10381095
statMock.mockResolvedValueOnce({

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,15 @@ async function readTextFilesFromDirectory(dirPath: string): Promise<Array<{ file
138138
)
139139

140140
// Filter out null values (directories, failed reads, or excluded files)
141-
return fileContents.filter((item): item is { filename: string; content: string } => item !== null)
141+
const filteredFiles = fileContents.filter((item): item is { filename: string; content: string } => item !== null)
142+
143+
// Sort files alphabetically by filename (case-insensitive) to ensure consistent order
144+
// This fixes the issue where symlinked files were read in random order
145+
return filteredFiles.sort((a, b) => {
146+
const filenameA = path.basename(a.filename).toLowerCase()
147+
const filenameB = path.basename(b.filename).toLowerCase()
148+
return filenameA.localeCompare(filenameB)
149+
})
142150
} catch (err) {
143151
return []
144152
}

0 commit comments

Comments
 (0)