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.
Files changed (39) hide show
  1. package/.github/workflows/commit-lint.yml +1 -1
  2. package/.github/workflows/configs/commitlint.config.cjs +32 -0
  3. package/.github/workflows/umino-project.yml +9 -3
  4. package/CHANGELOG.md +14 -0
  5. package/README.md +2 -2
  6. package/bin/adapter/repositories/ProcClaudeLiveSessionRepository.js +18 -6
  7. package/bin/adapter/repositories/ProcClaudeLiveSessionRepository.js.map +1 -1
  8. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +101 -60
  9. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  10. package/bin/adapter/repositories/issue/githubRateLimitRetry.js +77 -0
  11. package/bin/adapter/repositories/issue/githubRateLimitRetry.js.map +1 -0
  12. package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js +6 -6
  13. package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js.map +1 -1
  14. package/bin/domain/usecases/OauthTokenSelectUseCase.js +1 -1
  15. package/bin/domain/usecases/OauthTokenSelectUseCase.js.map +1 -1
  16. package/package.json +1 -1
  17. package/renovate.json +1 -0
  18. package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.test.ts +9 -9
  19. package/src/adapter/repositories/ProcClaudeLiveSessionRepository.test.ts +69 -7
  20. package/src/adapter/repositories/ProcClaudeLiveSessionRepository.ts +19 -8
  21. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +237 -1
  22. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +274 -209
  23. package/src/adapter/repositories/issue/githubRateLimitRetry.test.ts +167 -0
  24. package/src/adapter/repositories/issue/githubRateLimitRetry.ts +105 -0
  25. package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.test.ts +25 -3
  26. package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.ts +7 -7
  27. package/src/domain/usecases/OauthTokenSelectUseCase.test.ts +13 -4
  28. package/src/domain/usecases/OauthTokenSelectUseCase.ts +1 -1
  29. package/src/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.ts +1 -1
  30. package/types/adapter/repositories/ProcClaudeLiveSessionRepository.d.ts +1 -0
  31. package/types/adapter/repositories/ProcClaudeLiveSessionRepository.d.ts.map +1 -1
  32. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +6 -2
  33. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  34. package/types/adapter/repositories/issue/githubRateLimitRetry.d.ts +10 -0
  35. package/types/adapter/repositories/issue/githubRateLimitRetry.d.ts.map +1 -0
  36. package/types/domain/usecases/OauthTokenSelectUseCase.d.ts +1 -1
  37. package/types/domain/usecases/OauthTokenSelectUseCase.d.ts.map +1 -1
  38. package/types/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.d.ts +1 -1
  39. 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('Validation Failed: line must be part of the diff');
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
  });