github-issue-tower-defence-management 1.116.1 → 1.116.4
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/commit-lint.yml +1 -1
- package/.github/workflows/configs/commitlint.config.cjs +32 -0
- package/.github/workflows/umino-project.yml +9 -3
- package/CHANGELOG.md +14 -0
- package/README.md +2 -2
- package/bin/adapter/repositories/ProcClaudeLiveSessionRepository.js +18 -6
- package/bin/adapter/repositories/ProcClaudeLiveSessionRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +101 -60
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/githubRateLimitRetry.js +77 -0
- package/bin/adapter/repositories/issue/githubRateLimitRetry.js.map +1 -0
- package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js +6 -6
- package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js.map +1 -1
- package/bin/domain/usecases/OauthTokenSelectUseCase.js +1 -1
- package/bin/domain/usecases/OauthTokenSelectUseCase.js.map +1 -1
- package/package.json +1 -1
- package/renovate.json +1 -0
- package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.test.ts +9 -9
- package/src/adapter/repositories/ProcClaudeLiveSessionRepository.test.ts +69 -7
- package/src/adapter/repositories/ProcClaudeLiveSessionRepository.ts +19 -8
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +237 -1
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +274 -209
- package/src/adapter/repositories/issue/githubRateLimitRetry.test.ts +167 -0
- package/src/adapter/repositories/issue/githubRateLimitRetry.ts +105 -0
- package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.test.ts +25 -3
- package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.ts +7 -7
- package/src/domain/usecases/OauthTokenSelectUseCase.test.ts +13 -4
- package/src/domain/usecases/OauthTokenSelectUseCase.ts +1 -1
- package/src/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.ts +1 -1
- package/types/adapter/repositories/ProcClaudeLiveSessionRepository.d.ts +1 -0
- package/types/adapter/repositories/ProcClaudeLiveSessionRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +6 -2
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/githubRateLimitRetry.d.ts +10 -0
- package/types/adapter/repositories/issue/githubRateLimitRetry.d.ts.map +1 -0
- package/types/domain/usecases/OauthTokenSelectUseCase.d.ts +1 -1
- package/types/domain/usecases/OauthTokenSelectUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.d.ts.map +1 -1
|
@@ -526,6 +526,236 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
526
526
|
),
|
|
527
527
|
).rejects.toThrow('422');
|
|
528
528
|
});
|
|
529
|
+
|
|
530
|
+
it("should surface GitHub's reason together with the status when the API rejects the approval", async () => {
|
|
531
|
+
jest.spyOn(global, 'fetch').mockResolvedValueOnce(
|
|
532
|
+
new Response(
|
|
533
|
+
JSON.stringify({
|
|
534
|
+
message: 'Review Can not approve your own pull request',
|
|
535
|
+
}),
|
|
536
|
+
{
|
|
537
|
+
status: 422,
|
|
538
|
+
headers: { 'Content-Type': 'application/json' },
|
|
539
|
+
},
|
|
540
|
+
),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
544
|
+
await expect(
|
|
545
|
+
repository.approvePullRequest(
|
|
546
|
+
'https://github.com/HiromiShikata/test-repository/pull/42',
|
|
547
|
+
),
|
|
548
|
+
).rejects.toThrow(
|
|
549
|
+
'Failed to approve PR https://github.com/HiromiShikata/test-repository/pull/42: HTTP 422 Review Can not approve your own pull request',
|
|
550
|
+
);
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
describe('closePullRequest', () => {
|
|
555
|
+
afterEach(() => {
|
|
556
|
+
jest.restoreAllMocks();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("should surface GitHub's reason together with the status when the close fails", async () => {
|
|
560
|
+
jest.spyOn(global, 'fetch').mockResolvedValueOnce(
|
|
561
|
+
new Response(JSON.stringify({ message: 'Not Found' }), {
|
|
562
|
+
status: 404,
|
|
563
|
+
headers: { 'Content-Type': 'application/json' },
|
|
564
|
+
}),
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
568
|
+
await expect(
|
|
569
|
+
repository.closePullRequest(
|
|
570
|
+
'https://github.com/HiromiShikata/test-repository/pull/42',
|
|
571
|
+
),
|
|
572
|
+
).rejects.toThrow(
|
|
573
|
+
'Failed to close PR https://github.com/HiromiShikata/test-repository/pull/42: HTTP 404 Not Found',
|
|
574
|
+
);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should fall back to the status alone when the error body is not JSON', async () => {
|
|
578
|
+
jest.spyOn(global, 'fetch').mockResolvedValueOnce(
|
|
579
|
+
new Response('Internal Server Error', {
|
|
580
|
+
status: 500,
|
|
581
|
+
statusText: 'Internal Server Error',
|
|
582
|
+
}),
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
586
|
+
await expect(
|
|
587
|
+
repository.closePullRequest(
|
|
588
|
+
'https://github.com/HiromiShikata/test-repository/pull/42',
|
|
589
|
+
),
|
|
590
|
+
).rejects.toThrow(
|
|
591
|
+
'Failed to close PR https://github.com/HiromiShikata/test-repository/pull/42: HTTP 500',
|
|
592
|
+
);
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
describe('rate-limit-aware retry on console operations', () => {
|
|
597
|
+
afterEach(() => {
|
|
598
|
+
jest.restoreAllMocks();
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('retries a transient 403 rate-limit response and resolves the operation after a success', async () => {
|
|
602
|
+
const fetchSpy = jest
|
|
603
|
+
.spyOn(global, 'fetch')
|
|
604
|
+
.mockResolvedValueOnce(
|
|
605
|
+
new Response(JSON.stringify({ message: 'API rate limit exceeded' }), {
|
|
606
|
+
status: 403,
|
|
607
|
+
headers: { 'x-ratelimit-remaining': '0' },
|
|
608
|
+
}),
|
|
609
|
+
)
|
|
610
|
+
.mockResolvedValueOnce(
|
|
611
|
+
new Response(JSON.stringify({ state: 'closed' }), { status: 200 }),
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
const { repository, sleep } = createApiV3CheerioRestIssueRepository();
|
|
615
|
+
await repository.closeIssueByUrl(
|
|
616
|
+
'https://github.com/HiromiShikata/test-repository/issues/42',
|
|
617
|
+
'completed',
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
621
|
+
expect(sleep).toHaveBeenCalledTimes(1);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('retries a transient 429 secondary-rate-limit response and resolves the operation after a success', async () => {
|
|
625
|
+
const fetchSpy = jest
|
|
626
|
+
.spyOn(global, 'fetch')
|
|
627
|
+
.mockResolvedValueOnce(
|
|
628
|
+
new Response(
|
|
629
|
+
JSON.stringify({
|
|
630
|
+
message: 'You have exceeded a secondary rate limit',
|
|
631
|
+
}),
|
|
632
|
+
{ status: 429, headers: { 'retry-after': '1' } },
|
|
633
|
+
),
|
|
634
|
+
)
|
|
635
|
+
.mockResolvedValueOnce(
|
|
636
|
+
new Response(JSON.stringify({ state: 'open', merged: false }), {
|
|
637
|
+
status: 200,
|
|
638
|
+
}),
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
const { repository, sleep } = createApiV3CheerioRestIssueRepository();
|
|
642
|
+
const result = await repository.getIssueOrPullRequestState(
|
|
643
|
+
'https://github.com/HiromiShikata/test-repository/issues/42',
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
expect(result.state).toBe('open');
|
|
647
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
648
|
+
expect(sleep).toHaveBeenCalledTimes(1);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('does not retry a genuine permission 403 and surfaces a clear permission message', async () => {
|
|
652
|
+
const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue(
|
|
653
|
+
new Response(
|
|
654
|
+
JSON.stringify({
|
|
655
|
+
message: 'Resource not accessible by integration',
|
|
656
|
+
}),
|
|
657
|
+
{ status: 403, headers: { 'x-ratelimit-remaining': '4999' } },
|
|
658
|
+
),
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
const { repository, sleep } = createApiV3CheerioRestIssueRepository();
|
|
662
|
+
await expect(
|
|
663
|
+
repository.closeIssueByUrl(
|
|
664
|
+
'https://github.com/HiromiShikata/test-repository/issues/42',
|
|
665
|
+
'completed',
|
|
666
|
+
),
|
|
667
|
+
).rejects.toThrow(
|
|
668
|
+
'Failed to close issue https://github.com/HiromiShikata/test-repository/issues/42: HTTP 403 permission denied, the token cannot perform this operation Resource not accessible by integration',
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
672
|
+
expect(sleep).not.toHaveBeenCalled();
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('surfaces a clear rate-limit message including the reset time after the bounded retries are exhausted', async () => {
|
|
676
|
+
const resetEpochSeconds = 1700000000;
|
|
677
|
+
const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue(
|
|
678
|
+
new Response(JSON.stringify({ message: 'API rate limit exceeded' }), {
|
|
679
|
+
status: 403,
|
|
680
|
+
headers: {
|
|
681
|
+
'x-ratelimit-remaining': '0',
|
|
682
|
+
'x-ratelimit-reset': String(resetEpochSeconds),
|
|
683
|
+
},
|
|
684
|
+
}),
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
const { repository, sleep } = createApiV3CheerioRestIssueRepository();
|
|
688
|
+
const expectedResetIso = new Date(resetEpochSeconds * 1000).toISOString();
|
|
689
|
+
await expect(
|
|
690
|
+
repository.closeIssueByUrl(
|
|
691
|
+
'https://github.com/HiromiShikata/test-repository/issues/42',
|
|
692
|
+
'completed',
|
|
693
|
+
),
|
|
694
|
+
).rejects.toThrow(
|
|
695
|
+
`Failed to close issue https://github.com/HiromiShikata/test-repository/issues/42: HTTP 403 GitHub rate limit exceeded, please retry shortly (resets at ${expectedResetIso})`,
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
expect(fetchSpy).toHaveBeenCalledTimes(4);
|
|
699
|
+
expect(sleep).toHaveBeenCalledTimes(3);
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
describe('requestChangesWithInlineComment', () => {
|
|
704
|
+
afterEach(() => {
|
|
705
|
+
jest.restoreAllMocks();
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it("should surface GitHub's validation reason together with the status when the review POST fails", async () => {
|
|
709
|
+
jest.spyOn(global, 'fetch').mockResolvedValueOnce(
|
|
710
|
+
new Response(
|
|
711
|
+
JSON.stringify({
|
|
712
|
+
message: 'Validation Failed',
|
|
713
|
+
errors: [{ message: 'path must be part of the diff' }],
|
|
714
|
+
}),
|
|
715
|
+
{
|
|
716
|
+
status: 422,
|
|
717
|
+
headers: { 'Content-Type': 'application/json' },
|
|
718
|
+
},
|
|
719
|
+
),
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
723
|
+
await expect(
|
|
724
|
+
repository.requestChangesWithInlineComment(
|
|
725
|
+
'https://github.com/HiromiShikata/test-repository/pull/42',
|
|
726
|
+
'src/index.ts',
|
|
727
|
+
'Please address this.',
|
|
728
|
+
),
|
|
729
|
+
).rejects.toThrow(
|
|
730
|
+
'Failed to request changes on PR https://github.com/HiromiShikata/test-repository/pull/42: HTTP 422 Validation Failed: path must be part of the diff',
|
|
731
|
+
);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it('should surface string entries in the GitHub errors array', async () => {
|
|
735
|
+
jest.spyOn(global, 'fetch').mockResolvedValueOnce(
|
|
736
|
+
new Response(
|
|
737
|
+
JSON.stringify({
|
|
738
|
+
message: 'Validation Failed',
|
|
739
|
+
errors: ['position is required'],
|
|
740
|
+
}),
|
|
741
|
+
{
|
|
742
|
+
status: 422,
|
|
743
|
+
headers: { 'Content-Type': 'application/json' },
|
|
744
|
+
},
|
|
745
|
+
),
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
749
|
+
await expect(
|
|
750
|
+
repository.requestChangesWithInlineComment(
|
|
751
|
+
'https://github.com/HiromiShikata/test-repository/pull/42',
|
|
752
|
+
'src/index.ts',
|
|
753
|
+
'Please address this.',
|
|
754
|
+
),
|
|
755
|
+
).rejects.toThrow(
|
|
756
|
+
'Failed to request changes on PR https://github.com/HiromiShikata/test-repository/pull/42: HTTP 422 Validation Failed: position is required',
|
|
757
|
+
);
|
|
758
|
+
});
|
|
529
759
|
});
|
|
530
760
|
|
|
531
761
|
describe('createPullRequestReviewComment', () => {
|
|
@@ -610,7 +840,9 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
610
840
|
'RIGHT',
|
|
611
841
|
'Please rename this variable.',
|
|
612
842
|
),
|
|
613
|
-
).rejects.toThrow(
|
|
843
|
+
).rejects.toThrow(
|
|
844
|
+
'HTTP 422 Validation Failed: line must be part of the diff',
|
|
845
|
+
);
|
|
614
846
|
});
|
|
615
847
|
|
|
616
848
|
it('should throw when the head commit cannot be fetched', async () => {
|
|
@@ -1511,6 +1743,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
1511
1743
|
const graphqlProjectItemRepository = mock<GraphqlProjectItemRepository>();
|
|
1512
1744
|
const localStorageCacheRepository = mock<LocalStorageCacheRepository>();
|
|
1513
1745
|
const localStorageRepository = mock<LocalStorageRepository>();
|
|
1746
|
+
const sleep = jest.fn().mockResolvedValue(undefined);
|
|
1514
1747
|
|
|
1515
1748
|
const repository = new ApiV3CheerioRestIssueRepository(
|
|
1516
1749
|
apiV3IssueRepository,
|
|
@@ -1518,6 +1751,8 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
1518
1751
|
graphqlProjectItemRepository,
|
|
1519
1752
|
localStorageCacheRepository,
|
|
1520
1753
|
localStorageRepository,
|
|
1754
|
+
'dummy',
|
|
1755
|
+
sleep,
|
|
1521
1756
|
);
|
|
1522
1757
|
restIssueRepository.getIssue.mockResolvedValue({
|
|
1523
1758
|
labels: [],
|
|
@@ -1534,6 +1769,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
1534
1769
|
restIssueRepository,
|
|
1535
1770
|
graphqlProjectItemRepository,
|
|
1536
1771
|
localStorageCacheRepository,
|
|
1772
|
+
sleep,
|
|
1537
1773
|
};
|
|
1538
1774
|
};
|
|
1539
1775
|
});
|