github-issue-tower-defence-management 1.67.6 → 1.69.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/CHANGELOG.md +14 -0
- package/README.md +6 -1
- package/bin/adapter/entry-points/cli/index.js +1 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +4 -0
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +20 -8
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +29 -2
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +2 -0
- package/src/adapter/entry-points/cli/index.ts +1 -0
- package/src/adapter/entry-points/cli/projectConfig.ts +6 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +3 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +556 -31
- package/src/domain/usecases/StartPreparationUseCase.ts +66 -2
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +3 -0
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
7
7
|
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
8
8
|
import { ClaudeTokenUsageRepository } from './adapter-interfaces/ClaudeTokenUsageRepository';
|
|
9
|
+
import { ClaudeTokenUsage } from '../entities/ClaudeTokenUsage';
|
|
9
10
|
import { Issue } from '../entities/Issue';
|
|
10
11
|
import { Project } from '../entities/Project';
|
|
11
12
|
import { StoryObjectMap } from '../entities/StoryObjectMap';
|
|
@@ -152,6 +153,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
152
153
|
allowedIssueAuthors: null,
|
|
153
154
|
codexHomeCandidates: null,
|
|
154
155
|
allowIssueCacheMinutes: 0,
|
|
156
|
+
labelsAsLlmAgentName: null,
|
|
155
157
|
});
|
|
156
158
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
157
159
|
expect(mockIssueRepository.updateStatus.mock.calls[0][0]).toBe(mockProject);
|
|
@@ -216,6 +218,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
216
218
|
allowedIssueAuthors: null,
|
|
217
219
|
codexHomeCandidates: null,
|
|
218
220
|
allowIssueCacheMinutes: 0,
|
|
221
|
+
labelsAsLlmAgentName: null,
|
|
219
222
|
});
|
|
220
223
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
221
224
|
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
@@ -272,6 +275,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
272
275
|
allowedIssueAuthors: null,
|
|
273
276
|
codexHomeCandidates: null,
|
|
274
277
|
allowIssueCacheMinutes: 0,
|
|
278
|
+
labelsAsLlmAgentName: null,
|
|
275
279
|
});
|
|
276
280
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
277
281
|
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
@@ -319,6 +323,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
319
323
|
allowedIssueAuthors: null,
|
|
320
324
|
codexHomeCandidates: null,
|
|
321
325
|
allowIssueCacheMinutes: 0,
|
|
326
|
+
labelsAsLlmAgentName: null,
|
|
322
327
|
});
|
|
323
328
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
324
329
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
@@ -367,6 +372,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
367
372
|
allowedIssueAuthors: null,
|
|
368
373
|
codexHomeCandidates: null,
|
|
369
374
|
allowIssueCacheMinutes: 0,
|
|
375
|
+
labelsAsLlmAgentName: null,
|
|
370
376
|
});
|
|
371
377
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
372
378
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
@@ -414,6 +420,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
414
420
|
allowedIssueAuthors: null,
|
|
415
421
|
codexHomeCandidates: null,
|
|
416
422
|
allowIssueCacheMinutes: 0,
|
|
423
|
+
labelsAsLlmAgentName: null,
|
|
417
424
|
});
|
|
418
425
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
419
426
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
@@ -479,6 +486,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
479
486
|
allowedIssueAuthors: null,
|
|
480
487
|
codexHomeCandidates: null,
|
|
481
488
|
allowIssueCacheMinutes: 0,
|
|
489
|
+
labelsAsLlmAgentName: null,
|
|
482
490
|
});
|
|
483
491
|
expect(mockIssueRepository.closePullRequest).toHaveBeenCalledWith(
|
|
484
492
|
newerPR.url,
|
|
@@ -567,6 +575,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
567
575
|
allowedIssueAuthors: null,
|
|
568
576
|
codexHomeCandidates: null,
|
|
569
577
|
allowIssueCacheMinutes: 0,
|
|
578
|
+
labelsAsLlmAgentName: null,
|
|
570
579
|
});
|
|
571
580
|
expect(mockIssueRepository.closePullRequest).toHaveBeenCalledWith(
|
|
572
581
|
newerPR.url,
|
|
@@ -629,6 +638,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
629
638
|
allowedIssueAuthors: null,
|
|
630
639
|
codexHomeCandidates: null,
|
|
631
640
|
allowIssueCacheMinutes: 0,
|
|
641
|
+
labelsAsLlmAgentName: null,
|
|
632
642
|
});
|
|
633
643
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
634
644
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
@@ -672,6 +682,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
672
682
|
allowedIssueAuthors: null,
|
|
673
683
|
codexHomeCandidates: null,
|
|
674
684
|
allowIssueCacheMinutes: 0,
|
|
685
|
+
labelsAsLlmAgentName: null,
|
|
675
686
|
});
|
|
676
687
|
// Both awaiting issues should be updated (forward iteration: url1 first, then url2)
|
|
677
688
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(2);
|
|
@@ -728,6 +739,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
728
739
|
allowedIssueAuthors: null,
|
|
729
740
|
codexHomeCandidates: null,
|
|
730
741
|
allowIssueCacheMinutes: 0,
|
|
742
|
+
labelsAsLlmAgentName: null,
|
|
731
743
|
});
|
|
732
744
|
// Loop doesn't run because we're already at max (6 >= 6)
|
|
733
745
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
@@ -762,6 +774,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
762
774
|
allowedIssueAuthors: null,
|
|
763
775
|
codexHomeCandidates: null,
|
|
764
776
|
allowIssueCacheMinutes: 0,
|
|
777
|
+
labelsAsLlmAgentName: null,
|
|
765
778
|
});
|
|
766
779
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
767
780
|
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
@@ -806,6 +819,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
806
819
|
allowedIssueAuthors: null,
|
|
807
820
|
codexHomeCandidates: null,
|
|
808
821
|
allowIssueCacheMinutes: 0,
|
|
822
|
+
labelsAsLlmAgentName: null,
|
|
809
823
|
});
|
|
810
824
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
811
825
|
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
@@ -850,6 +864,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
850
864
|
allowedIssueAuthors: null,
|
|
851
865
|
codexHomeCandidates: null,
|
|
852
866
|
allowIssueCacheMinutes: 0,
|
|
867
|
+
labelsAsLlmAgentName: null,
|
|
853
868
|
});
|
|
854
869
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
855
870
|
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
@@ -894,6 +909,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
894
909
|
allowedIssueAuthors: null,
|
|
895
910
|
codexHomeCandidates: null,
|
|
896
911
|
allowIssueCacheMinutes: 0,
|
|
912
|
+
labelsAsLlmAgentName: null,
|
|
897
913
|
});
|
|
898
914
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
899
915
|
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
@@ -938,6 +954,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
938
954
|
allowedIssueAuthors: null,
|
|
939
955
|
codexHomeCandidates: null,
|
|
940
956
|
allowIssueCacheMinutes: 0,
|
|
957
|
+
labelsAsLlmAgentName: null,
|
|
941
958
|
});
|
|
942
959
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
943
960
|
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
@@ -982,6 +999,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
982
999
|
allowedIssueAuthors: null,
|
|
983
1000
|
codexHomeCandidates: null,
|
|
984
1001
|
allowIssueCacheMinutes: 0,
|
|
1002
|
+
labelsAsLlmAgentName: null,
|
|
985
1003
|
});
|
|
986
1004
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
987
1005
|
expect(mockLocalCommandRunner.runCommand.mock.calls[0]).toEqual([
|
|
@@ -1029,6 +1047,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1029
1047
|
allowedIssueAuthors: null,
|
|
1030
1048
|
codexHomeCandidates: null,
|
|
1031
1049
|
allowIssueCacheMinutes: 0,
|
|
1050
|
+
labelsAsLlmAgentName: null,
|
|
1032
1051
|
});
|
|
1033
1052
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1034
1053
|
'No LLM model configured for issue url1. Provide --defaultLlmModelName or add an llm-model: label.',
|
|
@@ -1076,6 +1095,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1076
1095
|
allowedIssueAuthors: null,
|
|
1077
1096
|
codexHomeCandidates: null,
|
|
1078
1097
|
allowIssueCacheMinutes: 0,
|
|
1098
|
+
labelsAsLlmAgentName: null,
|
|
1079
1099
|
});
|
|
1080
1100
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1081
1101
|
'No LLM model configured for issue url1. Provide --defaultLlmModelName or add an llm-model: label.',
|
|
@@ -1125,6 +1145,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1125
1145
|
allowedIssueAuthors: null,
|
|
1126
1146
|
codexHomeCandidates: null,
|
|
1127
1147
|
allowIssueCacheMinutes: 0,
|
|
1148
|
+
labelsAsLlmAgentName: null,
|
|
1128
1149
|
});
|
|
1129
1150
|
// No issues are in 'Awaiting Workspace' status, so no updates should happen
|
|
1130
1151
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
@@ -1159,6 +1180,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1159
1180
|
allowedIssueAuthors: null,
|
|
1160
1181
|
codexHomeCandidates: null,
|
|
1161
1182
|
allowIssueCacheMinutes: 0,
|
|
1183
|
+
labelsAsLlmAgentName: null,
|
|
1162
1184
|
});
|
|
1163
1185
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(3);
|
|
1164
1186
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(3);
|
|
@@ -1192,6 +1214,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1192
1214
|
allowedIssueAuthors: null,
|
|
1193
1215
|
codexHomeCandidates: null,
|
|
1194
1216
|
allowIssueCacheMinutes: 0,
|
|
1217
|
+
labelsAsLlmAgentName: null,
|
|
1195
1218
|
});
|
|
1196
1219
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(6);
|
|
1197
1220
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(6);
|
|
@@ -1248,6 +1271,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1248
1271
|
allowedIssueAuthors: null,
|
|
1249
1272
|
codexHomeCandidates: null,
|
|
1250
1273
|
allowIssueCacheMinutes: 0,
|
|
1274
|
+
labelsAsLlmAgentName: null,
|
|
1251
1275
|
});
|
|
1252
1276
|
|
|
1253
1277
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(12);
|
|
@@ -1313,6 +1337,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1313
1337
|
allowedIssueAuthors: null,
|
|
1314
1338
|
codexHomeCandidates: null,
|
|
1315
1339
|
allowIssueCacheMinutes: 0,
|
|
1340
|
+
labelsAsLlmAgentName: null,
|
|
1316
1341
|
});
|
|
1317
1342
|
|
|
1318
1343
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(12);
|
|
@@ -1376,6 +1401,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1376
1401
|
allowedIssueAuthors: null,
|
|
1377
1402
|
codexHomeCandidates: null,
|
|
1378
1403
|
allowIssueCacheMinutes: 0,
|
|
1404
|
+
labelsAsLlmAgentName: null,
|
|
1379
1405
|
});
|
|
1380
1406
|
|
|
1381
1407
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(2);
|
|
@@ -1423,6 +1449,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1423
1449
|
allowedIssueAuthors: null,
|
|
1424
1450
|
codexHomeCandidates: null,
|
|
1425
1451
|
allowIssueCacheMinutes: 0,
|
|
1452
|
+
labelsAsLlmAgentName: null,
|
|
1426
1453
|
});
|
|
1427
1454
|
|
|
1428
1455
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
@@ -1477,6 +1504,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1477
1504
|
allowedIssueAuthors: null,
|
|
1478
1505
|
codexHomeCandidates: null,
|
|
1479
1506
|
allowIssueCacheMinutes: 0,
|
|
1507
|
+
labelsAsLlmAgentName: null,
|
|
1480
1508
|
});
|
|
1481
1509
|
|
|
1482
1510
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
@@ -1534,6 +1562,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1534
1562
|
allowedIssueAuthors: null,
|
|
1535
1563
|
codexHomeCandidates: null,
|
|
1536
1564
|
allowIssueCacheMinutes: 0,
|
|
1565
|
+
labelsAsLlmAgentName: null,
|
|
1537
1566
|
});
|
|
1538
1567
|
|
|
1539
1568
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
@@ -1581,6 +1610,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1581
1610
|
allowedIssueAuthors: null,
|
|
1582
1611
|
codexHomeCandidates: null,
|
|
1583
1612
|
allowIssueCacheMinutes: 0,
|
|
1613
|
+
labelsAsLlmAgentName: null,
|
|
1584
1614
|
});
|
|
1585
1615
|
|
|
1586
1616
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
@@ -1628,6 +1658,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1628
1658
|
allowedIssueAuthors: null,
|
|
1629
1659
|
codexHomeCandidates: null,
|
|
1630
1660
|
allowIssueCacheMinutes: 0,
|
|
1661
|
+
labelsAsLlmAgentName: null,
|
|
1631
1662
|
});
|
|
1632
1663
|
|
|
1633
1664
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
@@ -1675,6 +1706,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1675
1706
|
allowedIssueAuthors: null,
|
|
1676
1707
|
codexHomeCandidates: null,
|
|
1677
1708
|
allowIssueCacheMinutes: 0,
|
|
1709
|
+
labelsAsLlmAgentName: null,
|
|
1678
1710
|
});
|
|
1679
1711
|
|
|
1680
1712
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
@@ -1728,6 +1760,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1728
1760
|
allowedIssueAuthors: ['user1', 'user2'],
|
|
1729
1761
|
codexHomeCandidates: null,
|
|
1730
1762
|
allowIssueCacheMinutes: 0,
|
|
1763
|
+
labelsAsLlmAgentName: null,
|
|
1731
1764
|
});
|
|
1732
1765
|
|
|
1733
1766
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
@@ -1775,6 +1808,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1775
1808
|
allowedIssueAuthors: null,
|
|
1776
1809
|
codexHomeCandidates: null,
|
|
1777
1810
|
allowIssueCacheMinutes: 0,
|
|
1811
|
+
labelsAsLlmAgentName: null,
|
|
1778
1812
|
});
|
|
1779
1813
|
|
|
1780
1814
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(2);
|
|
@@ -1820,6 +1854,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1820
1854
|
allowedIssueAuthors: ['user1', 'user2'],
|
|
1821
1855
|
codexHomeCandidates: null,
|
|
1822
1856
|
allowIssueCacheMinutes: 0,
|
|
1857
|
+
labelsAsLlmAgentName: null,
|
|
1823
1858
|
});
|
|
1824
1859
|
|
|
1825
1860
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
@@ -1859,6 +1894,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1859
1894
|
allowedIssueAuthors: ['user1'],
|
|
1860
1895
|
codexHomeCandidates: null,
|
|
1861
1896
|
allowIssueCacheMinutes: 0,
|
|
1897
|
+
labelsAsLlmAgentName: null,
|
|
1862
1898
|
});
|
|
1863
1899
|
|
|
1864
1900
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
@@ -1895,6 +1931,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1895
1931
|
allowedIssueAuthors: null,
|
|
1896
1932
|
codexHomeCandidates: null,
|
|
1897
1933
|
allowIssueCacheMinutes: 0,
|
|
1934
|
+
labelsAsLlmAgentName: null,
|
|
1898
1935
|
});
|
|
1899
1936
|
|
|
1900
1937
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
@@ -1942,6 +1979,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1942
1979
|
allowedIssueAuthors: null,
|
|
1943
1980
|
codexHomeCandidates: [],
|
|
1944
1981
|
allowIssueCacheMinutes: 0,
|
|
1982
|
+
labelsAsLlmAgentName: null,
|
|
1945
1983
|
});
|
|
1946
1984
|
|
|
1947
1985
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
@@ -1989,6 +2027,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
1989
2027
|
allowedIssueAuthors: null,
|
|
1990
2028
|
codexHomeCandidates: ['.codex-dev1'],
|
|
1991
2029
|
allowIssueCacheMinutes: 0,
|
|
2030
|
+
labelsAsLlmAgentName: null,
|
|
1992
2031
|
});
|
|
1993
2032
|
|
|
1994
2033
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
@@ -2056,6 +2095,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2056
2095
|
allowedIssueAuthors: null,
|
|
2057
2096
|
codexHomeCandidates: ['.codex-dev1', '.codex-dev2'],
|
|
2058
2097
|
allowIssueCacheMinutes: 0,
|
|
2098
|
+
labelsAsLlmAgentName: null,
|
|
2059
2099
|
});
|
|
2060
2100
|
|
|
2061
2101
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(3);
|
|
@@ -2120,6 +2160,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2120
2160
|
allowedIssueAuthors: null,
|
|
2121
2161
|
codexHomeCandidates: null,
|
|
2122
2162
|
allowIssueCacheMinutes: 0,
|
|
2163
|
+
labelsAsLlmAgentName: null,
|
|
2123
2164
|
});
|
|
2124
2165
|
|
|
2125
2166
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
@@ -2178,6 +2219,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2178
2219
|
allowedIssueAuthors: null,
|
|
2179
2220
|
codexHomeCandidates: null,
|
|
2180
2221
|
allowIssueCacheMinutes: 0,
|
|
2222
|
+
labelsAsLlmAgentName: null,
|
|
2181
2223
|
});
|
|
2182
2224
|
|
|
2183
2225
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
@@ -2205,6 +2247,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2205
2247
|
allowedIssueAuthors: null,
|
|
2206
2248
|
codexHomeCandidates: null,
|
|
2207
2249
|
allowIssueCacheMinutes: 5,
|
|
2250
|
+
labelsAsLlmAgentName: null,
|
|
2208
2251
|
});
|
|
2209
2252
|
|
|
2210
2253
|
expect(mockIssueRepository.getStoryObjectMap).toHaveBeenCalledWith(
|
|
@@ -2251,6 +2294,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2251
2294
|
allowedIssueAuthors: null,
|
|
2252
2295
|
codexHomeCandidates: null,
|
|
2253
2296
|
allowIssueCacheMinutes: 0,
|
|
2297
|
+
labelsAsLlmAgentName: null,
|
|
2254
2298
|
});
|
|
2255
2299
|
|
|
2256
2300
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
@@ -2300,6 +2344,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2300
2344
|
allowedIssueAuthors: null,
|
|
2301
2345
|
codexHomeCandidates: null,
|
|
2302
2346
|
allowIssueCacheMinutes: 0,
|
|
2347
|
+
labelsAsLlmAgentName: null,
|
|
2303
2348
|
});
|
|
2304
2349
|
|
|
2305
2350
|
expect(
|
|
@@ -2385,6 +2430,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2385
2430
|
allowedIssueAuthors: null,
|
|
2386
2431
|
codexHomeCandidates: null,
|
|
2387
2432
|
allowIssueCacheMinutes: 0,
|
|
2433
|
+
labelsAsLlmAgentName: null,
|
|
2388
2434
|
});
|
|
2389
2435
|
|
|
2390
2436
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(3);
|
|
@@ -2441,6 +2487,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2441
2487
|
allowedIssueAuthors: null,
|
|
2442
2488
|
codexHomeCandidates: null,
|
|
2443
2489
|
allowIssueCacheMinutes: 0,
|
|
2490
|
+
labelsAsLlmAgentName: null,
|
|
2444
2491
|
});
|
|
2445
2492
|
|
|
2446
2493
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
@@ -2450,7 +2497,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2450
2497
|
).toHaveLength(0);
|
|
2451
2498
|
});
|
|
2452
2499
|
|
|
2453
|
-
it('should pick the token with the
|
|
2500
|
+
it('should pick the token with the soonest 7-day reset deadline first', async () => {
|
|
2454
2501
|
const awaitingIssue = createMockIssue({
|
|
2455
2502
|
url: 'url1',
|
|
2456
2503
|
title: 'Issue 1',
|
|
@@ -2468,24 +2515,35 @@ describe('StartPreparationUseCase', () => {
|
|
|
2468
2515
|
stderr: '',
|
|
2469
2516
|
exitCode: 0,
|
|
2470
2517
|
});
|
|
2518
|
+
const nowEpochSeconds = Math.floor(Date.now() / 1000);
|
|
2471
2519
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2472
2520
|
{
|
|
2473
|
-
name: 'token-
|
|
2474
|
-
token: 'token-
|
|
2521
|
+
name: 'token-far-reset',
|
|
2522
|
+
token: 'token-far-reset',
|
|
2475
2523
|
fiveHourUtilization: 0.1,
|
|
2476
2524
|
sevenDayUtilization: 0.7,
|
|
2477
2525
|
blocked: false,
|
|
2478
2526
|
rejected: false,
|
|
2479
|
-
modelWeeklyLimits: {
|
|
2527
|
+
modelWeeklyLimits: {
|
|
2528
|
+
seven_day_opus: {
|
|
2529
|
+
rejected: false,
|
|
2530
|
+
resetsAt: nowEpochSeconds + 100 * 3600,
|
|
2531
|
+
},
|
|
2532
|
+
},
|
|
2480
2533
|
},
|
|
2481
2534
|
{
|
|
2482
|
-
name: 'token-
|
|
2483
|
-
token: 'token-
|
|
2535
|
+
name: 'token-soon-reset',
|
|
2536
|
+
token: 'token-soon-reset',
|
|
2484
2537
|
fiveHourUtilization: 0.5,
|
|
2485
2538
|
sevenDayUtilization: 0.2,
|
|
2486
2539
|
blocked: false,
|
|
2487
2540
|
rejected: false,
|
|
2488
|
-
modelWeeklyLimits: {
|
|
2541
|
+
modelWeeklyLimits: {
|
|
2542
|
+
seven_day_opus: {
|
|
2543
|
+
rejected: false,
|
|
2544
|
+
resetsAt: nowEpochSeconds + 20 * 3600,
|
|
2545
|
+
},
|
|
2546
|
+
},
|
|
2489
2547
|
},
|
|
2490
2548
|
]);
|
|
2491
2549
|
|
|
@@ -2500,12 +2558,13 @@ describe('StartPreparationUseCase', () => {
|
|
|
2500
2558
|
allowedIssueAuthors: null,
|
|
2501
2559
|
codexHomeCandidates: null,
|
|
2502
2560
|
allowIssueCacheMinutes: 0,
|
|
2561
|
+
labelsAsLlmAgentName: null,
|
|
2503
2562
|
});
|
|
2504
2563
|
|
|
2505
2564
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
2506
2565
|
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toEqual({
|
|
2507
2566
|
env: {
|
|
2508
|
-
CLAUDE_CODE_OAUTH_TOKEN: 'token-
|
|
2567
|
+
CLAUDE_CODE_OAUTH_TOKEN: 'token-soon-reset',
|
|
2509
2568
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8787',
|
|
2510
2569
|
},
|
|
2511
2570
|
});
|
|
@@ -2561,6 +2620,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2561
2620
|
allowedIssueAuthors: null,
|
|
2562
2621
|
codexHomeCandidates: null,
|
|
2563
2622
|
allowIssueCacheMinutes: 0,
|
|
2623
|
+
labelsAsLlmAgentName: null,
|
|
2564
2624
|
});
|
|
2565
2625
|
|
|
2566
2626
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
@@ -2625,6 +2685,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2625
2685
|
allowedIssueAuthors: null,
|
|
2626
2686
|
codexHomeCandidates: null,
|
|
2627
2687
|
allowIssueCacheMinutes: 0,
|
|
2688
|
+
labelsAsLlmAgentName: null,
|
|
2628
2689
|
});
|
|
2629
2690
|
|
|
2630
2691
|
expect(
|
|
@@ -2692,6 +2753,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2692
2753
|
allowedIssueAuthors: null,
|
|
2693
2754
|
codexHomeCandidates: null,
|
|
2694
2755
|
allowIssueCacheMinutes: 0,
|
|
2756
|
+
labelsAsLlmAgentName: null,
|
|
2695
2757
|
});
|
|
2696
2758
|
|
|
2697
2759
|
expect(
|
|
@@ -2706,7 +2768,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2706
2768
|
consoleWarnSpy.mockRestore();
|
|
2707
2769
|
});
|
|
2708
2770
|
|
|
2709
|
-
it('should sort tokens by 7-day
|
|
2771
|
+
it('should sort tokens by 7-day reset deadline ascending when all have full process capacity', async () => {
|
|
2710
2772
|
const awaitingIssues: Issue[] = [
|
|
2711
2773
|
createMockIssue({
|
|
2712
2774
|
url: 'url1',
|
|
@@ -2742,33 +2804,49 @@ describe('StartPreparationUseCase', () => {
|
|
|
2742
2804
|
stderr: '',
|
|
2743
2805
|
exitCode: 0,
|
|
2744
2806
|
});
|
|
2807
|
+
const nowEpochSeconds = Math.floor(Date.now() / 1000);
|
|
2745
2808
|
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
2746
2809
|
{
|
|
2747
|
-
name: 'token-7d-mid',
|
|
2748
|
-
token: 'token-7d-mid',
|
|
2810
|
+
name: 'token-7d-mid-reset',
|
|
2811
|
+
token: 'token-7d-mid-reset',
|
|
2749
2812
|
fiveHourUtilization: 0.1,
|
|
2750
2813
|
sevenDayUtilization: 0.5,
|
|
2751
2814
|
blocked: false,
|
|
2752
2815
|
rejected: false,
|
|
2753
|
-
modelWeeklyLimits: {
|
|
2816
|
+
modelWeeklyLimits: {
|
|
2817
|
+
seven_day_opus: {
|
|
2818
|
+
rejected: false,
|
|
2819
|
+
resetsAt: nowEpochSeconds + 50 * 3600,
|
|
2820
|
+
},
|
|
2821
|
+
},
|
|
2754
2822
|
},
|
|
2755
2823
|
{
|
|
2756
|
-
name: 'token-7d-
|
|
2757
|
-
token: 'token-7d-
|
|
2824
|
+
name: 'token-7d-soon-reset',
|
|
2825
|
+
token: 'token-7d-soon-reset',
|
|
2758
2826
|
fiveHourUtilization: 0.5,
|
|
2759
2827
|
sevenDayUtilization: 0.1,
|
|
2760
2828
|
blocked: false,
|
|
2761
2829
|
rejected: false,
|
|
2762
|
-
modelWeeklyLimits: {
|
|
2830
|
+
modelWeeklyLimits: {
|
|
2831
|
+
seven_day_opus: {
|
|
2832
|
+
rejected: false,
|
|
2833
|
+
resetsAt: nowEpochSeconds + 10 * 3600,
|
|
2834
|
+
},
|
|
2835
|
+
},
|
|
2763
2836
|
},
|
|
2764
2837
|
{
|
|
2765
|
-
name: 'token-7d-
|
|
2766
|
-
token: 'token-7d-
|
|
2838
|
+
name: 'token-7d-far-reset',
|
|
2839
|
+
token: 'token-7d-far-reset',
|
|
2767
2840
|
fiveHourUtilization: 0.3,
|
|
2768
2841
|
sevenDayUtilization: 0.7,
|
|
2769
2842
|
blocked: false,
|
|
2770
2843
|
rejected: false,
|
|
2771
|
-
modelWeeklyLimits: {
|
|
2844
|
+
modelWeeklyLimits: {
|
|
2845
|
+
seven_day_opus: {
|
|
2846
|
+
rejected: false,
|
|
2847
|
+
resetsAt: nowEpochSeconds + 150 * 3600,
|
|
2848
|
+
},
|
|
2849
|
+
},
|
|
2772
2850
|
},
|
|
2773
2851
|
]);
|
|
2774
2852
|
|
|
@@ -2783,17 +2861,18 @@ describe('StartPreparationUseCase', () => {
|
|
|
2783
2861
|
allowedIssueAuthors: null,
|
|
2784
2862
|
codexHomeCandidates: null,
|
|
2785
2863
|
allowIssueCacheMinutes: 0,
|
|
2864
|
+
labelsAsLlmAgentName: null,
|
|
2786
2865
|
});
|
|
2787
2866
|
|
|
2788
2867
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(3);
|
|
2789
2868
|
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toMatchObject({
|
|
2790
|
-
env: { CLAUDE_CODE_OAUTH_TOKEN: 'token-7d-
|
|
2869
|
+
env: { CLAUDE_CODE_OAUTH_TOKEN: 'token-7d-soon-reset' },
|
|
2791
2870
|
});
|
|
2792
2871
|
expect(mockLocalCommandRunner.runCommand.mock.calls[1][2]).toMatchObject({
|
|
2793
|
-
env: { CLAUDE_CODE_OAUTH_TOKEN: 'token-7d-mid' },
|
|
2872
|
+
env: { CLAUDE_CODE_OAUTH_TOKEN: 'token-7d-mid-reset' },
|
|
2794
2873
|
});
|
|
2795
2874
|
expect(mockLocalCommandRunner.runCommand.mock.calls[2][2]).toMatchObject({
|
|
2796
|
-
env: { CLAUDE_CODE_OAUTH_TOKEN: 'token-7d-
|
|
2875
|
+
env: { CLAUDE_CODE_OAUTH_TOKEN: 'token-7d-far-reset' },
|
|
2797
2876
|
});
|
|
2798
2877
|
});
|
|
2799
2878
|
|
|
@@ -2840,6 +2919,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2840
2919
|
allowedIssueAuthors: null,
|
|
2841
2920
|
codexHomeCandidates: null,
|
|
2842
2921
|
allowIssueCacheMinutes: 0,
|
|
2922
|
+
labelsAsLlmAgentName: null,
|
|
2843
2923
|
});
|
|
2844
2924
|
|
|
2845
2925
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(3);
|
|
@@ -2901,6 +2981,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2901
2981
|
allowedIssueAuthors: null,
|
|
2902
2982
|
codexHomeCandidates: null,
|
|
2903
2983
|
allowIssueCacheMinutes: 0,
|
|
2984
|
+
labelsAsLlmAgentName: null,
|
|
2904
2985
|
});
|
|
2905
2986
|
|
|
2906
2987
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
@@ -2962,6 +3043,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
2962
3043
|
allowedIssueAuthors: null,
|
|
2963
3044
|
codexHomeCandidates: null,
|
|
2964
3045
|
allowIssueCacheMinutes: 0,
|
|
3046
|
+
labelsAsLlmAgentName: null,
|
|
2965
3047
|
});
|
|
2966
3048
|
|
|
2967
3049
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
@@ -3023,6 +3105,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
3023
3105
|
allowedIssueAuthors: null,
|
|
3024
3106
|
codexHomeCandidates: null,
|
|
3025
3107
|
allowIssueCacheMinutes: 0,
|
|
3108
|
+
labelsAsLlmAgentName: null,
|
|
3026
3109
|
});
|
|
3027
3110
|
|
|
3028
3111
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
@@ -3087,6 +3170,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
3087
3170
|
allowedIssueAuthors: null,
|
|
3088
3171
|
codexHomeCandidates: null,
|
|
3089
3172
|
allowIssueCacheMinutes: 0,
|
|
3173
|
+
labelsAsLlmAgentName: null,
|
|
3090
3174
|
});
|
|
3091
3175
|
|
|
3092
3176
|
expect(
|
|
@@ -3133,6 +3217,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
3133
3217
|
allowedIssueAuthors: null,
|
|
3134
3218
|
codexHomeCandidates: null,
|
|
3135
3219
|
allowIssueCacheMinutes: 0,
|
|
3220
|
+
labelsAsLlmAgentName: null,
|
|
3136
3221
|
});
|
|
3137
3222
|
|
|
3138
3223
|
expect(
|
|
@@ -3197,6 +3282,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
3197
3282
|
allowedIssueAuthors: null,
|
|
3198
3283
|
codexHomeCandidates: null,
|
|
3199
3284
|
allowIssueCacheMinutes: 0,
|
|
3285
|
+
labelsAsLlmAgentName: null,
|
|
3200
3286
|
});
|
|
3201
3287
|
|
|
3202
3288
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
@@ -3261,6 +3347,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
3261
3347
|
allowedIssueAuthors: null,
|
|
3262
3348
|
codexHomeCandidates: null,
|
|
3263
3349
|
allowIssueCacheMinutes: 0,
|
|
3350
|
+
labelsAsLlmAgentName: null,
|
|
3264
3351
|
});
|
|
3265
3352
|
|
|
3266
3353
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
@@ -3325,6 +3412,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
3325
3412
|
allowedIssueAuthors: null,
|
|
3326
3413
|
codexHomeCandidates: null,
|
|
3327
3414
|
allowIssueCacheMinutes: 0,
|
|
3415
|
+
labelsAsLlmAgentName: null,
|
|
3328
3416
|
});
|
|
3329
3417
|
|
|
3330
3418
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
@@ -3389,6 +3477,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
3389
3477
|
allowedIssueAuthors: null,
|
|
3390
3478
|
codexHomeCandidates: null,
|
|
3391
3479
|
allowIssueCacheMinutes: 0,
|
|
3480
|
+
labelsAsLlmAgentName: null,
|
|
3392
3481
|
});
|
|
3393
3482
|
|
|
3394
3483
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
@@ -3399,6 +3488,431 @@ describe('StartPreparationUseCase', () => {
|
|
|
3399
3488
|
},
|
|
3400
3489
|
});
|
|
3401
3490
|
});
|
|
3491
|
+
|
|
3492
|
+
it('should pick the token whose 7-day reset is sooner before the token whose 7-day reset is farther', async () => {
|
|
3493
|
+
const awaitingIssue = createMockIssue({
|
|
3494
|
+
url: 'url1',
|
|
3495
|
+
title: 'Issue 1',
|
|
3496
|
+
labels: ['category:impl'],
|
|
3497
|
+
status: 'Awaiting Workspace',
|
|
3498
|
+
number: 1,
|
|
3499
|
+
itemId: 'item-1',
|
|
3500
|
+
});
|
|
3501
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
3502
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
3503
|
+
createMockStoryObjectMap([awaitingIssue]),
|
|
3504
|
+
);
|
|
3505
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
3506
|
+
stdout: '',
|
|
3507
|
+
stderr: '',
|
|
3508
|
+
exitCode: 0,
|
|
3509
|
+
});
|
|
3510
|
+
const nowEpochSeconds = Math.floor(Date.now() / 1000);
|
|
3511
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
3512
|
+
{
|
|
3513
|
+
name: 'token-b-far-reset',
|
|
3514
|
+
token: 'token-b-far-reset',
|
|
3515
|
+
fiveHourUtilization: 0.1,
|
|
3516
|
+
sevenDayUtilization: 0.1,
|
|
3517
|
+
blocked: false,
|
|
3518
|
+
rejected: false,
|
|
3519
|
+
modelWeeklyLimits: {
|
|
3520
|
+
seven_day_opus: {
|
|
3521
|
+
rejected: false,
|
|
3522
|
+
resetsAt: nowEpochSeconds + 100 * 3600,
|
|
3523
|
+
},
|
|
3524
|
+
},
|
|
3525
|
+
},
|
|
3526
|
+
{
|
|
3527
|
+
name: 'token-a-soon-reset',
|
|
3528
|
+
token: 'token-a-soon-reset',
|
|
3529
|
+
fiveHourUtilization: 0.1,
|
|
3530
|
+
sevenDayUtilization: 0.1,
|
|
3531
|
+
blocked: false,
|
|
3532
|
+
rejected: false,
|
|
3533
|
+
modelWeeklyLimits: {
|
|
3534
|
+
seven_day_opus: {
|
|
3535
|
+
rejected: false,
|
|
3536
|
+
resetsAt: nowEpochSeconds + 20 * 3600,
|
|
3537
|
+
},
|
|
3538
|
+
},
|
|
3539
|
+
},
|
|
3540
|
+
]);
|
|
3541
|
+
|
|
3542
|
+
await useCase.run({
|
|
3543
|
+
projectUrl: 'https://github.com/user/repo',
|
|
3544
|
+
defaultAgentName: 'agent1',
|
|
3545
|
+
defaultLlmModelName: 'claude-opus',
|
|
3546
|
+
defaultLlmAgentName: null,
|
|
3547
|
+
configFilePath: '/path/to/config.yml',
|
|
3548
|
+
maximumPreparingIssuesCount: null,
|
|
3549
|
+
utilizationPercentageThreshold: 90,
|
|
3550
|
+
allowedIssueAuthors: null,
|
|
3551
|
+
codexHomeCandidates: null,
|
|
3552
|
+
allowIssueCacheMinutes: 0,
|
|
3553
|
+
labelsAsLlmAgentName: null,
|
|
3554
|
+
});
|
|
3555
|
+
|
|
3556
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
3557
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toEqual({
|
|
3558
|
+
env: {
|
|
3559
|
+
CLAUDE_CODE_OAUTH_TOKEN: 'token-a-soon-reset',
|
|
3560
|
+
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8787',
|
|
3561
|
+
},
|
|
3562
|
+
});
|
|
3563
|
+
});
|
|
3564
|
+
|
|
3565
|
+
it('should exclude a blocked token even when it has a sooner 7-day reset than an eligible token', async () => {
|
|
3566
|
+
const awaitingIssue = createMockIssue({
|
|
3567
|
+
url: 'url1',
|
|
3568
|
+
title: 'Issue 1',
|
|
3569
|
+
labels: ['category:impl'],
|
|
3570
|
+
status: 'Awaiting Workspace',
|
|
3571
|
+
number: 1,
|
|
3572
|
+
itemId: 'item-1',
|
|
3573
|
+
});
|
|
3574
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
3575
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
3576
|
+
createMockStoryObjectMap([awaitingIssue]),
|
|
3577
|
+
);
|
|
3578
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
3579
|
+
stdout: '',
|
|
3580
|
+
stderr: '',
|
|
3581
|
+
exitCode: 0,
|
|
3582
|
+
});
|
|
3583
|
+
const nowEpochSeconds = Math.floor(Date.now() / 1000);
|
|
3584
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
3585
|
+
{
|
|
3586
|
+
name: 'token-a-blocked-soon-reset',
|
|
3587
|
+
token: 'token-a-blocked-soon-reset',
|
|
3588
|
+
fiveHourUtilization: 0.1,
|
|
3589
|
+
sevenDayUtilization: 0.1,
|
|
3590
|
+
blocked: true,
|
|
3591
|
+
rejected: false,
|
|
3592
|
+
modelWeeklyLimits: {
|
|
3593
|
+
seven_day_opus: {
|
|
3594
|
+
rejected: false,
|
|
3595
|
+
resetsAt: nowEpochSeconds + 20 * 3600,
|
|
3596
|
+
},
|
|
3597
|
+
},
|
|
3598
|
+
},
|
|
3599
|
+
{
|
|
3600
|
+
name: 'token-b-far-reset',
|
|
3601
|
+
token: 'token-b-far-reset',
|
|
3602
|
+
fiveHourUtilization: 0.1,
|
|
3603
|
+
sevenDayUtilization: 0.1,
|
|
3604
|
+
blocked: false,
|
|
3605
|
+
rejected: false,
|
|
3606
|
+
modelWeeklyLimits: {
|
|
3607
|
+
seven_day_opus: {
|
|
3608
|
+
rejected: false,
|
|
3609
|
+
resetsAt: nowEpochSeconds + 100 * 3600,
|
|
3610
|
+
},
|
|
3611
|
+
},
|
|
3612
|
+
},
|
|
3613
|
+
]);
|
|
3614
|
+
|
|
3615
|
+
await useCase.run({
|
|
3616
|
+
projectUrl: 'https://github.com/user/repo',
|
|
3617
|
+
defaultAgentName: 'agent1',
|
|
3618
|
+
defaultLlmModelName: 'claude-opus',
|
|
3619
|
+
defaultLlmAgentName: null,
|
|
3620
|
+
configFilePath: '/path/to/config.yml',
|
|
3621
|
+
maximumPreparingIssuesCount: null,
|
|
3622
|
+
utilizationPercentageThreshold: 90,
|
|
3623
|
+
allowedIssueAuthors: null,
|
|
3624
|
+
codexHomeCandidates: null,
|
|
3625
|
+
allowIssueCacheMinutes: 0,
|
|
3626
|
+
labelsAsLlmAgentName: null,
|
|
3627
|
+
});
|
|
3628
|
+
|
|
3629
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
3630
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toEqual({
|
|
3631
|
+
env: {
|
|
3632
|
+
CLAUDE_CODE_OAUTH_TOKEN: 'token-b-far-reset',
|
|
3633
|
+
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8787',
|
|
3634
|
+
},
|
|
3635
|
+
});
|
|
3636
|
+
});
|
|
3637
|
+
|
|
3638
|
+
it('should place a token whose 7-day reset is unknown after a token whose 7-day reset is known', async () => {
|
|
3639
|
+
const awaitingIssues: Issue[] = [
|
|
3640
|
+
createMockIssue({
|
|
3641
|
+
url: 'url1',
|
|
3642
|
+
title: 'Issue 1',
|
|
3643
|
+
labels: ['category:impl'],
|
|
3644
|
+
status: 'Awaiting Workspace',
|
|
3645
|
+
number: 1,
|
|
3646
|
+
itemId: 'item-1',
|
|
3647
|
+
}),
|
|
3648
|
+
createMockIssue({
|
|
3649
|
+
url: 'url2',
|
|
3650
|
+
title: 'Issue 2',
|
|
3651
|
+
labels: ['category:impl'],
|
|
3652
|
+
status: 'Awaiting Workspace',
|
|
3653
|
+
number: 2,
|
|
3654
|
+
itemId: 'item-2',
|
|
3655
|
+
}),
|
|
3656
|
+
];
|
|
3657
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
3658
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
3659
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
3660
|
+
);
|
|
3661
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
3662
|
+
stdout: '',
|
|
3663
|
+
stderr: '',
|
|
3664
|
+
exitCode: 0,
|
|
3665
|
+
});
|
|
3666
|
+
const nowEpochSeconds = Math.floor(Date.now() / 1000);
|
|
3667
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
3668
|
+
{
|
|
3669
|
+
name: 'token-a-unknown-reset',
|
|
3670
|
+
token: 'token-a-unknown-reset',
|
|
3671
|
+
fiveHourUtilization: 0.1,
|
|
3672
|
+
sevenDayUtilization: 0.1,
|
|
3673
|
+
blocked: false,
|
|
3674
|
+
rejected: false,
|
|
3675
|
+
modelWeeklyLimits: {},
|
|
3676
|
+
},
|
|
3677
|
+
{
|
|
3678
|
+
name: 'token-b-known-soon-reset',
|
|
3679
|
+
token: 'token-b-known-soon-reset',
|
|
3680
|
+
fiveHourUtilization: 0.1,
|
|
3681
|
+
sevenDayUtilization: 0.1,
|
|
3682
|
+
blocked: false,
|
|
3683
|
+
rejected: false,
|
|
3684
|
+
modelWeeklyLimits: {
|
|
3685
|
+
seven_day_opus: {
|
|
3686
|
+
rejected: false,
|
|
3687
|
+
resetsAt: nowEpochSeconds + 20 * 3600,
|
|
3688
|
+
},
|
|
3689
|
+
},
|
|
3690
|
+
},
|
|
3691
|
+
]);
|
|
3692
|
+
|
|
3693
|
+
await useCase.run({
|
|
3694
|
+
projectUrl: 'https://github.com/user/repo',
|
|
3695
|
+
defaultAgentName: 'agent1',
|
|
3696
|
+
defaultLlmModelName: 'claude-opus',
|
|
3697
|
+
defaultLlmAgentName: null,
|
|
3698
|
+
configFilePath: '/path/to/config.yml',
|
|
3699
|
+
maximumPreparingIssuesCount: null,
|
|
3700
|
+
utilizationPercentageThreshold: 90,
|
|
3701
|
+
allowedIssueAuthors: null,
|
|
3702
|
+
codexHomeCandidates: null,
|
|
3703
|
+
allowIssueCacheMinutes: 0,
|
|
3704
|
+
labelsAsLlmAgentName: null,
|
|
3705
|
+
});
|
|
3706
|
+
|
|
3707
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(2);
|
|
3708
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toMatchObject({
|
|
3709
|
+
env: { CLAUDE_CODE_OAUTH_TOKEN: 'token-b-known-soon-reset' },
|
|
3710
|
+
});
|
|
3711
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[1][2]).toMatchObject({
|
|
3712
|
+
env: { CLAUDE_CODE_OAUTH_TOKEN: 'token-a-unknown-reset' },
|
|
3713
|
+
});
|
|
3714
|
+
});
|
|
3715
|
+
|
|
3716
|
+
it('should fall back to 5-hour utilization ascending as tiebreaker when 7-day reset deadlines are identical', async () => {
|
|
3717
|
+
const awaitingIssue = createMockIssue({
|
|
3718
|
+
url: 'url1',
|
|
3719
|
+
title: 'Issue 1',
|
|
3720
|
+
labels: ['category:impl'],
|
|
3721
|
+
status: 'Awaiting Workspace',
|
|
3722
|
+
number: 1,
|
|
3723
|
+
itemId: 'item-1',
|
|
3724
|
+
});
|
|
3725
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
3726
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
3727
|
+
createMockStoryObjectMap([awaitingIssue]),
|
|
3728
|
+
);
|
|
3729
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
3730
|
+
stdout: '',
|
|
3731
|
+
stderr: '',
|
|
3732
|
+
exitCode: 0,
|
|
3733
|
+
});
|
|
3734
|
+
const nowEpochSeconds = Math.floor(Date.now() / 1000);
|
|
3735
|
+
const sharedResetsAt = nowEpochSeconds + 50 * 3600;
|
|
3736
|
+
mockClaudeTokenUsageRepository.getAvailableTokenUsages.mockResolvedValue([
|
|
3737
|
+
{
|
|
3738
|
+
name: 'token-busy-5h',
|
|
3739
|
+
token: 'token-busy-5h',
|
|
3740
|
+
fiveHourUtilization: 0.6,
|
|
3741
|
+
sevenDayUtilization: 0.1,
|
|
3742
|
+
blocked: false,
|
|
3743
|
+
rejected: false,
|
|
3744
|
+
modelWeeklyLimits: {
|
|
3745
|
+
seven_day_opus: { rejected: false, resetsAt: sharedResetsAt },
|
|
3746
|
+
},
|
|
3747
|
+
},
|
|
3748
|
+
{
|
|
3749
|
+
name: 'token-idle-5h',
|
|
3750
|
+
token: 'token-idle-5h',
|
|
3751
|
+
fiveHourUtilization: 0.1,
|
|
3752
|
+
sevenDayUtilization: 0.1,
|
|
3753
|
+
blocked: false,
|
|
3754
|
+
rejected: false,
|
|
3755
|
+
modelWeeklyLimits: {
|
|
3756
|
+
seven_day_opus: { rejected: false, resetsAt: sharedResetsAt },
|
|
3757
|
+
},
|
|
3758
|
+
},
|
|
3759
|
+
]);
|
|
3760
|
+
|
|
3761
|
+
await useCase.run({
|
|
3762
|
+
projectUrl: 'https://github.com/user/repo',
|
|
3763
|
+
defaultAgentName: 'agent1',
|
|
3764
|
+
defaultLlmModelName: 'claude-opus',
|
|
3765
|
+
defaultLlmAgentName: null,
|
|
3766
|
+
configFilePath: '/path/to/config.yml',
|
|
3767
|
+
maximumPreparingIssuesCount: null,
|
|
3768
|
+
utilizationPercentageThreshold: 90,
|
|
3769
|
+
allowedIssueAuthors: null,
|
|
3770
|
+
codexHomeCandidates: null,
|
|
3771
|
+
allowIssueCacheMinutes: 0,
|
|
3772
|
+
labelsAsLlmAgentName: null,
|
|
3773
|
+
});
|
|
3774
|
+
|
|
3775
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
3776
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls[0][2]).toEqual({
|
|
3777
|
+
env: {
|
|
3778
|
+
CLAUDE_CODE_OAUTH_TOKEN: 'token-idle-5h',
|
|
3779
|
+
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8787',
|
|
3780
|
+
},
|
|
3781
|
+
});
|
|
3782
|
+
});
|
|
3783
|
+
|
|
3784
|
+
describe('agent selection precedence', () => {
|
|
3785
|
+
const runWithIssueLabels = async (params: {
|
|
3786
|
+
labels: string[];
|
|
3787
|
+
defaultAgentName: string;
|
|
3788
|
+
defaultLlmAgentName: string | null;
|
|
3789
|
+
labelsAsLlmAgentName: string[] | null;
|
|
3790
|
+
}): Promise<string> => {
|
|
3791
|
+
const awaitingIssues: Issue[] = [
|
|
3792
|
+
createMockIssue({
|
|
3793
|
+
url: 'url1',
|
|
3794
|
+
title: 'Issue 1',
|
|
3795
|
+
labels: params.labels,
|
|
3796
|
+
status: 'Awaiting Workspace',
|
|
3797
|
+
}),
|
|
3798
|
+
];
|
|
3799
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
3800
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
3801
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
3802
|
+
);
|
|
3803
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
3804
|
+
stdout: '',
|
|
3805
|
+
stderr: '',
|
|
3806
|
+
exitCode: 0,
|
|
3807
|
+
});
|
|
3808
|
+
await useCase.run({
|
|
3809
|
+
projectUrl: 'https://github.com/user/repo',
|
|
3810
|
+
defaultAgentName: params.defaultAgentName,
|
|
3811
|
+
defaultLlmModelName: 'claude-opus',
|
|
3812
|
+
defaultLlmAgentName: params.defaultLlmAgentName,
|
|
3813
|
+
configFilePath: '/path/to/config.yml',
|
|
3814
|
+
maximumPreparingIssuesCount: null,
|
|
3815
|
+
utilizationPercentageThreshold: 90,
|
|
3816
|
+
allowedIssueAuthors: null,
|
|
3817
|
+
codexHomeCandidates: null,
|
|
3818
|
+
allowIssueCacheMinutes: 0,
|
|
3819
|
+
labelsAsLlmAgentName: params.labelsAsLlmAgentName,
|
|
3820
|
+
});
|
|
3821
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
3822
|
+
const awArgs = mockLocalCommandRunner.runCommand.mock.calls[0][1];
|
|
3823
|
+
return awArgs[1];
|
|
3824
|
+
};
|
|
3825
|
+
|
|
3826
|
+
it('selects explicit llm-agent: label over labelsAsLlmAgentName mapping', async () => {
|
|
3827
|
+
const selectedAgent = await runWithIssueLabels({
|
|
3828
|
+
labels: ['llm-agent:explicit-agent', 'story', 'category:impl'],
|
|
3829
|
+
defaultAgentName: 'default-agent',
|
|
3830
|
+
defaultLlmAgentName: 'default-llm-agent',
|
|
3831
|
+
labelsAsLlmAgentName: ['story'],
|
|
3832
|
+
});
|
|
3833
|
+
expect(selectedAgent).toBe('explicit-agent');
|
|
3834
|
+
});
|
|
3835
|
+
|
|
3836
|
+
it('uses the label name as the agent name when an issue label is listed in labelsAsLlmAgentName, over category: label', async () => {
|
|
3837
|
+
const selectedAgent = await runWithIssueLabels({
|
|
3838
|
+
labels: ['story', 'category:impl'],
|
|
3839
|
+
defaultAgentName: 'default-agent',
|
|
3840
|
+
defaultLlmAgentName: 'default-llm-agent',
|
|
3841
|
+
labelsAsLlmAgentName: ['story'],
|
|
3842
|
+
});
|
|
3843
|
+
expect(selectedAgent).toBe('story');
|
|
3844
|
+
});
|
|
3845
|
+
|
|
3846
|
+
it('matches labelsAsLlmAgentName entries exactly including colons in the label name', async () => {
|
|
3847
|
+
const selectedAgent = await runWithIssueLabels({
|
|
3848
|
+
labels: ['story:body-condition', 'category:impl'],
|
|
3849
|
+
defaultAgentName: 'default-agent',
|
|
3850
|
+
defaultLlmAgentName: 'default-llm-agent',
|
|
3851
|
+
labelsAsLlmAgentName: ['story', 'story:body-condition'],
|
|
3852
|
+
});
|
|
3853
|
+
expect(selectedAgent).toBe('story:body-condition');
|
|
3854
|
+
});
|
|
3855
|
+
|
|
3856
|
+
it('falls through to category: label when no llm-agent: label and no issue label is in labelsAsLlmAgentName', async () => {
|
|
3857
|
+
const selectedAgent = await runWithIssueLabels({
|
|
3858
|
+
labels: ['unrelated-label', 'category:impl'],
|
|
3859
|
+
defaultAgentName: 'default-agent',
|
|
3860
|
+
defaultLlmAgentName: 'default-llm-agent',
|
|
3861
|
+
labelsAsLlmAgentName: ['story'],
|
|
3862
|
+
});
|
|
3863
|
+
expect(selectedAgent).toBe('impl');
|
|
3864
|
+
});
|
|
3865
|
+
|
|
3866
|
+
it('falls through to defaultLlmAgentName when no llm-agent:, no labelsAsLlmAgentName match, and no category: label', async () => {
|
|
3867
|
+
const selectedAgent = await runWithIssueLabels({
|
|
3868
|
+
labels: ['unrelated-label'],
|
|
3869
|
+
defaultAgentName: 'default-agent',
|
|
3870
|
+
defaultLlmAgentName: 'default-llm-agent',
|
|
3871
|
+
labelsAsLlmAgentName: ['story'],
|
|
3872
|
+
});
|
|
3873
|
+
expect(selectedAgent).toBe('default-llm-agent');
|
|
3874
|
+
});
|
|
3875
|
+
|
|
3876
|
+
it('falls through to defaultAgentName when no llm-agent:, no labelsAsLlmAgentName match, no category: label, and no defaultLlmAgentName', async () => {
|
|
3877
|
+
const selectedAgent = await runWithIssueLabels({
|
|
3878
|
+
labels: ['unrelated-label'],
|
|
3879
|
+
defaultAgentName: 'default-agent',
|
|
3880
|
+
defaultLlmAgentName: null,
|
|
3881
|
+
labelsAsLlmAgentName: ['story'],
|
|
3882
|
+
});
|
|
3883
|
+
expect(selectedAgent).toBe('default-agent');
|
|
3884
|
+
});
|
|
3885
|
+
|
|
3886
|
+
it('ignores labels that are not listed in labelsAsLlmAgentName', async () => {
|
|
3887
|
+
const selectedAgent = await runWithIssueLabels({
|
|
3888
|
+
labels: ['untracked-label'],
|
|
3889
|
+
defaultAgentName: 'default-agent',
|
|
3890
|
+
defaultLlmAgentName: 'default-llm-agent',
|
|
3891
|
+
labelsAsLlmAgentName: ['story'],
|
|
3892
|
+
});
|
|
3893
|
+
expect(selectedAgent).toBe('default-llm-agent');
|
|
3894
|
+
});
|
|
3895
|
+
|
|
3896
|
+
it('ignores labels that are not listed in labelsAsLlmAgentName when other entries are present', async () => {
|
|
3897
|
+
const selectedAgent = await runWithIssueLabels({
|
|
3898
|
+
labels: ['untracked-label', 'another-untracked-label'],
|
|
3899
|
+
defaultAgentName: 'default-agent',
|
|
3900
|
+
defaultLlmAgentName: 'default-llm-agent',
|
|
3901
|
+
labelsAsLlmAgentName: ['story', 'story:body-condition'],
|
|
3902
|
+
});
|
|
3903
|
+
expect(selectedAgent).toBe('default-llm-agent');
|
|
3904
|
+
});
|
|
3905
|
+
|
|
3906
|
+
it('does not affect selection when labelsAsLlmAgentName is null and only category: label is present', async () => {
|
|
3907
|
+
const selectedAgent = await runWithIssueLabels({
|
|
3908
|
+
labels: ['category:impl'],
|
|
3909
|
+
defaultAgentName: 'default-agent',
|
|
3910
|
+
defaultLlmAgentName: 'default-llm-agent',
|
|
3911
|
+
labelsAsLlmAgentName: null,
|
|
3912
|
+
});
|
|
3913
|
+
expect(selectedAgent).toBe('impl');
|
|
3914
|
+
});
|
|
3915
|
+
});
|
|
3402
3916
|
});
|
|
3403
3917
|
|
|
3404
3918
|
describe('StartPreparationUseCase.buildRotationOrder', () => {
|
|
@@ -3444,25 +3958,36 @@ describe('StartPreparationUseCase.buildRotationOrder', () => {
|
|
|
3444
3958
|
mockClaudeTokenUsageRepositoryForRotation,
|
|
3445
3959
|
);
|
|
3446
3960
|
|
|
3447
|
-
it('lists selected tokens first in ascending 7-day
|
|
3448
|
-
const
|
|
3961
|
+
it('lists selected tokens first in ascending 7-day reset deadline order then excluded tokens', () => {
|
|
3962
|
+
const nowEpochSeconds = Math.floor(Date.now() / 1000);
|
|
3963
|
+
const tokenUsages: ClaudeTokenUsage[] = [
|
|
3449
3964
|
{
|
|
3450
|
-
name: '
|
|
3451
|
-
token: 'sk-ant-
|
|
3965
|
+
name: 'far-7d-reset',
|
|
3966
|
+
token: 'sk-ant-far',
|
|
3452
3967
|
fiveHourUtilization: 0.1,
|
|
3453
3968
|
sevenDayUtilization: 0.8,
|
|
3454
3969
|
blocked: false,
|
|
3455
3970
|
rejected: false,
|
|
3456
|
-
modelWeeklyLimits: {
|
|
3971
|
+
modelWeeklyLimits: {
|
|
3972
|
+
seven_day: {
|
|
3973
|
+
rejected: false,
|
|
3974
|
+
resetsAt: nowEpochSeconds + 100 * 3600,
|
|
3975
|
+
},
|
|
3976
|
+
},
|
|
3457
3977
|
},
|
|
3458
3978
|
{
|
|
3459
|
-
name: '
|
|
3460
|
-
token: 'sk-ant-
|
|
3979
|
+
name: 'soon-7d-reset',
|
|
3980
|
+
token: 'sk-ant-soon',
|
|
3461
3981
|
fiveHourUtilization: 0.5,
|
|
3462
3982
|
sevenDayUtilization: 0.1,
|
|
3463
3983
|
blocked: false,
|
|
3464
3984
|
rejected: false,
|
|
3465
|
-
modelWeeklyLimits: {
|
|
3985
|
+
modelWeeklyLimits: {
|
|
3986
|
+
seven_day: {
|
|
3987
|
+
rejected: false,
|
|
3988
|
+
resetsAt: nowEpochSeconds + 20 * 3600,
|
|
3989
|
+
},
|
|
3990
|
+
},
|
|
3466
3991
|
},
|
|
3467
3992
|
{
|
|
3468
3993
|
name: 'blocked-token',
|
|
@@ -3476,8 +4001,8 @@ describe('StartPreparationUseCase.buildRotationOrder', () => {
|
|
|
3476
4001
|
];
|
|
3477
4002
|
const result = useCase.buildRotationOrder(tokenUsages, 90, null);
|
|
3478
4003
|
|
|
3479
|
-
expect(result[0].name).toBe('
|
|
3480
|
-
expect(result[1].name).toBe('
|
|
4004
|
+
expect(result[0].name).toBe('soon-7d-reset');
|
|
4005
|
+
expect(result[1].name).toBe('far-7d-reset');
|
|
3481
4006
|
expect(result[2].name).toBe('blocked-token');
|
|
3482
4007
|
expect(result[2].blocked).toBe(true);
|
|
3483
4008
|
expect(result[2].thresholdExcluded).toBe(false);
|