@@ -1033,6 +1033,157 @@ 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+
1093+ it ( "should sort symlinks by their symlink names, not target names" , async ( ) => {
1094+ // Reset mocks
1095+ statMock . mockReset ( )
1096+ readdirMock . mockReset ( )
1097+ readlinkMock . mockReset ( )
1098+ readFileMock . mockReset ( )
1099+
1100+ // First call: check if .roo/rules directory exists
1101+ statMock . mockResolvedValueOnce ( {
1102+ isDirectory : vi . fn ( ) . mockReturnValue ( true ) ,
1103+ } as any )
1104+
1105+ // Simulate listing files with symlinks that point to files with different names
1106+ readdirMock . mockResolvedValueOnce ( [
1107+ {
1108+ name : "01-first.link" ,
1109+ isFile : ( ) => false ,
1110+ isSymbolicLink : ( ) => true ,
1111+ parentPath : "/fake/path/.roo/rules" ,
1112+ } ,
1113+ {
1114+ name : "02-second.link" ,
1115+ isFile : ( ) => false ,
1116+ isSymbolicLink : ( ) => true ,
1117+ parentPath : "/fake/path/.roo/rules" ,
1118+ } ,
1119+ {
1120+ name : "03-third.link" ,
1121+ isFile : ( ) => false ,
1122+ isSymbolicLink : ( ) => true ,
1123+ parentPath : "/fake/path/.roo/rules" ,
1124+ } ,
1125+ ] as any )
1126+
1127+ // Mock readlink to return target paths that would sort differently than symlink names
1128+ readlinkMock
1129+ . mockResolvedValueOnce ( "../../targets/zzz-last.txt" ) // 01-first.link -> zzz-last.txt
1130+ . mockResolvedValueOnce ( "../../targets/aaa-first.txt" ) // 02-second.link -> aaa-first.txt
1131+ . mockResolvedValueOnce ( "../../targets/mmm-middle.txt" ) // 03-third.link -> mmm-middle.txt
1132+
1133+ // Set up stat mock for the remaining calls
1134+ statMock . mockImplementation ( ( path ) => {
1135+ const normalizedPath = path . toString ( ) . replace ( / \\ / g, "/" )
1136+ // Target files exist and are files
1137+ if ( normalizedPath . endsWith ( ".txt" ) ) {
1138+ return Promise . resolve ( {
1139+ isFile : vi . fn ( ) . mockReturnValue ( true ) ,
1140+ isDirectory : vi . fn ( ) . mockReturnValue ( false ) ,
1141+ } as any )
1142+ }
1143+ return Promise . resolve ( {
1144+ isFile : vi . fn ( ) . mockReturnValue ( false ) ,
1145+ isDirectory : vi . fn ( ) . mockReturnValue ( false ) ,
1146+ } as any )
1147+ } )
1148+
1149+ readFileMock . mockImplementation ( ( filePath : PathLike ) => {
1150+ const pathStr = filePath . toString ( )
1151+ const normalizedPath = pathStr . replace ( / \\ / g, "/" )
1152+ if ( normalizedPath . endsWith ( "zzz-last.txt" ) ) {
1153+ return Promise . resolve ( "content from zzz-last.txt" )
1154+ }
1155+ if ( normalizedPath . endsWith ( "aaa-first.txt" ) ) {
1156+ return Promise . resolve ( "content from aaa-first.txt" )
1157+ }
1158+ if ( normalizedPath . endsWith ( "mmm-middle.txt" ) ) {
1159+ return Promise . resolve ( "content from mmm-middle.txt" )
1160+ }
1161+ return Promise . reject ( { code : "ENOENT" } )
1162+ } )
1163+
1164+ const result = await loadRuleFiles ( "/fake/path" )
1165+
1166+ // Content should appear in order of symlink names (01-first, 02-second, 03-third)
1167+ // NOT in order of target names (aaa-first, mmm-middle, zzz-last)
1168+ const firstIndex = result . indexOf ( "content from zzz-last.txt" ) // from 01-first.link
1169+ const secondIndex = result . indexOf ( "content from aaa-first.txt" ) // from 02-second.link
1170+ const thirdIndex = result . indexOf ( "content from mmm-middle.txt" ) // from 03-third.link
1171+
1172+ // All content should be found
1173+ expect ( firstIndex ) . toBeGreaterThan ( - 1 )
1174+ expect ( secondIndex ) . toBeGreaterThan ( - 1 )
1175+ expect ( thirdIndex ) . toBeGreaterThan ( - 1 )
1176+
1177+ // And they should be in the order of symlink names, not target names
1178+ expect ( firstIndex ) . toBeLessThan ( secondIndex )
1179+ expect ( secondIndex ) . toBeLessThan ( thirdIndex )
1180+
1181+ // Verify the target paths are shown (not symlink paths)
1182+ expect ( result ) . toContain ( "zzz-last.txt" )
1183+ expect ( result ) . toContain ( "aaa-first.txt" )
1184+ expect ( result ) . toContain ( "mmm-middle.txt" )
1185+ } )
1186+
10361187 it ( "should handle empty file list gracefully" , async ( ) => {
10371188 // Simulate .roo/rules directory exists
10381189 statMock . mockResolvedValueOnce ( {
0 commit comments