@@ -18,6 +18,14 @@ import { MultiSearchReplaceDiffStrategy } from "../../diff/strategies/multi-sear
1818import { MultiFileSearchReplaceDiffStrategy } from "../../diff/strategies/multi-file-search-replace"
1919import { EXPERIMENT_IDS } from "../../../shared/experiments"
2020
21+ // Mock delay before any imports that might use it
22+ vi . mock ( "delay" , ( ) => ( {
23+ __esModule : true ,
24+ default : vi . fn ( ) . mockResolvedValue ( undefined ) ,
25+ } ) )
26+
27+ import delay from "delay"
28+
2129vi . mock ( "execa" , ( ) => ( {
2230 execa : vi . fn ( ) ,
2331} ) )
@@ -869,6 +877,353 @@ describe("Cline", () => {
869877 } )
870878 } )
871879
880+ describe ( "Subtask Rate Limiting" , ( ) => {
881+ let mockProvider : any
882+ let mockApiConfig : any
883+ let mockDelay : ReturnType < typeof vi . fn >
884+
885+ beforeEach ( ( ) => {
886+ vi . clearAllMocks ( )
887+ // Reset the global timestamp before each test
888+ Task . resetGlobalApiRequestTime ( )
889+
890+ mockApiConfig = {
891+ apiProvider : "anthropic" ,
892+ apiKey : "test-key" ,
893+ rateLimitSeconds : 5 ,
894+ }
895+
896+ mockProvider = {
897+ context : {
898+ globalStorageUri : { fsPath : "/test/storage" } ,
899+ } ,
900+ getState : vi . fn ( ) . mockResolvedValue ( {
901+ apiConfiguration : mockApiConfig ,
902+ } ) ,
903+ say : vi . fn ( ) ,
904+ postStateToWebview : vi . fn ( ) . mockResolvedValue ( undefined ) ,
905+ postMessageToWebview : vi . fn ( ) . mockResolvedValue ( undefined ) ,
906+ updateTaskHistory : vi . fn ( ) . mockResolvedValue ( undefined ) ,
907+ }
908+
909+ // Get the mocked delay function
910+ mockDelay = delay as ReturnType < typeof vi . fn >
911+ mockDelay . mockClear ( )
912+ } )
913+
914+ afterEach ( ( ) => {
915+ // Clean up the global state after each test
916+ Task . resetGlobalApiRequestTime ( )
917+ } )
918+
919+ it ( "should enforce rate limiting across parent and subtask" , async ( ) => {
920+ // Add a spy to track getState calls
921+ const getStateSpy = vi . spyOn ( mockProvider , "getState" )
922+
923+ // Create parent task
924+ const parent = new Task ( {
925+ provider : mockProvider ,
926+ apiConfiguration : mockApiConfig ,
927+ task : "parent task" ,
928+ startTask : false ,
929+ } )
930+
931+ // Mock the API stream response
932+ const mockStream = {
933+ async * [ Symbol . asyncIterator ] ( ) {
934+ yield { type : "text" , text : "parent response" }
935+ } ,
936+ async next ( ) {
937+ return { done : true , value : { type : "text" , text : "parent response" } }
938+ } ,
939+ async return ( ) {
940+ return { done : true , value : undefined }
941+ } ,
942+ async throw ( e : any ) {
943+ throw e
944+ } ,
945+ [ Symbol . asyncDispose ] : async ( ) => { } ,
946+ } as AsyncGenerator < ApiStreamChunk >
947+
948+ vi . spyOn ( parent . api , "createMessage" ) . mockReturnValue ( mockStream )
949+
950+ // Make an API request with the parent task
951+ const parentIterator = parent . attemptApiRequest ( 0 )
952+ await parentIterator . next ( )
953+
954+ // Verify no delay was applied for the first request
955+ expect ( mockDelay ) . not . toHaveBeenCalled ( )
956+
957+ // Create a subtask immediately after
958+ const child = new Task ( {
959+ provider : mockProvider ,
960+ apiConfiguration : mockApiConfig ,
961+ task : "child task" ,
962+ parentTask : parent ,
963+ rootTask : parent ,
964+ startTask : false ,
965+ } )
966+
967+ // Mock the child's API stream
968+ const childMockStream = {
969+ async * [ Symbol . asyncIterator ] ( ) {
970+ yield { type : "text" , text : "child response" }
971+ } ,
972+ async next ( ) {
973+ return { done : true , value : { type : "text" , text : "child response" } }
974+ } ,
975+ async return ( ) {
976+ return { done : true , value : undefined }
977+ } ,
978+ async throw ( e : any ) {
979+ throw e
980+ } ,
981+ [ Symbol . asyncDispose ] : async ( ) => { } ,
982+ } as AsyncGenerator < ApiStreamChunk >
983+
984+ vi . spyOn ( child . api , "createMessage" ) . mockReturnValue ( childMockStream )
985+
986+ // Make an API request with the child task
987+ const childIterator = child . attemptApiRequest ( 0 )
988+ await childIterator . next ( )
989+
990+ // Verify rate limiting was applied
991+ expect ( mockDelay ) . toHaveBeenCalledTimes ( mockApiConfig . rateLimitSeconds )
992+ expect ( mockDelay ) . toHaveBeenCalledWith ( 1000 )
993+ } , 10000 ) // Increase timeout to 10 seconds
994+
995+ it ( "should not apply rate limiting if enough time has passed" , async ( ) => {
996+ // Create parent task
997+ const parent = new Task ( {
998+ provider : mockProvider ,
999+ apiConfiguration : mockApiConfig ,
1000+ task : "parent task" ,
1001+ startTask : false ,
1002+ } )
1003+
1004+ // Mock the API stream response
1005+ const mockStream = {
1006+ async * [ Symbol . asyncIterator ] ( ) {
1007+ yield { type : "text" , text : "response" }
1008+ } ,
1009+ async next ( ) {
1010+ return { done : true , value : { type : "text" , text : "response" } }
1011+ } ,
1012+ async return ( ) {
1013+ return { done : true , value : undefined }
1014+ } ,
1015+ async throw ( e : any ) {
1016+ throw e
1017+ } ,
1018+ [ Symbol . asyncDispose ] : async ( ) => { } ,
1019+ } as AsyncGenerator < ApiStreamChunk >
1020+
1021+ vi . spyOn ( parent . api , "createMessage" ) . mockReturnValue ( mockStream )
1022+
1023+ // Make an API request with the parent task
1024+ const parentIterator = parent . attemptApiRequest ( 0 )
1025+ await parentIterator . next ( )
1026+
1027+ // Simulate time passing (more than rate limit)
1028+ const originalDateNow = Date . now
1029+ const mockTime = Date . now ( ) + ( mockApiConfig . rateLimitSeconds + 1 ) * 1000
1030+ Date . now = vi . fn ( ( ) => mockTime )
1031+
1032+ // Create a subtask after time has passed
1033+ const child = new Task ( {
1034+ provider : mockProvider ,
1035+ apiConfiguration : mockApiConfig ,
1036+ task : "child task" ,
1037+ parentTask : parent ,
1038+ rootTask : parent ,
1039+ startTask : false ,
1040+ } )
1041+
1042+ vi . spyOn ( child . api , "createMessage" ) . mockReturnValue ( mockStream )
1043+
1044+ // Make an API request with the child task
1045+ const childIterator = child . attemptApiRequest ( 0 )
1046+ await childIterator . next ( )
1047+
1048+ // Verify no rate limiting was applied
1049+ expect ( mockDelay ) . not . toHaveBeenCalled ( )
1050+
1051+ // Restore Date.now
1052+ Date . now = originalDateNow
1053+ } )
1054+
1055+ it ( "should share rate limiting across multiple subtasks" , async ( ) => {
1056+ // Create parent task
1057+ const parent = new Task ( {
1058+ provider : mockProvider ,
1059+ apiConfiguration : mockApiConfig ,
1060+ task : "parent task" ,
1061+ startTask : false ,
1062+ } )
1063+
1064+ // Mock the API stream response
1065+ const mockStream = {
1066+ async * [ Symbol . asyncIterator ] ( ) {
1067+ yield { type : "text" , text : "response" }
1068+ } ,
1069+ async next ( ) {
1070+ return { done : true , value : { type : "text" , text : "response" } }
1071+ } ,
1072+ async return ( ) {
1073+ return { done : true , value : undefined }
1074+ } ,
1075+ async throw ( e : any ) {
1076+ throw e
1077+ } ,
1078+ [ Symbol . asyncDispose ] : async ( ) => { } ,
1079+ } as AsyncGenerator < ApiStreamChunk >
1080+
1081+ vi . spyOn ( parent . api , "createMessage" ) . mockReturnValue ( mockStream )
1082+
1083+ // Make an API request with the parent task
1084+ const parentIterator = parent . attemptApiRequest ( 0 )
1085+ await parentIterator . next ( )
1086+
1087+ // Create first subtask
1088+ const child1 = new Task ( {
1089+ provider : mockProvider ,
1090+ apiConfiguration : mockApiConfig ,
1091+ task : "child task 1" ,
1092+ parentTask : parent ,
1093+ rootTask : parent ,
1094+ startTask : false ,
1095+ } )
1096+
1097+ vi . spyOn ( child1 . api , "createMessage" ) . mockReturnValue ( mockStream )
1098+
1099+ // Make an API request with the first child task
1100+ const child1Iterator = child1 . attemptApiRequest ( 0 )
1101+ await child1Iterator . next ( )
1102+
1103+ // Verify rate limiting was applied
1104+ const firstDelayCount = mockDelay . mock . calls . length
1105+ expect ( firstDelayCount ) . toBe ( mockApiConfig . rateLimitSeconds )
1106+
1107+ // Clear the mock to count new delays
1108+ mockDelay . mockClear ( )
1109+
1110+ // Create second subtask immediately after
1111+ const child2 = new Task ( {
1112+ provider : mockProvider ,
1113+ apiConfiguration : mockApiConfig ,
1114+ task : "child task 2" ,
1115+ parentTask : parent ,
1116+ rootTask : parent ,
1117+ startTask : false ,
1118+ } )
1119+
1120+ vi . spyOn ( child2 . api , "createMessage" ) . mockReturnValue ( mockStream )
1121+
1122+ // Make an API request with the second child task
1123+ const child2Iterator = child2 . attemptApiRequest ( 0 )
1124+ await child2Iterator . next ( )
1125+
1126+ // Verify rate limiting was applied again
1127+ expect ( mockDelay ) . toHaveBeenCalledTimes ( mockApiConfig . rateLimitSeconds )
1128+ } , 15000 ) // Increase timeout to 15 seconds
1129+
1130+ it ( "should handle rate limiting with zero rate limit" , async ( ) => {
1131+ // Update config to have zero rate limit
1132+ mockApiConfig . rateLimitSeconds = 0
1133+ mockProvider . getState . mockResolvedValue ( {
1134+ apiConfiguration : mockApiConfig ,
1135+ } )
1136+
1137+ // Create parent task
1138+ const parent = new Task ( {
1139+ provider : mockProvider ,
1140+ apiConfiguration : mockApiConfig ,
1141+ task : "parent task" ,
1142+ startTask : false ,
1143+ } )
1144+
1145+ // Mock the API stream response
1146+ const mockStream = {
1147+ async * [ Symbol . asyncIterator ] ( ) {
1148+ yield { type : "text" , text : "response" }
1149+ } ,
1150+ async next ( ) {
1151+ return { done : true , value : { type : "text" , text : "response" } }
1152+ } ,
1153+ async return ( ) {
1154+ return { done : true , value : undefined }
1155+ } ,
1156+ async throw ( e : any ) {
1157+ throw e
1158+ } ,
1159+ [ Symbol . asyncDispose ] : async ( ) => { } ,
1160+ } as AsyncGenerator < ApiStreamChunk >
1161+
1162+ vi . spyOn ( parent . api , "createMessage" ) . mockReturnValue ( mockStream )
1163+
1164+ // Make an API request with the parent task
1165+ const parentIterator = parent . attemptApiRequest ( 0 )
1166+ await parentIterator . next ( )
1167+
1168+ // Create a subtask
1169+ const child = new Task ( {
1170+ provider : mockProvider ,
1171+ apiConfiguration : mockApiConfig ,
1172+ task : "child task" ,
1173+ parentTask : parent ,
1174+ rootTask : parent ,
1175+ startTask : false ,
1176+ } )
1177+
1178+ vi . spyOn ( child . api , "createMessage" ) . mockReturnValue ( mockStream )
1179+
1180+ // Make an API request with the child task
1181+ const childIterator = child . attemptApiRequest ( 0 )
1182+ await childIterator . next ( )
1183+
1184+ // Verify no delay was applied
1185+ expect ( mockDelay ) . not . toHaveBeenCalled ( )
1186+ } )
1187+
1188+ it ( "should update global timestamp even when no rate limiting is needed" , async ( ) => {
1189+ // Create task
1190+ const task = new Task ( {
1191+ provider : mockProvider ,
1192+ apiConfiguration : mockApiConfig ,
1193+ task : "test task" ,
1194+ startTask : false ,
1195+ } )
1196+
1197+ // Mock the API stream response
1198+ const mockStream = {
1199+ async * [ Symbol . asyncIterator ] ( ) {
1200+ yield { type : "text" , text : "response" }
1201+ } ,
1202+ async next ( ) {
1203+ return { done : true , value : { type : "text" , text : "response" } }
1204+ } ,
1205+ async return ( ) {
1206+ return { done : true , value : undefined }
1207+ } ,
1208+ async throw ( e : any ) {
1209+ throw e
1210+ } ,
1211+ [ Symbol . asyncDispose ] : async ( ) => { } ,
1212+ } as AsyncGenerator < ApiStreamChunk >
1213+
1214+ vi . spyOn ( task . api , "createMessage" ) . mockReturnValue ( mockStream )
1215+
1216+ // Make an API request
1217+ const iterator = task . attemptApiRequest ( 0 )
1218+ await iterator . next ( )
1219+
1220+ // Access the private static property via reflection for testing
1221+ const globalTimestamp = ( Task as any ) . lastGlobalApiRequestTime
1222+ expect ( globalTimestamp ) . toBeDefined ( )
1223+ expect ( globalTimestamp ) . toBeGreaterThan ( 0 )
1224+ } )
1225+ } )
1226+
8721227 describe ( "Dynamic Strategy Selection" , ( ) => {
8731228 let mockProvider : any
8741229 let mockApiConfig : any
0 commit comments