github-issue-tower-defence-management 1.58.2 → 1.59.0

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 (61) hide show
  1. package/.gitattributes +2 -0
  2. package/.prettierignore +1 -0
  3. package/CHANGELOG.md +21 -0
  4. package/README.md +3 -3
  5. package/bin/adapter/entry-points/cli/index.js +1 -3
  6. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +8 -4
  8. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/proxy/RateLimitCache.js +104 -12
  10. package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
  11. package/bin/adapter/proxy/proxyEntry.js +19 -0
  12. package/bin/adapter/proxy/proxyEntry.js.map +1 -1
  13. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +33 -2
  14. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -1
  15. package/bin/adapter/repositories/ProxyRateLimitCacheRepository.js +92 -0
  16. package/bin/adapter/repositories/ProxyRateLimitCacheRepository.js.map +1 -0
  17. package/bin/domain/usecases/HandleScheduledEventUseCase.js +7 -1
  18. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  19. package/bin/domain/usecases/StartPreparationUseCase.js +34 -35
  20. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  21. package/bin/domain/usecases/UpdateRateLimitCacheUseCase.js +18 -0
  22. package/bin/domain/usecases/UpdateRateLimitCacheUseCase.js.map +1 -0
  23. package/bin/domain/usecases/adapter-interfaces/RateLimitCacheRepository.js +3 -0
  24. package/bin/domain/usecases/adapter-interfaces/RateLimitCacheRepository.js.map +1 -0
  25. package/package.json +1 -1
  26. package/src/adapter/entry-points/cli/index.ts +0 -3
  27. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +6 -0
  28. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +9 -3
  29. package/src/adapter/proxy/RateLimitCache.test.ts +250 -0
  30. package/src/adapter/proxy/RateLimitCache.ts +114 -25
  31. package/src/adapter/proxy/proxyEntry.ts +24 -1
  32. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +354 -7
  33. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +42 -2
  34. package/src/adapter/repositories/ProxyRateLimitCacheRepository.test.ts +101 -0
  35. package/src/adapter/repositories/ProxyRateLimitCacheRepository.ts +66 -0
  36. package/src/domain/entities/ClaudeTokenUsage.ts +7 -0
  37. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +63 -0
  38. package/src/domain/usecases/HandleScheduledEventUseCase.ts +7 -0
  39. package/src/domain/usecases/StartPreparationUseCase.test.ts +1212 -887
  40. package/src/domain/usecases/StartPreparationUseCase.ts +47 -57
  41. package/src/domain/usecases/UpdateRateLimitCacheUseCase.test.ts +81 -0
  42. package/src/domain/usecases/UpdateRateLimitCacheUseCase.ts +16 -0
  43. package/src/domain/usecases/adapter-interfaces/RateLimitCacheRepository.ts +9 -0
  44. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  45. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  46. package/types/adapter/proxy/RateLimitCache.d.ts +11 -0
  47. package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
  48. package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
  49. package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts.map +1 -1
  50. package/types/adapter/repositories/ProxyRateLimitCacheRepository.d.ts +10 -0
  51. package/types/adapter/repositories/ProxyRateLimitCacheRepository.d.ts.map +1 -0
  52. package/types/domain/entities/ClaudeTokenUsage.d.ts +6 -0
  53. package/types/domain/entities/ClaudeTokenUsage.d.ts.map +1 -1
  54. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -1
  55. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  56. package/types/domain/usecases/StartPreparationUseCase.d.ts +3 -3
  57. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  58. package/types/domain/usecases/UpdateRateLimitCacheUseCase.d.ts +9 -0
  59. package/types/domain/usecases/UpdateRateLimitCacheUseCase.d.ts.map +1 -0
  60. package/types/domain/usecases/adapter-interfaces/RateLimitCacheRepository.d.ts +9 -0
  61. package/types/domain/usecases/adapter-interfaces/RateLimitCacheRepository.d.ts.map +1 -0
@@ -0,0 +1,101 @@
1
+ const mockReadRateLimit = jest.fn();
2
+ const mockLoadTokens = jest.fn();
3
+
4
+ jest.mock('../proxy/RateLimitCache', () => ({
5
+ PROXY_PORT: 8787,
6
+ readRateLimit: mockReadRateLimit,
7
+ }));
8
+
9
+ jest.mock('../proxy/TokenListLoader', () => ({
10
+ loadTokens: mockLoadTokens,
11
+ }));
12
+
13
+ import { ProxyRateLimitCacheRepository } from './ProxyRateLimitCacheRepository';
14
+
15
+ describe('ProxyRateLimitCacheRepository', () => {
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+ });
19
+
20
+ describe('getTokenRateLimitCaches', () => {
21
+ it('should return an empty list when no token path is configured', () => {
22
+ const repository = new ProxyRateLimitCacheRepository(null);
23
+
24
+ const result = repository.getTokenRateLimitCaches();
25
+
26
+ expect(result).toEqual([]);
27
+ expect(mockLoadTokens).not.toHaveBeenCalled();
28
+ });
29
+
30
+ it('should return an empty list when the token list cannot be loaded', () => {
31
+ mockLoadTokens.mockReturnValue(null);
32
+ const repository = new ProxyRateLimitCacheRepository('/tokens.json');
33
+
34
+ const result = repository.getTokenRateLimitCaches();
35
+
36
+ expect(result).toEqual([]);
37
+ });
38
+
39
+ const futureReset = Math.floor(Date.now() / 1000) + 3600;
40
+
41
+ it('should return fiveHourReset as unifiedReset for a token with a cached snapshot', () => {
42
+ mockLoadTokens.mockReturnValue(['token-a']);
43
+ mockReadRateLimit.mockReturnValue({
44
+ fiveHourUtilization: 42,
45
+ fiveHourReset: futureReset,
46
+ sevenDayUtilization: 0,
47
+ sevenDayReset: futureReset,
48
+ blocked: false,
49
+ rejected: false,
50
+ unifiedRejected: false,
51
+ fiveHourRejected: false,
52
+ sevenDayRejected: false,
53
+ modelWeeklyLimits: {},
54
+ });
55
+ const repository = new ProxyRateLimitCacheRepository('/tokens.json');
56
+
57
+ const result = repository.getTokenRateLimitCaches();
58
+
59
+ expect(result).toEqual([{ token: 'token-a', unifiedReset: futureReset }]);
60
+ });
61
+
62
+ it('should return unifiedReset of 0 when no snapshot exists for a token', () => {
63
+ mockLoadTokens.mockReturnValue(['token-a']);
64
+ mockReadRateLimit.mockReturnValue(null);
65
+ const repository = new ProxyRateLimitCacheRepository('/tokens.json');
66
+
67
+ const result = repository.getTokenRateLimitCaches();
68
+
69
+ expect(result).toEqual([{ token: 'token-a', unifiedReset: 0 }]);
70
+ });
71
+
72
+ it('should return entries for all tokens in the list', () => {
73
+ mockLoadTokens.mockReturnValue(['token-a', 'token-b']);
74
+ mockReadRateLimit.mockImplementation((token: string) => {
75
+ if (token === 'token-a') {
76
+ return {
77
+ fiveHourUtilization: 10,
78
+ fiveHourReset: futureReset,
79
+ sevenDayUtilization: 0,
80
+ sevenDayReset: futureReset,
81
+ blocked: false,
82
+ rejected: false,
83
+ unifiedRejected: false,
84
+ fiveHourRejected: false,
85
+ sevenDayRejected: false,
86
+ modelWeeklyLimits: {},
87
+ };
88
+ }
89
+ return null;
90
+ });
91
+ const repository = new ProxyRateLimitCacheRepository('/tokens.json');
92
+
93
+ const result = repository.getTokenRateLimitCaches();
94
+
95
+ expect(result).toEqual([
96
+ { token: 'token-a', unifiedReset: futureReset },
97
+ { token: 'token-b', unifiedReset: 0 },
98
+ ]);
99
+ });
100
+ });
101
+ });
@@ -0,0 +1,66 @@
1
+ import * as http from 'http';
2
+ import { RateLimitCacheRepository } from '../../domain/usecases/adapter-interfaces/RateLimitCacheRepository';
3
+ import { TokenRateLimitCache } from '../../domain/usecases/adapter-interfaces/RateLimitCacheRepository';
4
+ import { PROXY_PORT, readRateLimit } from '../proxy/RateLimitCache';
5
+ import { loadTokens } from '../proxy/TokenListLoader';
6
+
7
+ const HAIKU_MODEL = 'claude-haiku-4-5';
8
+
9
+ const PROBE_REQUEST_BODY = JSON.stringify({
10
+ model: HAIKU_MODEL,
11
+ max_tokens: 1,
12
+ messages: [{ role: 'user', content: 'hi' }],
13
+ });
14
+
15
+ export class ProxyRateLimitCacheRepository implements RateLimitCacheRepository {
16
+ constructor(
17
+ private readonly tokenListJsonPath: string | null,
18
+ private readonly port: number = PROXY_PORT,
19
+ ) {}
20
+
21
+ getTokenRateLimitCaches = (): TokenRateLimitCache[] => {
22
+ if (this.tokenListJsonPath === null) {
23
+ return [];
24
+ }
25
+ const tokens = loadTokens(this.tokenListJsonPath);
26
+ if (tokens === null) {
27
+ return [];
28
+ }
29
+ return tokens.map((token) => {
30
+ const snapshot = readRateLimit(token);
31
+ const unifiedReset = snapshot !== null ? snapshot.fiveHourReset : 0;
32
+ return { token, unifiedReset };
33
+ });
34
+ };
35
+
36
+ probeToken = async (token: string): Promise<void> => {
37
+ await new Promise<void>((resolve) => {
38
+ const request = http.request(
39
+ {
40
+ host: '127.0.0.1',
41
+ port: this.port,
42
+ method: 'POST',
43
+ path: '/v1/messages',
44
+ headers: {
45
+ 'content-type': 'application/json',
46
+ 'anthropic-version': '2023-06-01',
47
+ authorization: `Bearer ${token}`,
48
+ 'content-length': Buffer.byteLength(PROBE_REQUEST_BODY),
49
+ },
50
+ },
51
+ (response) => {
52
+ response.resume();
53
+ response.on('end', () => resolve());
54
+ },
55
+ );
56
+ request.on('error', (error) => {
57
+ console.error(
58
+ `[UpdateRateLimitCache] Probe request failed for token hash: ${error.message}`,
59
+ );
60
+ resolve();
61
+ });
62
+ request.write(PROBE_REQUEST_BODY);
63
+ request.end();
64
+ });
65
+ };
66
+ }
@@ -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
  };
@@ -23,6 +23,7 @@ import { StartPreparationUseCase } from './StartPreparationUseCase';
23
23
  import { RevertOrphanedPreparationUseCase } from './RevertOrphanedPreparationUseCase';
24
24
  import { RevertNotReadyAwaitingQualityCheckUseCase } from './RevertNotReadyAwaitingQualityCheckUseCase';
25
25
  import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCase';
26
+ import { UpdateRateLimitCacheUseCase } from './UpdateRateLimitCacheUseCase';
26
27
 
27
28
  describe('HandleScheduledEventUseCase', () => {
28
29
  describe('createTargetDateTimes', () => {
@@ -112,6 +113,7 @@ describe('HandleScheduledEventUseCase', () => {
112
113
  mock<RevertOrphanedPreparationUseCase>();
113
114
  const mockRevertNotReadyAwaitingQualityCheckUseCase =
114
115
  mock<RevertNotReadyAwaitingQualityCheckUseCase>();
116
+ const mockUpdateRateLimitCacheUseCase = mock<UpdateRateLimitCacheUseCase>();
115
117
  const mockDateRepository = mock<DateRepository>();
116
118
  const mockSpreadsheetRepository = mock<SpreadsheetRepository>();
117
119
  const mockProjectRepository = mock<ProjectRepository>();
@@ -135,6 +137,7 @@ describe('HandleScheduledEventUseCase', () => {
135
137
  mockStartPreparationUseCase,
136
138
  mockRevertOrphanedPreparationUseCase,
137
139
  mockRevertNotReadyAwaitingQualityCheckUseCase,
140
+ mockUpdateRateLimitCacheUseCase,
138
141
  mockDateRepository,
139
142
  mockSpreadsheetRepository,
140
143
  mockProjectRepository,
@@ -367,6 +370,66 @@ describe('HandleScheduledEventUseCase', () => {
367
370
  ).not.toHaveBeenCalled();
368
371
  });
369
372
 
373
+ it('should invoke UpdateRateLimitCacheUseCase before StartPreparationUseCase when startPreparation is configured', async () => {
374
+ const callOrder: string[] = [];
375
+ mockUpdateRateLimitCacheUseCase.run.mockImplementation(async () => {
376
+ callOrder.push('updateRateLimitCache');
377
+ });
378
+ mockStartPreparationUseCase.run.mockImplementation(async () => {
379
+ callOrder.push('startPreparation');
380
+ });
381
+
382
+ const input = {
383
+ projectName: 'test-project',
384
+ org: 'test-org',
385
+ projectUrl: 'https://github.com/test-org/test-project',
386
+ manager: 'test-manager',
387
+ workingReport: {
388
+ repo: 'test-repo',
389
+ members: ['member1'],
390
+ spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
391
+ },
392
+ urlOfStoryView: 'https://github.com/test-org/test-project/issues',
393
+ disabled: false,
394
+ allowIssueCacheMinutes: 60,
395
+ startPreparation: {
396
+ defaultAgentName: 'aw',
397
+ configFilePath: '/path/to/config.yml',
398
+ maximumPreparingIssuesCount: null,
399
+ },
400
+ };
401
+
402
+ mockProjectRepository.getProject.mockResolvedValue(mock<Project>());
403
+ await useCase.run(input);
404
+
405
+ expect(mockUpdateRateLimitCacheUseCase.run).toHaveBeenCalledTimes(1);
406
+ expect(callOrder.indexOf('updateRateLimitCache')).toBeLessThan(
407
+ callOrder.indexOf('startPreparation'),
408
+ );
409
+ });
410
+
411
+ it('should not invoke UpdateRateLimitCacheUseCase when startPreparation is absent', async () => {
412
+ const input = {
413
+ projectName: 'test-project',
414
+ org: 'test-org',
415
+ projectUrl: 'https://github.com/test-org/test-project',
416
+ manager: 'test-manager',
417
+ workingReport: {
418
+ repo: 'test-repo',
419
+ members: ['member1'],
420
+ spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
421
+ },
422
+ urlOfStoryView: 'https://github.com/test-org/test-project/issues',
423
+ disabled: false,
424
+ allowIssueCacheMinutes: 60,
425
+ };
426
+
427
+ mockProjectRepository.getProject.mockResolvedValue(mock<Project>());
428
+ await useCase.run(input);
429
+
430
+ expect(mockUpdateRateLimitCacheUseCase.run).not.toHaveBeenCalled();
431
+ });
432
+
370
433
  describe('story issue creation progress logs', () => {
371
434
  const storyInput = {
372
435
  projectName: 'test-project',
@@ -23,6 +23,7 @@ import { StartPreparationUseCase } from './StartPreparationUseCase';
23
23
  import { RevertOrphanedPreparationUseCase } from './RevertOrphanedPreparationUseCase';
24
24
  import { RevertNotReadyAwaitingQualityCheckUseCase } from './RevertNotReadyAwaitingQualityCheckUseCase';
25
25
  import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCase';
26
+ import { UpdateRateLimitCacheUseCase } from './UpdateRateLimitCacheUseCase';
26
27
 
27
28
  export class ProjectNotFoundError extends Error {
28
29
  constructor(message: string) {
@@ -52,6 +53,7 @@ export class HandleScheduledEventUseCase {
52
53
  readonly startPreparationUseCase: StartPreparationUseCase,
53
54
  readonly revertOrphanedPreparationUseCase: RevertOrphanedPreparationUseCase,
54
55
  readonly revertNotReadyAwaitingQualityCheckUseCase: RevertNotReadyAwaitingQualityCheckUseCase,
56
+ readonly updateRateLimitCacheUseCase: UpdateRateLimitCacheUseCase | null,
55
57
  readonly dateRepository: DateRepository,
56
58
  readonly spreadsheetRepository: SpreadsheetRepository,
57
59
  readonly projectRepository: ProjectRepository,
@@ -264,6 +266,11 @@ ${JSON.stringify(e)}
264
266
  });
265
267
  }
266
268
  if (input.startPreparation) {
269
+ if (this.updateRateLimitCacheUseCase !== null) {
270
+ await this.updateRateLimitCacheUseCase.run({
271
+ nowEpochSeconds: Date.now() / 1000,
272
+ });
273
+ }
267
274
  if (input.startPreparation.preparationProcessCheckCommand) {
268
275
  await this.revertOrphanedPreparationUseCase.run({
269
276
  projectUrl: input.projectUrl,