github-issue-tower-defence-management 1.58.1 → 1.58.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +3 -3
  3. package/bin/adapter/entry-points/cli/index.js +1 -3
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -3
  6. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  7. package/bin/adapter/proxy/RateLimitCache.js +94 -3
  8. package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
  9. package/bin/adapter/proxy/proxyEntry.js +19 -0
  10. package/bin/adapter/proxy/proxyEntry.js.map +1 -1
  11. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +33 -2
  12. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -1
  13. package/bin/domain/usecases/StartPreparationUseCase.js +34 -35
  14. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  15. package/package.json +1 -1
  16. package/src/adapter/entry-points/cli/index.ts +0 -3
  17. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +0 -3
  18. package/src/adapter/proxy/RateLimitCache.test.ts +190 -0
  19. package/src/adapter/proxy/RateLimitCache.ts +104 -2
  20. package/src/adapter/proxy/proxyEntry.ts +24 -1
  21. package/src/adapter/repositories/GraphqlProjectRepository.test.ts +2 -1
  22. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +354 -7
  23. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +42 -2
  24. package/src/domain/entities/ClaudeTokenUsage.ts +7 -0
  25. package/src/domain/usecases/StartPreparationUseCase.test.ts +1212 -887
  26. package/src/domain/usecases/StartPreparationUseCase.ts +47 -57
  27. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  28. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  29. package/types/adapter/proxy/RateLimitCache.d.ts +11 -0
  30. package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
  31. package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
  32. package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts.map +1 -1
  33. package/types/domain/entities/ClaudeTokenUsage.d.ts +6 -0
  34. package/types/domain/entities/ClaudeTokenUsage.d.ts.map +1 -1
  35. package/types/domain/usecases/StartPreparationUseCase.d.ts +3 -3
  36. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
@@ -65,16 +65,24 @@ describe('ProxyClaudeTokenUsageRepository', () => {
65
65
  expect(mockLoadTokens.mock.calls).toEqual([['/tokens.json']]);
66
66
  });
67
67
 
68
+ const futureReset = Math.floor(Date.now() / 1000) + 3600;
69
+ const pastReset = Math.floor(Date.now() / 1000) - 3600;
70
+
68
71
  it('should map each token to its cached utilization', async () => {
69
72
  mockLoadTokens.mockReturnValue(['token-a', 'token-b']);
70
73
  mockReadRateLimit.mockImplementation((token: string) => {
71
74
  if (token === 'token-a') {
72
75
  return {
73
76
  fiveHourUtilization: 42,
74
- fiveHourReset: 0,
77
+ fiveHourReset: futureReset,
75
78
  sevenDayUtilization: 0,
76
- sevenDayReset: 0,
79
+ sevenDayReset: futureReset,
77
80
  blocked: false,
81
+ rejected: false,
82
+ unifiedRejected: false,
83
+ fiveHourRejected: false,
84
+ sevenDayRejected: false,
85
+ modelWeeklyLimits: {},
78
86
  };
79
87
  }
80
88
  return null;
@@ -84,8 +92,20 @@ describe('ProxyClaudeTokenUsageRepository', () => {
84
92
  const result = await repository.getAvailableTokenUsages();
85
93
 
86
94
  expect(result).toEqual([
87
- { token: 'token-a', fiveHourUtilization: 42, blocked: false },
88
- { token: 'token-b', fiveHourUtilization: 0, blocked: false },
95
+ {
96
+ token: 'token-a',
97
+ fiveHourUtilization: 42,
98
+ blocked: false,
99
+ rejected: false,
100
+ modelWeeklyLimits: {},
101
+ },
102
+ {
103
+ token: 'token-b',
104
+ fiveHourUtilization: 0,
105
+ blocked: false,
106
+ rejected: false,
107
+ modelWeeklyLimits: {},
108
+ },
89
109
  ]);
90
110
  });
91
111
 
@@ -93,17 +113,344 @@ describe('ProxyClaudeTokenUsageRepository', () => {
93
113
  mockLoadTokens.mockReturnValue(['token-a']);
94
114
  mockReadRateLimit.mockReturnValue({
95
115
  fiveHourUtilization: 5,
96
- fiveHourReset: 0,
116
+ fiveHourReset: futureReset,
97
117
  sevenDayUtilization: 0,
98
- sevenDayReset: 0,
118
+ sevenDayReset: futureReset,
99
119
  blocked: true,
120
+ rejected: false,
121
+ unifiedRejected: false,
122
+ fiveHourRejected: false,
123
+ sevenDayRejected: false,
124
+ modelWeeklyLimits: {},
125
+ });
126
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
127
+
128
+ const result = await repository.getAvailableTokenUsages();
129
+
130
+ expect(result).toEqual([
131
+ {
132
+ token: 'token-a',
133
+ fiveHourUtilization: 5,
134
+ blocked: true,
135
+ rejected: false,
136
+ modelWeeklyLimits: {},
137
+ },
138
+ ]);
139
+ });
140
+
141
+ it('should propagate the rejected status from the cache', async () => {
142
+ mockLoadTokens.mockReturnValue(['token-a']);
143
+ mockReadRateLimit.mockReturnValue({
144
+ fiveHourUtilization: 100,
145
+ fiveHourReset: futureReset,
146
+ sevenDayUtilization: 0,
147
+ sevenDayReset: futureReset,
148
+ blocked: false,
149
+ rejected: true,
150
+ unifiedRejected: false,
151
+ fiveHourRejected: true,
152
+ sevenDayRejected: false,
153
+ modelWeeklyLimits: {},
154
+ });
155
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
156
+
157
+ const result = await repository.getAvailableTokenUsages();
158
+
159
+ expect(result).toEqual([
160
+ {
161
+ token: 'token-a',
162
+ fiveHourUtilization: 100,
163
+ blocked: false,
164
+ rejected: true,
165
+ modelWeeklyLimits: {},
166
+ },
167
+ ]);
168
+ });
169
+
170
+ it('should normalize fiveHourUtilization to 0 when the 5h reset has passed', async () => {
171
+ mockLoadTokens.mockReturnValue(['token-a']);
172
+ mockReadRateLimit.mockReturnValue({
173
+ fiveHourUtilization: 100,
174
+ fiveHourReset: pastReset,
175
+ sevenDayUtilization: 30,
176
+ sevenDayReset: futureReset,
177
+ blocked: false,
178
+ rejected: false,
179
+ unifiedRejected: false,
180
+ fiveHourRejected: false,
181
+ sevenDayRejected: false,
182
+ modelWeeklyLimits: {},
183
+ });
184
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
185
+
186
+ const result = await repository.getAvailableTokenUsages();
187
+
188
+ expect(result).toEqual([
189
+ {
190
+ token: 'token-a',
191
+ fiveHourUtilization: 0,
192
+ blocked: false,
193
+ rejected: false,
194
+ modelWeeklyLimits: {},
195
+ },
196
+ ]);
197
+ });
198
+
199
+ it('should keep fiveHourUtilization when the 5h reset is in the future', async () => {
200
+ mockLoadTokens.mockReturnValue(['token-a']);
201
+ mockReadRateLimit.mockReturnValue({
202
+ fiveHourUtilization: 95,
203
+ fiveHourReset: futureReset,
204
+ sevenDayUtilization: 0,
205
+ sevenDayReset: futureReset,
206
+ blocked: false,
207
+ rejected: false,
208
+ unifiedRejected: false,
209
+ fiveHourRejected: false,
210
+ sevenDayRejected: false,
211
+ modelWeeklyLimits: {},
212
+ });
213
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
214
+
215
+ const result = await repository.getAvailableTokenUsages();
216
+
217
+ expect(result).toEqual([
218
+ {
219
+ token: 'token-a',
220
+ fiveHourUtilization: 95,
221
+ blocked: false,
222
+ rejected: false,
223
+ modelWeeklyLimits: {},
224
+ },
225
+ ]);
226
+ });
227
+
228
+ it('should clear a 5h-origin rejection once the 5h reset has passed', async () => {
229
+ mockLoadTokens.mockReturnValue(['token-a']);
230
+ mockReadRateLimit.mockReturnValue({
231
+ fiveHourUtilization: 100,
232
+ fiveHourReset: pastReset,
233
+ sevenDayUtilization: 0,
234
+ sevenDayReset: futureReset,
235
+ blocked: false,
236
+ rejected: true,
237
+ unifiedRejected: false,
238
+ fiveHourRejected: true,
239
+ sevenDayRejected: false,
240
+ modelWeeklyLimits: {},
241
+ });
242
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
243
+
244
+ const result = await repository.getAvailableTokenUsages();
245
+
246
+ expect(result).toEqual([
247
+ {
248
+ token: 'token-a',
249
+ fiveHourUtilization: 0,
250
+ blocked: false,
251
+ rejected: false,
252
+ modelWeeklyLimits: {},
253
+ },
254
+ ]);
255
+ });
256
+
257
+ it('should clear a 7d-origin rejection once the 7d reset has passed', async () => {
258
+ mockLoadTokens.mockReturnValue(['token-a']);
259
+ mockReadRateLimit.mockReturnValue({
260
+ fiveHourUtilization: 10,
261
+ fiveHourReset: futureReset,
262
+ sevenDayUtilization: 100,
263
+ sevenDayReset: pastReset,
264
+ blocked: false,
265
+ rejected: true,
266
+ unifiedRejected: false,
267
+ fiveHourRejected: false,
268
+ sevenDayRejected: true,
269
+ modelWeeklyLimits: {},
270
+ });
271
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
272
+
273
+ const result = await repository.getAvailableTokenUsages();
274
+
275
+ expect(result).toEqual([
276
+ {
277
+ token: 'token-a',
278
+ fiveHourUtilization: 10,
279
+ blocked: false,
280
+ rejected: false,
281
+ modelWeeklyLimits: {},
282
+ },
283
+ ]);
284
+ });
285
+
286
+ it('should keep a 5h-origin rejection while the 5h reset is in the future', async () => {
287
+ mockLoadTokens.mockReturnValue(['token-a']);
288
+ mockReadRateLimit.mockReturnValue({
289
+ fiveHourUtilization: 100,
290
+ fiveHourReset: futureReset,
291
+ sevenDayUtilization: 0,
292
+ sevenDayReset: futureReset,
293
+ blocked: false,
294
+ rejected: true,
295
+ unifiedRejected: false,
296
+ fiveHourRejected: true,
297
+ sevenDayRejected: false,
298
+ modelWeeklyLimits: {},
299
+ });
300
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
301
+
302
+ const result = await repository.getAvailableTokenUsages();
303
+
304
+ expect(result).toEqual([
305
+ {
306
+ token: 'token-a',
307
+ fiveHourUtilization: 100,
308
+ blocked: false,
309
+ rejected: true,
310
+ modelWeeklyLimits: {},
311
+ },
312
+ ]);
313
+ });
314
+
315
+ it('should keep a still-active 7d rejection after the 5h reset has passed', async () => {
316
+ mockLoadTokens.mockReturnValue(['token-a']);
317
+ mockReadRateLimit.mockReturnValue({
318
+ fiveHourUtilization: 100,
319
+ fiveHourReset: pastReset,
320
+ sevenDayUtilization: 100,
321
+ sevenDayReset: futureReset,
322
+ blocked: false,
323
+ rejected: true,
324
+ unifiedRejected: false,
325
+ fiveHourRejected: false,
326
+ sevenDayRejected: true,
327
+ modelWeeklyLimits: {},
328
+ });
329
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
330
+
331
+ const result = await repository.getAvailableTokenUsages();
332
+
333
+ expect(result).toEqual([
334
+ {
335
+ token: 'token-a',
336
+ fiveHourUtilization: 0,
337
+ blocked: false,
338
+ rejected: true,
339
+ modelWeeklyLimits: {},
340
+ },
341
+ ]);
342
+ });
343
+
344
+ it('should clear a unified rejection once the 5h reset has passed', async () => {
345
+ mockLoadTokens.mockReturnValue(['token-a']);
346
+ mockReadRateLimit.mockReturnValue({
347
+ fiveHourUtilization: 100,
348
+ fiveHourReset: pastReset,
349
+ sevenDayUtilization: 0,
350
+ sevenDayReset: futureReset,
351
+ blocked: false,
352
+ rejected: true,
353
+ unifiedRejected: true,
354
+ fiveHourRejected: false,
355
+ sevenDayRejected: false,
356
+ modelWeeklyLimits: {},
357
+ });
358
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
359
+
360
+ const result = await repository.getAvailableTokenUsages();
361
+
362
+ expect(result).toEqual([
363
+ {
364
+ token: 'token-a',
365
+ fiveHourUtilization: 0,
366
+ blocked: false,
367
+ rejected: false,
368
+ modelWeeklyLimits: {},
369
+ },
370
+ ]);
371
+ });
372
+
373
+ it('should default rejected to false when no snapshot exists', async () => {
374
+ mockLoadTokens.mockReturnValue(['token-a']);
375
+ mockReadRateLimit.mockReturnValue(null);
376
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
377
+
378
+ const result = await repository.getAvailableTokenUsages();
379
+
380
+ expect(result).toEqual([
381
+ {
382
+ token: 'token-a',
383
+ fiveHourUtilization: 0,
384
+ blocked: false,
385
+ rejected: false,
386
+ modelWeeklyLimits: {},
387
+ },
388
+ ]);
389
+ });
390
+
391
+ it('should keep a model weekly rejection while its reset is in the future', async () => {
392
+ mockLoadTokens.mockReturnValue(['token-a']);
393
+ mockReadRateLimit.mockReturnValue({
394
+ fiveHourUtilization: 5,
395
+ fiveHourReset: futureReset,
396
+ sevenDayUtilization: 10,
397
+ sevenDayReset: futureReset,
398
+ blocked: false,
399
+ rejected: false,
400
+ unifiedRejected: false,
401
+ fiveHourRejected: false,
402
+ sevenDayRejected: false,
403
+ modelWeeklyLimits: {
404
+ seven_day_sonnet: { rejected: true, resetsAt: futureReset },
405
+ },
406
+ });
407
+ const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
408
+
409
+ const result = await repository.getAvailableTokenUsages();
410
+
411
+ expect(result).toEqual([
412
+ {
413
+ token: 'token-a',
414
+ fiveHourUtilization: 5,
415
+ blocked: false,
416
+ rejected: false,
417
+ modelWeeklyLimits: {
418
+ seven_day_sonnet: { rejected: true, resetsAt: futureReset },
419
+ },
420
+ },
421
+ ]);
422
+ });
423
+
424
+ it('should clear a model weekly rejection once its reset has passed', async () => {
425
+ mockLoadTokens.mockReturnValue(['token-a']);
426
+ mockReadRateLimit.mockReturnValue({
427
+ fiveHourUtilization: 5,
428
+ fiveHourReset: futureReset,
429
+ sevenDayUtilization: 10,
430
+ sevenDayReset: futureReset,
431
+ blocked: false,
432
+ rejected: false,
433
+ unifiedRejected: false,
434
+ fiveHourRejected: false,
435
+ sevenDayRejected: false,
436
+ modelWeeklyLimits: {
437
+ seven_day_sonnet: { rejected: true, resetsAt: pastReset },
438
+ },
100
439
  });
101
440
  const repository = new ProxyClaudeTokenUsageRepository('/tokens.json');
102
441
 
103
442
  const result = await repository.getAvailableTokenUsages();
104
443
 
105
444
  expect(result).toEqual([
106
- { token: 'token-a', fiveHourUtilization: 5, blocked: true },
445
+ {
446
+ token: 'token-a',
447
+ fiveHourUtilization: 5,
448
+ blocked: false,
449
+ rejected: false,
450
+ modelWeeklyLimits: {
451
+ seven_day_sonnet: { rejected: false, resetsAt: pastReset },
452
+ },
453
+ },
107
454
  ]);
108
455
  });
109
456
  });
@@ -22,12 +22,52 @@ export class ProxyClaudeTokenUsageRepository implements ClaudeTokenUsageReposito
22
22
  if (tokens === null) {
23
23
  return [];
24
24
  }
25
+ const nowEpochSeconds = Date.now() / 1000;
25
26
  return tokens.map((token) => {
26
27
  const snapshot = readRateLimit(token);
28
+ if (snapshot === null) {
29
+ return {
30
+ token,
31
+ fiveHourUtilization: 0,
32
+ blocked: false,
33
+ rejected: false,
34
+ modelWeeklyLimits: {},
35
+ };
36
+ }
37
+ const fiveHourExpired = nowEpochSeconds > snapshot.fiveHourReset;
38
+ const sevenDayExpired = nowEpochSeconds > snapshot.sevenDayReset;
39
+ const fiveHourUtilization = fiveHourExpired
40
+ ? 0
41
+ : snapshot.fiveHourUtilization;
42
+ const fiveHourRejectionActive =
43
+ snapshot.fiveHourRejected && !fiveHourExpired;
44
+ const sevenDayRejectionActive =
45
+ snapshot.sevenDayRejected && !sevenDayExpired;
46
+ const unifiedRejectionActive =
47
+ snapshot.unifiedRejected && !fiveHourExpired;
48
+ const rejected =
49
+ unifiedRejectionActive ||
50
+ fiveHourRejectionActive ||
51
+ sevenDayRejectionActive;
52
+ const modelWeeklyLimits: Record<
53
+ string,
54
+ { rejected: boolean; resetsAt: number }
55
+ > = {};
56
+ for (const [limitType, limit] of Object.entries(
57
+ snapshot.modelWeeklyLimits,
58
+ )) {
59
+ const expired = nowEpochSeconds > limit.resetsAt;
60
+ modelWeeklyLimits[limitType] = {
61
+ rejected: limit.rejected && !expired,
62
+ resetsAt: limit.resetsAt,
63
+ };
64
+ }
27
65
  return {
28
66
  token,
29
- fiveHourUtilization: snapshot ? snapshot.fiveHourUtilization : 0,
30
- blocked: snapshot?.blocked ?? false,
67
+ fiveHourUtilization,
68
+ blocked: snapshot.blocked,
69
+ rejected,
70
+ modelWeeklyLimits,
31
71
  };
32
72
  });
33
73
  };
@@ -1,5 +1,12 @@
1
+ export type ClaudeModelWeeklyLimit = {
2
+ rejected: boolean;
3
+ resetsAt: number;
4
+ };
5
+
1
6
  export type ClaudeTokenUsage = {
2
7
  token: string;
3
8
  fiveHourUtilization: number;
4
9
  blocked: boolean;
10
+ rejected: boolean;
11
+ modelWeeklyLimits: Record<string, ClaudeModelWeeklyLimit>;
5
12
  };