github-issue-tower-defence-management 1.60.1 → 1.63.1
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/.github/workflows/publish.yml +13 -0
- package/.github/workflows/test.yml +0 -4
- package/CHANGELOG.md +14 -0
- package/README.md +53 -10
- package/bin/adapter/entry-points/cli/index.js +11 -11
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js +3 -22
- package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +8 -22
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/rotationOrderFileWriter.js +56 -0
- package/bin/adapter/entry-points/handlers/rotationOrderFileWriter.js.map +1 -0
- package/bin/adapter/entry-points/handlers/situationFileWriter.js +5 -0
- package/bin/adapter/entry-points/handlers/situationFileWriter.js.map +1 -1
- package/bin/adapter/proxy/TokenListLoader.js +21 -6
- package/bin/adapter/proxy/TokenListLoader.js.map +1 -1
- package/bin/adapter/proxy/proxyEntry.js +1 -0
- package/bin/adapter/proxy/proxyEntry.js.map +1 -1
- package/bin/adapter/repositories/BaseGitHubRepository.js +1 -113
- package/bin/adapter/repositories/BaseGitHubRepository.js.map +1 -1
- package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +5 -3
- package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +8 -7
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js +19 -9
- package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +15 -3
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/IssueRejectionEvaluator.js +8 -1
- package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +5 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js +32 -1
- package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +91 -12
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -4
- package/src/adapter/entry-points/cli/index.test.ts +16 -16
- package/src/adapter/entry-points/cli/index.ts +8 -11
- package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.test.ts +2 -55
- package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.ts +1 -11
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +6 -56
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +7 -11
- package/src/adapter/entry-points/handlers/rotationOrderFileWriter.test.ts +177 -0
- package/src/adapter/entry-points/handlers/rotationOrderFileWriter.ts +20 -0
- package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +36 -0
- package/src/adapter/entry-points/handlers/situationFileWriter.ts +8 -0
- package/src/adapter/proxy/TokenListLoader.test.ts +50 -1
- package/src/adapter/proxy/TokenListLoader.ts +25 -5
- package/src/adapter/proxy/proxyEntry.test.ts +270 -1
- package/src/adapter/proxy/proxyEntry.ts +2 -1
- package/src/adapter/repositories/BaseGitHubRepository.test.ts +1 -186
- package/src/adapter/repositories/BaseGitHubRepository.ts +1 -139
- package/src/adapter/repositories/GraphqlProjectRepository.errorHandling.test.ts +0 -1
- package/src/adapter/repositories/GraphqlProjectRepository.fetchProjectId.test.ts +4 -1
- package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +60 -19
- package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +6 -4
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +23 -13
- package/src/adapter/repositories/issue/ApiV3IssueRepository.test.ts +0 -1
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +0 -8
- package/src/adapter/repositories/issue/RestIssueRepository.test.ts +0 -1
- package/src/domain/entities/ClaudeTokenUsage.ts +1 -0
- package/src/domain/usecases/CreateNewStoryByLabelUseCase.test.ts +196 -11
- package/src/domain/usecases/CreateNewStoryByLabelUseCase.ts +32 -15
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +4 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +21 -5
- package/src/domain/usecases/IssueRejectionEvaluator.test.ts +153 -0
- package/src/domain/usecases/IssueRejectionEvaluator.ts +8 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +175 -31
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +7 -1
- package/src/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.test.ts +32 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +39 -5
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +1 -1
- package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.test.ts +139 -20
- package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.ts +62 -2
- package/src/domain/usecases/StartPreparationUseCase.test.ts +404 -21
- package/src/domain/usecases/StartPreparationUseCase.ts +152 -16
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +16 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/rotationOrderFileWriter.d.ts +3 -0
- package/types/adapter/entry-points/handlers/rotationOrderFileWriter.d.ts.map +1 -0
- package/types/adapter/entry-points/handlers/situationFileWriter.d.ts +1 -0
- package/types/adapter/entry-points/handlers/situationFileWriter.d.ts.map +1 -1
- package/types/adapter/proxy/TokenListLoader.d.ts +5 -0
- package/types/adapter/proxy/TokenListLoader.d.ts.map +1 -1
- package/types/adapter/proxy/proxyEntry.d.ts +2 -1
- package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
- package/types/adapter/repositories/BaseGitHubRepository.d.ts +1 -23
- package/types/adapter/repositories/BaseGitHubRepository.d.ts.map +1 -1
- package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +14 -5
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/entities/ClaudeTokenUsage.d.ts +1 -0
- package/types/domain/entities/ClaudeTokenUsage.d.ts.map +1 -1
- package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts +4 -2
- package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +5 -2
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts +1 -1
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts +5 -2
- package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +15 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +14 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/bin/adapter/repositories/issue/CheerioIssueRepository.js +0 -136
- package/bin/adapter/repositories/issue/CheerioIssueRepository.js.map +0 -1
- package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js +0 -1606
- package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js.map +0 -1
- package/src/adapter/repositories/issue/CheerioIssueRepository.test.ts +0 -6552
- package/src/adapter/repositories/issue/CheerioIssueRepository.ts +0 -142
- package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.test.ts +0 -118
- package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.ts +0 -584
- package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts +0 -40
- package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts.map +0 -1
- package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts +0 -220
- package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts.map +0 -1
|
@@ -187,6 +187,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
187
187
|
url: 'https://github.com/user/repo/pull/42',
|
|
188
188
|
branchName: 'i1',
|
|
189
189
|
createdAt: new Date('2024-01-01'),
|
|
190
|
+
isDraft: false,
|
|
190
191
|
isConflicted: false,
|
|
191
192
|
isPassedAllCiJob: false,
|
|
192
193
|
isCiStateSuccess: false,
|
|
@@ -247,6 +248,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
247
248
|
url: 'https://github.com/user/repo/pull/354',
|
|
248
249
|
branchName: 'dependabot/npm_and_yarn/multi-cc382f683c',
|
|
249
250
|
createdAt: new Date('2024-01-01'),
|
|
251
|
+
isDraft: false,
|
|
250
252
|
isConflicted: false,
|
|
251
253
|
isPassedAllCiJob: false,
|
|
252
254
|
isCiStateSuccess: false,
|
|
@@ -343,6 +345,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
343
345
|
url: 'https://github.com/user/repo/pull/999',
|
|
344
346
|
branchName: null,
|
|
345
347
|
createdAt: new Date('2024-01-01'),
|
|
348
|
+
isDraft: false,
|
|
346
349
|
isConflicted: false,
|
|
347
350
|
isPassedAllCiJob: false,
|
|
348
351
|
isCiStateSuccess: false,
|
|
@@ -389,6 +392,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
389
392
|
url: 'https://github.com/user/repo/pull/999',
|
|
390
393
|
branchName: 'evil$(rm -rf /)',
|
|
391
394
|
createdAt: new Date('2024-01-01'),
|
|
395
|
+
isDraft: false,
|
|
392
396
|
isConflicted: false,
|
|
393
397
|
isPassedAllCiJob: false,
|
|
394
398
|
isCiStateSuccess: false,
|
|
@@ -431,6 +435,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
431
435
|
url: 'https://github.com/user/repo/pull/42',
|
|
432
436
|
branchName: 'i1',
|
|
433
437
|
createdAt: new Date('2024-01-01T00:00:00Z'),
|
|
438
|
+
isDraft: false,
|
|
434
439
|
isConflicted: false,
|
|
435
440
|
isPassedAllCiJob: false,
|
|
436
441
|
isCiStateSuccess: false,
|
|
@@ -442,6 +447,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
442
447
|
url: 'https://github.com/user/repo/pull/43',
|
|
443
448
|
branchName: 'i1-fix',
|
|
444
449
|
createdAt: new Date('2024-01-02T00:00:00Z'),
|
|
450
|
+
isDraft: false,
|
|
445
451
|
isConflicted: false,
|
|
446
452
|
isPassedAllCiJob: false,
|
|
447
453
|
isCiStateSuccess: false,
|
|
@@ -519,6 +525,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
519
525
|
url: 'https://github.com/user/repo/pull/42',
|
|
520
526
|
branchName: null,
|
|
521
527
|
createdAt: new Date('2024-01-01T00:00:00Z'),
|
|
528
|
+
isDraft: false,
|
|
522
529
|
isConflicted: false,
|
|
523
530
|
isPassedAllCiJob: false,
|
|
524
531
|
isCiStateSuccess: false,
|
|
@@ -530,6 +537,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
530
537
|
url: 'https://github.com/user/repo/pull/43',
|
|
531
538
|
branchName: 'i1-fix',
|
|
532
539
|
createdAt: new Date('2024-01-02T00:00:00Z'),
|
|
540
|
+
isDraft: false,
|
|
533
541
|
isConflicted: false,
|
|
534
542
|
isPassedAllCiJob: false,
|
|
535
543
|
isCiStateSuccess: false,
|
|
@@ -592,6 +600,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
592
600
|
url: 'https://github.com/user/repo/pull/42',
|
|
593
601
|
branchName: null,
|
|
594
602
|
createdAt: new Date('2024-01-01'),
|
|
603
|
+
isDraft: false,
|
|
595
604
|
isConflicted: false,
|
|
596
605
|
isPassedAllCiJob: false,
|
|
597
606
|
isCiStateSuccess: false,
|
|
@@ -1187,6 +1196,123 @@ describe('StartPreparationUseCase', () => {
|
|
|
1187
1196
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(6);
|
|
1188
1197
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(6);
|
|
1189
1198
|
});
|
|
1199
|
+
it('should allow six preparing processes per available Claude OAuth token when maximumPreparingIssuesCount is null', async () => {
|
|
1200
|
+
const awaitingIssues: Issue[] = Array.from({ length: 20 }, (_, i) =>
|
|
1201
|
+
createMockIssue({
|
|
1202
|
+
url: `url${i + 1}`,
|
|
1203
|
+
title: `Issue ${i + 1}`,
|
|
1204
|
+
labels: [],
|
|
1205
|
+
status: 'Awaiting Workspace',
|
|
1206
|
+
number: i + 1,
|
|
1207
|
+
itemId: `item-${i + 1}`,
|
|
1208
|
+
}),
|
|
1209
|
+
);
|
|
1210
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1211
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1212
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
1213
|
+
);
|
|
1214
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1215
|
+
stdout: '',
|
|
1216
|
+
stderr: '',
|
|
1217
|
+
exitCode: 0,
|
|
1218
|
+
});
|
|
1219
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
1220
|
+
{
|
|
1221
|
+
name: 'token-a',
|
|
1222
|
+
token: 'token-a',
|
|
1223
|
+
fiveHourUtilization: 0,
|
|
1224
|
+
blocked: false,
|
|
1225
|
+
rejected: false,
|
|
1226
|
+
modelWeeklyLimits: {},
|
|
1227
|
+
},
|
|
1228
|
+
{
|
|
1229
|
+
name: 'token-b',
|
|
1230
|
+
token: 'token-b',
|
|
1231
|
+
fiveHourUtilization: 0,
|
|
1232
|
+
blocked: false,
|
|
1233
|
+
rejected: false,
|
|
1234
|
+
modelWeeklyLimits: {},
|
|
1235
|
+
},
|
|
1236
|
+
]);
|
|
1237
|
+
|
|
1238
|
+
await useCase.run({
|
|
1239
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1240
|
+
defaultAgentName: 'agent1',
|
|
1241
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1242
|
+
defaultLlmAgentName: null,
|
|
1243
|
+
configFilePath: '/path/to/config.yml',
|
|
1244
|
+
maximumPreparingIssuesCount: null,
|
|
1245
|
+
utilizationPercentageThreshold: 90,
|
|
1246
|
+
allowedIssueAuthors: null,
|
|
1247
|
+
codexHomeCandidates: null,
|
|
1248
|
+
allowIssueCacheMinutes: 0,
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(12);
|
|
1252
|
+
const spawnedTokens = mockLocalCommandRunner.runCommand.mock.calls.map(
|
|
1253
|
+
(call) => call[2]?.env?.CLAUDE_CODE_OAUTH_TOKEN,
|
|
1254
|
+
);
|
|
1255
|
+
expect(spawnedTokens.filter((token) => token === 'token-a')).toHaveLength(
|
|
1256
|
+
6,
|
|
1257
|
+
);
|
|
1258
|
+
expect(spawnedTokens.filter((token) => token === 'token-b')).toHaveLength(
|
|
1259
|
+
6,
|
|
1260
|
+
);
|
|
1261
|
+
});
|
|
1262
|
+
it('should cap configured maximumPreparingIssuesCount to six per available Claude OAuth token', async () => {
|
|
1263
|
+
const awaitingIssues: Issue[] = Array.from({ length: 20 }, (_, i) =>
|
|
1264
|
+
createMockIssue({
|
|
1265
|
+
url: `url${i + 1}`,
|
|
1266
|
+
title: `Issue ${i + 1}`,
|
|
1267
|
+
labels: [],
|
|
1268
|
+
status: 'Awaiting Workspace',
|
|
1269
|
+
number: i + 1,
|
|
1270
|
+
itemId: `item-${i + 1}`,
|
|
1271
|
+
}),
|
|
1272
|
+
);
|
|
1273
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1274
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1275
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
1276
|
+
);
|
|
1277
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
1278
|
+
stdout: '',
|
|
1279
|
+
stderr: '',
|
|
1280
|
+
exitCode: 0,
|
|
1281
|
+
});
|
|
1282
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
1283
|
+
{
|
|
1284
|
+
name: 'token-a',
|
|
1285
|
+
token: 'token-a',
|
|
1286
|
+
fiveHourUtilization: 0,
|
|
1287
|
+
blocked: false,
|
|
1288
|
+
rejected: false,
|
|
1289
|
+
modelWeeklyLimits: {},
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
name: 'token-b',
|
|
1293
|
+
token: 'token-b',
|
|
1294
|
+
fiveHourUtilization: 0,
|
|
1295
|
+
blocked: false,
|
|
1296
|
+
rejected: false,
|
|
1297
|
+
modelWeeklyLimits: {},
|
|
1298
|
+
},
|
|
1299
|
+
]);
|
|
1300
|
+
|
|
1301
|
+
await useCase.run({
|
|
1302
|
+
projectUrl: 'https://github.com/user/repo',
|
|
1303
|
+
defaultAgentName: 'agent1',
|
|
1304
|
+
defaultLlmModelName: 'claude-sonnet-4-6',
|
|
1305
|
+
defaultLlmAgentName: null,
|
|
1306
|
+
configFilePath: '/path/to/config.yml',
|
|
1307
|
+
maximumPreparingIssuesCount: 20,
|
|
1308
|
+
utilizationPercentageThreshold: 90,
|
|
1309
|
+
allowedIssueAuthors: null,
|
|
1310
|
+
codexHomeCandidates: null,
|
|
1311
|
+
allowIssueCacheMinutes: 0,
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(12);
|
|
1315
|
+
});
|
|
1190
1316
|
|
|
1191
1317
|
it('should not skip issues from repositories with workflow blockers', async () => {
|
|
1192
1318
|
const blockerIssue = createMockIssue({
|
|
@@ -2149,6 +2275,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2149
2275
|
});
|
|
2150
2276
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2151
2277
|
{
|
|
2278
|
+
name: 'token-a',
|
|
2152
2279
|
token: 'token-a',
|
|
2153
2280
|
fiveHourUtilization: 0,
|
|
2154
2281
|
blocked: false,
|
|
@@ -2223,6 +2350,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2223
2350
|
});
|
|
2224
2351
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2225
2352
|
{
|
|
2353
|
+
name: 'token-a',
|
|
2226
2354
|
token: 'token-a',
|
|
2227
2355
|
fiveHourUtilization: 0,
|
|
2228
2356
|
blocked: false,
|
|
@@ -2230,6 +2358,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2230
2358
|
modelWeeklyLimits: {},
|
|
2231
2359
|
},
|
|
2232
2360
|
{
|
|
2361
|
+
name: 'token-b',
|
|
2233
2362
|
token: 'token-b',
|
|
2234
2363
|
fiveHourUtilization: 0,
|
|
2235
2364
|
blocked: false,
|
|
@@ -2334,6 +2463,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2334
2463
|
});
|
|
2335
2464
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2336
2465
|
{
|
|
2466
|
+
name: 'token-high',
|
|
2337
2467
|
token: 'token-high',
|
|
2338
2468
|
fiveHourUtilization: 0.8,
|
|
2339
2469
|
blocked: false,
|
|
@@ -2341,6 +2471,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2341
2471
|
modelWeeklyLimits: {},
|
|
2342
2472
|
},
|
|
2343
2473
|
{
|
|
2474
|
+
name: 'token-low',
|
|
2344
2475
|
token: 'token-low',
|
|
2345
2476
|
fiveHourUtilization: 0.1,
|
|
2346
2477
|
blocked: false,
|
|
@@ -2391,6 +2522,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2391
2522
|
});
|
|
2392
2523
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2393
2524
|
{
|
|
2525
|
+
name: 'token-blocked',
|
|
2394
2526
|
token: 'token-blocked',
|
|
2395
2527
|
fiveHourUtilization: 0.05,
|
|
2396
2528
|
blocked: true,
|
|
@@ -2398,6 +2530,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2398
2530
|
modelWeeklyLimits: {},
|
|
2399
2531
|
},
|
|
2400
2532
|
{
|
|
2533
|
+
name: 'token-ok',
|
|
2401
2534
|
token: 'token-ok',
|
|
2402
2535
|
fiveHourUtilization: 0.5,
|
|
2403
2536
|
blocked: false,
|
|
@@ -2448,6 +2581,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2448
2581
|
});
|
|
2449
2582
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2450
2583
|
{
|
|
2584
|
+
name: 'token-a',
|
|
2451
2585
|
token: 'token-a',
|
|
2452
2586
|
fiveHourUtilization: 0.05,
|
|
2453
2587
|
blocked: true,
|
|
@@ -2455,6 +2589,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2455
2589
|
modelWeeklyLimits: {},
|
|
2456
2590
|
},
|
|
2457
2591
|
{
|
|
2592
|
+
name: 'token-b',
|
|
2458
2593
|
token: 'token-b',
|
|
2459
2594
|
fiveHourUtilization: 0.08,
|
|
2460
2595
|
blocked: true,
|
|
@@ -2491,7 +2626,72 @@ describe('StartPreparationUseCase', () => {
|
|
|
2491
2626
|
consoleWarnSpy.mockRestore();
|
|
2492
2627
|
});
|
|
2493
2628
|
|
|
2494
|
-
it('should
|
|
2629
|
+
it('should skip preparation when every configured token is at or above 95 percent 5h utilization', async () => {
|
|
2630
|
+
const awaitingIssue = createMockIssue({
|
|
2631
|
+
url: 'url1',
|
|
2632
|
+
title: 'Issue 1',
|
|
2633
|
+
labels: ['category:impl'],
|
|
2634
|
+
status: 'Awaiting Workspace',
|
|
2635
|
+
number: 1,
|
|
2636
|
+
itemId: 'item-1',
|
|
2637
|
+
});
|
|
2638
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2639
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2640
|
+
createMockStoryObjectMap([awaitingIssue]),
|
|
2641
|
+
);
|
|
2642
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2643
|
+
stdout: '',
|
|
2644
|
+
stderr: '',
|
|
2645
|
+
exitCode: 0,
|
|
2646
|
+
});
|
|
2647
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2648
|
+
{
|
|
2649
|
+
name: 'token-a',
|
|
2650
|
+
token: 'token-a',
|
|
2651
|
+
fiveHourUtilization: 0.95,
|
|
2652
|
+
blocked: false,
|
|
2653
|
+
rejected: false,
|
|
2654
|
+
modelWeeklyLimits: {},
|
|
2655
|
+
},
|
|
2656
|
+
{
|
|
2657
|
+
name: 'token-b',
|
|
2658
|
+
token: 'token-b',
|
|
2659
|
+
fiveHourUtilization: 0.97,
|
|
2660
|
+
blocked: false,
|
|
2661
|
+
rejected: false,
|
|
2662
|
+
modelWeeklyLimits: {},
|
|
2663
|
+
},
|
|
2664
|
+
]);
|
|
2665
|
+
const consoleWarnSpy = jest
|
|
2666
|
+
.spyOn(console, 'warn')
|
|
2667
|
+
.mockImplementation(() => {});
|
|
2668
|
+
|
|
2669
|
+
await useCase.run({
|
|
2670
|
+
projectUrl: 'https://github.com/user/repo',
|
|
2671
|
+
defaultAgentName: 'agent1',
|
|
2672
|
+
defaultLlmModelName: 'claude-opus',
|
|
2673
|
+
defaultLlmAgentName: null,
|
|
2674
|
+
configFilePath: '/path/to/config.yml',
|
|
2675
|
+
maximumPreparingIssuesCount: null,
|
|
2676
|
+
utilizationPercentageThreshold: 90,
|
|
2677
|
+
allowedIssueAuthors: null,
|
|
2678
|
+
codexHomeCandidates: null,
|
|
2679
|
+
allowIssueCacheMinutes: 0,
|
|
2680
|
+
});
|
|
2681
|
+
|
|
2682
|
+
expect(
|
|
2683
|
+
mockClaudeTokenUsageRepository.ensureObservable.mock.calls,
|
|
2684
|
+
).toHaveLength(0);
|
|
2685
|
+
expect(mockProjectRepository.getByUrl).not.toHaveBeenCalled();
|
|
2686
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
2687
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
2688
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
2689
|
+
expect.stringContaining('5h utilization >= 95%'),
|
|
2690
|
+
);
|
|
2691
|
+
consoleWarnSpy.mockRestore();
|
|
2692
|
+
});
|
|
2693
|
+
|
|
2694
|
+
it('should return all tokens sorted ascending when all have full process capacity', async () => {
|
|
2495
2695
|
const awaitingIssues: Issue[] = [
|
|
2496
2696
|
createMockIssue({
|
|
2497
2697
|
url: 'url1',
|
|
@@ -2529,6 +2729,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2529
2729
|
});
|
|
2530
2730
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2531
2731
|
{
|
|
2732
|
+
name: 'token-mid',
|
|
2532
2733
|
token: 'token-mid',
|
|
2533
2734
|
fiveHourUtilization: 0.5,
|
|
2534
2735
|
blocked: false,
|
|
@@ -2536,6 +2737,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2536
2737
|
modelWeeklyLimits: {},
|
|
2537
2738
|
},
|
|
2538
2739
|
{
|
|
2740
|
+
name: 'token-low',
|
|
2539
2741
|
token: 'token-low',
|
|
2540
2742
|
fiveHourUtilization: 0.1,
|
|
2541
2743
|
blocked: false,
|
|
@@ -2543,6 +2745,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2543
2745
|
modelWeeklyLimits: {},
|
|
2544
2746
|
},
|
|
2545
2747
|
{
|
|
2748
|
+
name: 'token-high',
|
|
2546
2749
|
token: 'token-high',
|
|
2547
2750
|
fiveHourUtilization: 0.8,
|
|
2548
2751
|
blocked: false,
|
|
@@ -2576,18 +2779,20 @@ describe('StartPreparationUseCase', () => {
|
|
|
2576
2779
|
});
|
|
2577
2780
|
});
|
|
2578
2781
|
|
|
2579
|
-
it('should
|
|
2580
|
-
const
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2782
|
+
it('should reduce token process capacity exponentially above 80 percent 5h utilization', async () => {
|
|
2783
|
+
const awaitingIssues: Issue[] = Array.from({ length: 10 }, (_, i) =>
|
|
2784
|
+
createMockIssue({
|
|
2785
|
+
url: `url${i + 1}`,
|
|
2786
|
+
title: `Issue ${i + 1}`,
|
|
2787
|
+
labels: ['category:impl'],
|
|
2788
|
+
status: 'Awaiting Workspace',
|
|
2789
|
+
number: i + 1,
|
|
2790
|
+
itemId: `item-${i + 1}`,
|
|
2791
|
+
}),
|
|
2792
|
+
);
|
|
2588
2793
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2589
2794
|
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2590
|
-
createMockStoryObjectMap(
|
|
2795
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
2591
2796
|
);
|
|
2592
2797
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2593
2798
|
stdout: '',
|
|
@@ -2596,15 +2801,33 @@ describe('StartPreparationUseCase', () => {
|
|
|
2596
2801
|
});
|
|
2597
2802
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2598
2803
|
{
|
|
2599
|
-
|
|
2804
|
+
name: 'token-full-capacity',
|
|
2805
|
+
token: 'token-full-capacity',
|
|
2806
|
+
fiveHourUtilization: 0.8,
|
|
2807
|
+
blocked: false,
|
|
2808
|
+
rejected: false,
|
|
2809
|
+
modelWeeklyLimits: {},
|
|
2810
|
+
},
|
|
2811
|
+
{
|
|
2812
|
+
name: 'token-two-slots',
|
|
2813
|
+
token: 'token-two-slots',
|
|
2814
|
+
fiveHourUtilization: 0.85,
|
|
2815
|
+
blocked: false,
|
|
2816
|
+
rejected: false,
|
|
2817
|
+
modelWeeklyLimits: {},
|
|
2818
|
+
},
|
|
2819
|
+
{
|
|
2820
|
+
name: 'token-one-slot',
|
|
2821
|
+
token: 'token-one-slot',
|
|
2600
2822
|
fiveHourUtilization: 0.9,
|
|
2601
2823
|
blocked: false,
|
|
2602
2824
|
rejected: false,
|
|
2603
2825
|
modelWeeklyLimits: {},
|
|
2604
2826
|
},
|
|
2605
2827
|
{
|
|
2606
|
-
|
|
2607
|
-
|
|
2828
|
+
name: 'token-zero-slots',
|
|
2829
|
+
token: 'token-zero-slots',
|
|
2830
|
+
fiveHourUtilization: 0.95,
|
|
2608
2831
|
blocked: false,
|
|
2609
2832
|
rejected: false,
|
|
2610
2833
|
modelWeeklyLimits: {},
|
|
@@ -2624,13 +2847,20 @@ describe('StartPreparationUseCase', () => {
|
|
|
2624
2847
|
allowIssueCacheMinutes: 0,
|
|
2625
2848
|
});
|
|
2626
2849
|
|
|
2627
|
-
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(
|
|
2628
|
-
|
|
2629
|
-
env
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2850
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(9);
|
|
2851
|
+
const spawnedTokens = mockLocalCommandRunner.runCommand.mock.calls.map(
|
|
2852
|
+
(call) => call[2]?.env?.CLAUDE_CODE_OAUTH_TOKEN,
|
|
2853
|
+
);
|
|
2854
|
+
expect(
|
|
2855
|
+
spawnedTokens.filter((token) => token === 'token-full-capacity'),
|
|
2856
|
+
).toHaveLength(6);
|
|
2857
|
+
expect(
|
|
2858
|
+
spawnedTokens.filter((token) => token === 'token-two-slots'),
|
|
2859
|
+
).toHaveLength(2);
|
|
2860
|
+
expect(
|
|
2861
|
+
spawnedTokens.filter((token) => token === 'token-one-slot'),
|
|
2862
|
+
).toHaveLength(1);
|
|
2863
|
+
expect(spawnedTokens).not.toContain('token-zero-slots');
|
|
2634
2864
|
});
|
|
2635
2865
|
|
|
2636
2866
|
it('should exclude a rejected token from rotation', async () => {
|
|
@@ -2653,6 +2883,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2653
2883
|
});
|
|
2654
2884
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2655
2885
|
{
|
|
2886
|
+
name: 'token-rejected',
|
|
2656
2887
|
token: 'token-rejected',
|
|
2657
2888
|
fiveHourUtilization: 0.1,
|
|
2658
2889
|
blocked: false,
|
|
@@ -2660,6 +2891,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2660
2891
|
modelWeeklyLimits: {},
|
|
2661
2892
|
},
|
|
2662
2893
|
{
|
|
2894
|
+
name: 'token-ok',
|
|
2663
2895
|
token: 'token-ok',
|
|
2664
2896
|
fiveHourUtilization: 0.5,
|
|
2665
2897
|
blocked: false,
|
|
@@ -2710,6 +2942,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2710
2942
|
});
|
|
2711
2943
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2712
2944
|
{
|
|
2945
|
+
name: 'token-reset',
|
|
2713
2946
|
token: 'token-reset',
|
|
2714
2947
|
fiveHourUtilization: 0,
|
|
2715
2948
|
blocked: false,
|
|
@@ -2717,6 +2950,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2717
2950
|
modelWeeklyLimits: {},
|
|
2718
2951
|
},
|
|
2719
2952
|
{
|
|
2953
|
+
name: 'token-busy',
|
|
2720
2954
|
token: 'token-busy',
|
|
2721
2955
|
fiveHourUtilization: 0.5,
|
|
2722
2956
|
blocked: false,
|
|
@@ -2767,6 +3001,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2767
3001
|
});
|
|
2768
3002
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2769
3003
|
{
|
|
3004
|
+
name: 'token-saturated',
|
|
2770
3005
|
token: 'token-saturated',
|
|
2771
3006
|
fiveHourUtilization: 0.95,
|
|
2772
3007
|
blocked: false,
|
|
@@ -2774,6 +3009,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2774
3009
|
modelWeeklyLimits: {},
|
|
2775
3010
|
},
|
|
2776
3011
|
{
|
|
3012
|
+
name: 'token-ok',
|
|
2777
3013
|
token: 'token-ok',
|
|
2778
3014
|
fiveHourUtilization: 0.2,
|
|
2779
3015
|
blocked: false,
|
|
@@ -2824,6 +3060,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2824
3060
|
});
|
|
2825
3061
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2826
3062
|
{
|
|
3063
|
+
name: 'token-a',
|
|
2827
3064
|
token: 'token-a',
|
|
2828
3065
|
fiveHourUtilization: 0.1,
|
|
2829
3066
|
blocked: false,
|
|
@@ -2831,6 +3068,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2831
3068
|
modelWeeklyLimits: {},
|
|
2832
3069
|
},
|
|
2833
3070
|
{
|
|
3071
|
+
name: 'token-b',
|
|
2834
3072
|
token: 'token-b',
|
|
2835
3073
|
fiveHourUtilization: 0.2,
|
|
2836
3074
|
blocked: false,
|
|
@@ -2931,6 +3169,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2931
3169
|
const futureReset = Math.floor(Date.now() / 1000) + 3600;
|
|
2932
3170
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2933
3171
|
{
|
|
3172
|
+
name: 'token-sonnet-exhausted',
|
|
2934
3173
|
token: 'token-sonnet-exhausted',
|
|
2935
3174
|
fiveHourUtilization: 0.1,
|
|
2936
3175
|
blocked: false,
|
|
@@ -2940,6 +3179,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2940
3179
|
},
|
|
2941
3180
|
},
|
|
2942
3181
|
{
|
|
3182
|
+
name: 'token-ok',
|
|
2943
3183
|
token: 'token-ok',
|
|
2944
3184
|
fiveHourUtilization: 0.5,
|
|
2945
3185
|
blocked: false,
|
|
@@ -2991,6 +3231,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2991
3231
|
const pastReset = Math.floor(Date.now() / 1000) - 3600;
|
|
2992
3232
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2993
3233
|
{
|
|
3234
|
+
name: 'token-recovered',
|
|
2994
3235
|
token: 'token-recovered',
|
|
2995
3236
|
fiveHourUtilization: 0.1,
|
|
2996
3237
|
blocked: false,
|
|
@@ -3000,6 +3241,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
3000
3241
|
},
|
|
3001
3242
|
},
|
|
3002
3243
|
{
|
|
3244
|
+
name: 'token-busy',
|
|
3003
3245
|
token: 'token-busy',
|
|
3004
3246
|
fiveHourUtilization: 0.5,
|
|
3005
3247
|
blocked: false,
|
|
@@ -3051,6 +3293,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
3051
3293
|
const futureReset = Math.floor(Date.now() / 1000) + 3600;
|
|
3052
3294
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
3053
3295
|
{
|
|
3296
|
+
name: 'token-sonnet-exhausted',
|
|
3054
3297
|
token: 'token-sonnet-exhausted',
|
|
3055
3298
|
fiveHourUtilization: 0.1,
|
|
3056
3299
|
blocked: false,
|
|
@@ -3060,6 +3303,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
3060
3303
|
},
|
|
3061
3304
|
},
|
|
3062
3305
|
{
|
|
3306
|
+
name: 'token-higher-util',
|
|
3063
3307
|
token: 'token-higher-util',
|
|
3064
3308
|
fiveHourUtilization: 0.5,
|
|
3065
3309
|
blocked: false,
|
|
@@ -3111,6 +3355,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
3111
3355
|
const futureReset = Math.floor(Date.now() / 1000) + 3600;
|
|
3112
3356
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
3113
3357
|
{
|
|
3358
|
+
name: 'token-weekly-exhausted',
|
|
3114
3359
|
token: 'token-weekly-exhausted',
|
|
3115
3360
|
fiveHourUtilization: 0.1,
|
|
3116
3361
|
blocked: false,
|
|
@@ -3120,6 +3365,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
3120
3365
|
},
|
|
3121
3366
|
},
|
|
3122
3367
|
{
|
|
3368
|
+
name: 'token-ok',
|
|
3123
3369
|
token: 'token-ok',
|
|
3124
3370
|
fiveHourUtilization: 0.5,
|
|
3125
3371
|
blocked: false,
|
|
@@ -3150,3 +3396,140 @@ describe('StartPreparationUseCase', () => {
|
|
|
3150
3396
|
});
|
|
3151
3397
|
});
|
|
3152
3398
|
});
|
|
3399
|
+
|
|
3400
|
+
describe('StartPreparationUseCase.buildRotationOrder', () => {
|
|
3401
|
+
const mockProjectRepositoryForRotation: Mocked<
|
|
3402
|
+
Pick<ProjectRepository, 'getByUrl'>
|
|
3403
|
+
> = {
|
|
3404
|
+
getByUrl: jest.fn(),
|
|
3405
|
+
};
|
|
3406
|
+
const mockIssueRepositoryForRotation: Mocked<
|
|
3407
|
+
Pick<
|
|
3408
|
+
IssueRepository,
|
|
3409
|
+
| 'getStoryObjectMap'
|
|
3410
|
+
| 'updateStatus'
|
|
3411
|
+
| 'findRelatedOpenPRs'
|
|
3412
|
+
| 'getOpenPullRequest'
|
|
3413
|
+
| 'closePullRequest'
|
|
3414
|
+
| 'deletePullRequestBranch'
|
|
3415
|
+
| 'createCommentByUrl'
|
|
3416
|
+
>
|
|
3417
|
+
> = {
|
|
3418
|
+
getStoryObjectMap: jest.fn(),
|
|
3419
|
+
updateStatus: jest.fn(),
|
|
3420
|
+
findRelatedOpenPRs: jest.fn(),
|
|
3421
|
+
getOpenPullRequest: jest.fn(),
|
|
3422
|
+
closePullRequest: jest.fn(),
|
|
3423
|
+
deletePullRequestBranch: jest.fn(),
|
|
3424
|
+
createCommentByUrl: jest.fn(),
|
|
3425
|
+
};
|
|
3426
|
+
const mockLocalCommandRunnerForRotation: Mocked<LocalCommandRunner> = {
|
|
3427
|
+
runCommand: jest.fn(),
|
|
3428
|
+
};
|
|
3429
|
+
const mockClaudeTokenUsageRepositoryForRotation: Mocked<ClaudeTokenUsageRepository> =
|
|
3430
|
+
{
|
|
3431
|
+
ensureObservable: jest.fn(),
|
|
3432
|
+
getAvailableTokenUsages: jest.fn(),
|
|
3433
|
+
proxyBaseUrl: jest.fn(),
|
|
3434
|
+
};
|
|
3435
|
+
|
|
3436
|
+
const useCase = new StartPreparationUseCase(
|
|
3437
|
+
mockProjectRepositoryForRotation,
|
|
3438
|
+
mockIssueRepositoryForRotation,
|
|
3439
|
+
mockLocalCommandRunnerForRotation,
|
|
3440
|
+
mockClaudeTokenUsageRepositoryForRotation,
|
|
3441
|
+
);
|
|
3442
|
+
|
|
3443
|
+
it('lists selected tokens first in ascending utilization order then excluded tokens', () => {
|
|
3444
|
+
const tokenUsages = [
|
|
3445
|
+
{
|
|
3446
|
+
name: 'high-util',
|
|
3447
|
+
token: 'sk-ant-high',
|
|
3448
|
+
fiveHourUtilization: 0.8,
|
|
3449
|
+
blocked: false,
|
|
3450
|
+
rejected: false,
|
|
3451
|
+
modelWeeklyLimits: {},
|
|
3452
|
+
},
|
|
3453
|
+
{
|
|
3454
|
+
name: 'low-util',
|
|
3455
|
+
token: 'sk-ant-low',
|
|
3456
|
+
fiveHourUtilization: 0.1,
|
|
3457
|
+
blocked: false,
|
|
3458
|
+
rejected: false,
|
|
3459
|
+
modelWeeklyLimits: {},
|
|
3460
|
+
},
|
|
3461
|
+
{
|
|
3462
|
+
name: 'blocked-token',
|
|
3463
|
+
token: 'sk-ant-blocked',
|
|
3464
|
+
fiveHourUtilization: 0.0,
|
|
3465
|
+
blocked: true,
|
|
3466
|
+
rejected: false,
|
|
3467
|
+
modelWeeklyLimits: {},
|
|
3468
|
+
},
|
|
3469
|
+
];
|
|
3470
|
+
const result = useCase.buildRotationOrder(tokenUsages, 90, null);
|
|
3471
|
+
|
|
3472
|
+
expect(result[0].name).toBe('low-util');
|
|
3473
|
+
expect(result[1].name).toBe('high-util');
|
|
3474
|
+
expect(result[2].name).toBe('blocked-token');
|
|
3475
|
+
expect(result[2].blocked).toBe(true);
|
|
3476
|
+
expect(result[2].thresholdExcluded).toBe(false);
|
|
3477
|
+
});
|
|
3478
|
+
|
|
3479
|
+
it('does not include raw token strings in output entries', () => {
|
|
3480
|
+
const tokenUsages = [
|
|
3481
|
+
{
|
|
3482
|
+
name: 'my-token',
|
|
3483
|
+
token: 'sk-ant-secret-value',
|
|
3484
|
+
fiveHourUtilization: 0.1,
|
|
3485
|
+
blocked: false,
|
|
3486
|
+
rejected: false,
|
|
3487
|
+
modelWeeklyLimits: {},
|
|
3488
|
+
},
|
|
3489
|
+
];
|
|
3490
|
+
const result = useCase.buildRotationOrder(tokenUsages, 90, null);
|
|
3491
|
+
const serialized = JSON.stringify(result);
|
|
3492
|
+
|
|
3493
|
+
expect(serialized).not.toContain('sk-ant-secret-value');
|
|
3494
|
+
expect(result[0].name).toBe('my-token');
|
|
3495
|
+
});
|
|
3496
|
+
|
|
3497
|
+
it('marks thresholdExcluded true when token is at or above 95 percent utilization', () => {
|
|
3498
|
+
const tokenUsages = [
|
|
3499
|
+
{
|
|
3500
|
+
name: 'over-threshold',
|
|
3501
|
+
token: 'sk-ant-over',
|
|
3502
|
+
fiveHourUtilization: 0.95,
|
|
3503
|
+
blocked: false,
|
|
3504
|
+
rejected: false,
|
|
3505
|
+
modelWeeklyLimits: {},
|
|
3506
|
+
},
|
|
3507
|
+
];
|
|
3508
|
+
const result = useCase.buildRotationOrder(tokenUsages, 90, null);
|
|
3509
|
+
|
|
3510
|
+
expect(result).toHaveLength(1);
|
|
3511
|
+
expect(result[0].thresholdExcluded).toBe(true);
|
|
3512
|
+
expect(result[0].blocked).toBe(false);
|
|
3513
|
+
expect(result[0].rejected).toBe(false);
|
|
3514
|
+
});
|
|
3515
|
+
|
|
3516
|
+
it('does not mark thresholdExcluded for tokens in the 90 to 94 percent utilization range because selectRotationTokens still assigns them slots', () => {
|
|
3517
|
+
const tokenUsages = [
|
|
3518
|
+
{
|
|
3519
|
+
name: 'mid-util',
|
|
3520
|
+
token: 'sk-ant-mid',
|
|
3521
|
+
fiveHourUtilization: 0.92,
|
|
3522
|
+
blocked: false,
|
|
3523
|
+
rejected: false,
|
|
3524
|
+
modelWeeklyLimits: {},
|
|
3525
|
+
},
|
|
3526
|
+
];
|
|
3527
|
+
const result = useCase.buildRotationOrder(tokenUsages, 90, null);
|
|
3528
|
+
|
|
3529
|
+
expect(result).toHaveLength(1);
|
|
3530
|
+
expect(result[0].thresholdExcluded).toBe(false);
|
|
3531
|
+
expect(result[0].blocked).toBe(false);
|
|
3532
|
+
expect(result[0].rejected).toBe(false);
|
|
3533
|
+
expect(result[0].fiveHourUtilization).toBe(0.92);
|
|
3534
|
+
});
|
|
3535
|
+
});
|