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.
- package/.gitattributes +2 -0
- package/.prettierignore +1 -0
- package/CHANGELOG.md +21 -0
- package/README.md +3 -3
- package/bin/adapter/entry-points/cli/index.js +1 -3
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +8 -4
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/proxy/RateLimitCache.js +104 -12
- package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
- package/bin/adapter/proxy/proxyEntry.js +19 -0
- package/bin/adapter/proxy/proxyEntry.js.map +1 -1
- package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +33 -2
- package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -1
- package/bin/adapter/repositories/ProxyRateLimitCacheRepository.js +92 -0
- package/bin/adapter/repositories/ProxyRateLimitCacheRepository.js.map +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +7 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +34 -35
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/UpdateRateLimitCacheUseCase.js +18 -0
- package/bin/domain/usecases/UpdateRateLimitCacheUseCase.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/RateLimitCacheRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/RateLimitCacheRepository.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +0 -3
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +6 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +9 -3
- package/src/adapter/proxy/RateLimitCache.test.ts +250 -0
- package/src/adapter/proxy/RateLimitCache.ts +114 -25
- package/src/adapter/proxy/proxyEntry.ts +24 -1
- package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +354 -7
- package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +42 -2
- package/src/adapter/repositories/ProxyRateLimitCacheRepository.test.ts +101 -0
- package/src/adapter/repositories/ProxyRateLimitCacheRepository.ts +66 -0
- package/src/domain/entities/ClaudeTokenUsage.ts +7 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +63 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +7 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +1212 -887
- package/src/domain/usecases/StartPreparationUseCase.ts +47 -57
- package/src/domain/usecases/UpdateRateLimitCacheUseCase.test.ts +81 -0
- package/src/domain/usecases/UpdateRateLimitCacheUseCase.ts +16 -0
- package/src/domain/usecases/adapter-interfaces/RateLimitCacheRepository.ts +9 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/proxy/RateLimitCache.d.ts +11 -0
- package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
- package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
- package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts.map +1 -1
- package/types/adapter/repositories/ProxyRateLimitCacheRepository.d.ts +10 -0
- package/types/adapter/repositories/ProxyRateLimitCacheRepository.d.ts.map +1 -0
- package/types/domain/entities/ClaudeTokenUsage.d.ts +6 -0
- package/types/domain/entities/ClaudeTokenUsage.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +3 -3
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/UpdateRateLimitCacheUseCase.d.ts +9 -0
- package/types/domain/usecases/UpdateRateLimitCacheUseCase.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/RateLimitCacheRepository.d.ts +9 -0
- 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,
|