@@ -1033,6 +1033,157 @@ describe("Rules directory reading", () => {
1033
1033
expect ( result ) . toContain ( "content of file3" )
1034
1034
} )
1035
1035
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
+
1036
1187
it ( "should handle empty file list gracefully" , async ( ) => {
1037
1188
// Simulate .roo/rules directory exists
1038
1189
statMock . mockResolvedValueOnce ( {
0 commit comments