github-issue-tower-defence-management 1.85.0 → 1.87.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.
Files changed (64) hide show
  1. package/.eslintrc.cjs +5 -1
  2. package/.github/workflows/console-ui.yml +47 -0
  3. package/.prettierignore +3 -0
  4. package/CHANGELOG.md +15 -0
  5. package/README.md +12 -0
  6. package/bin/adapter/entry-points/cli/index.js +37 -0
  7. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  8. package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
  9. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  10. package/bin/adapter/entry-points/console/consoleServer.js +204 -0
  11. package/bin/adapter/entry-points/console/consoleServer.js.map +1 -0
  12. package/bin/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +1 -0
  13. package/bin/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +49 -0
  14. package/bin/adapter/entry-points/console/ui-dist/index.html +13 -0
  15. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +306 -0
  16. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  17. package/package.json +22 -2
  18. package/scripts/copyConsoleUiDist.mjs +35 -0
  19. package/src/adapter/entry-points/cli/index.test.ts +126 -0
  20. package/src/adapter/entry-points/cli/index.ts +66 -0
  21. package/src/adapter/entry-points/cli/projectConfig.ts +4 -0
  22. package/src/adapter/entry-points/console/consoleServer.test.ts +297 -0
  23. package/src/adapter/entry-points/console/consoleServer.ts +220 -0
  24. package/src/adapter/entry-points/console/ui/.storybook/main.ts +12 -0
  25. package/src/adapter/entry-points/console/ui/.storybook/preview.ts +15 -0
  26. package/src/adapter/entry-points/console/ui/biome.json +47 -0
  27. package/src/adapter/entry-points/console/ui/components.json +20 -0
  28. package/src/adapter/entry-points/console/ui/index.html +12 -0
  29. package/src/adapter/entry-points/console/ui/src/components/ui/badge.stories.tsx +35 -0
  30. package/src/adapter/entry-points/console/ui/src/components/ui/badge.tsx +28 -0
  31. package/src/adapter/entry-points/console/ui/src/components/ui/button.stories.tsx +34 -0
  32. package/src/adapter/entry-points/console/ui/src/components/ui/button.tsx +50 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +44 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +58 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +34 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +32 -0
  37. package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +47 -0
  38. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +65 -0
  39. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.ts +64 -0
  40. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +19 -0
  41. package/src/adapter/entry-points/console/ui/src/features/console/types.ts +69 -0
  42. package/src/adapter/entry-points/console/ui/src/index.css +31 -0
  43. package/src/adapter/entry-points/console/ui/src/lib/utils.ts +4 -0
  44. package/src/adapter/entry-points/console/ui/src/main.tsx +15 -0
  45. package/src/adapter/entry-points/console/ui/src/vite-env.d.ts +1 -0
  46. package/src/adapter/entry-points/console/ui/tsconfig.json +24 -0
  47. package/src/adapter/entry-points/console/ui/vite.config.ts +19 -0
  48. package/src/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +1 -0
  49. package/src/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +49 -0
  50. package/src/adapter/entry-points/console/ui-dist/index.html +13 -0
  51. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +630 -0
  52. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +492 -0
  53. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +51 -0
  54. package/tsconfig.build.json +7 -1
  55. package/tsconfig.json +6 -1
  56. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  57. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  58. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  59. package/types/adapter/entry-points/console/consoleServer.d.ts +19 -0
  60. package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -0
  61. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +18 -1
  62. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  63. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +47 -0
  64. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>TDPM Console</title>
7
+ <script type="module" crossorigin src="./assets/index-DcOZ02ON.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/index-DFxrSRH4.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
@@ -519,6 +519,636 @@ describe('ApiV3CheerioRestIssueRepository', () => {
519
519
  });
520
520
  });
521
521
 
522
+ describe('getIssueOrPullRequestBody', () => {
523
+ afterEach(() => {
524
+ jest.restoreAllMocks();
525
+ });
526
+
527
+ it('should fetch and return the issue body', async () => {
528
+ const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValueOnce(
529
+ new Response(JSON.stringify({ body: 'issue body content' }), {
530
+ status: 200,
531
+ headers: { 'Content-Type': 'application/json' },
532
+ }),
533
+ );
534
+
535
+ const { repository } = createApiV3CheerioRestIssueRepository();
536
+ const result = await repository.getIssueOrPullRequestBody(
537
+ 'https://github.com/HiromiShikata/test-repository/issues/42',
538
+ );
539
+
540
+ expect(result).toBe('issue body content');
541
+ expect(fetchSpy).toHaveBeenCalledWith(
542
+ 'https://api.github.com/repos/HiromiShikata/test-repository/issues/42',
543
+ expect.objectContaining({ method: 'GET' }),
544
+ );
545
+ });
546
+
547
+ it('should map a null body to an empty string', async () => {
548
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
549
+ new Response(JSON.stringify({ body: null }), {
550
+ status: 200,
551
+ headers: { 'Content-Type': 'application/json' },
552
+ }),
553
+ );
554
+
555
+ const { repository } = createApiV3CheerioRestIssueRepository();
556
+ const result = await repository.getIssueOrPullRequestBody(
557
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
558
+ );
559
+
560
+ expect(result).toBe('');
561
+ });
562
+
563
+ it('should throw when the API responds with a non-2xx status', async () => {
564
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
565
+ new Response('Not Found', {
566
+ status: 404,
567
+ statusText: 'Not Found',
568
+ }),
569
+ );
570
+
571
+ const { repository } = createApiV3CheerioRestIssueRepository();
572
+ await expect(
573
+ repository.getIssueOrPullRequestBody(
574
+ 'https://github.com/HiromiShikata/test-repository/issues/42',
575
+ ),
576
+ ).rejects.toThrow('404');
577
+ });
578
+ });
579
+
580
+ describe('getIssueOrPullRequestComments', () => {
581
+ afterEach(() => {
582
+ jest.restoreAllMocks();
583
+ });
584
+
585
+ it('should fetch a single page of comments ordered oldest-first', async () => {
586
+ const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValueOnce(
587
+ new Response(
588
+ JSON.stringify([
589
+ {
590
+ user: { login: 'alice' },
591
+ body: 'first comment',
592
+ created_at: '2024-01-01T00:00:00Z',
593
+ },
594
+ {
595
+ user: { login: 'bob' },
596
+ body: 'second comment',
597
+ created_at: '2024-01-02T00:00:00Z',
598
+ },
599
+ ]),
600
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
601
+ ),
602
+ );
603
+
604
+ const { repository } = createApiV3CheerioRestIssueRepository();
605
+ const result = await repository.getIssueOrPullRequestComments(
606
+ 'https://github.com/HiromiShikata/test-repository/issues/42',
607
+ );
608
+
609
+ expect(result).toEqual([
610
+ {
611
+ author: 'alice',
612
+ body: 'first comment',
613
+ createdAt: new Date('2024-01-01T00:00:00Z'),
614
+ },
615
+ {
616
+ author: 'bob',
617
+ body: 'second comment',
618
+ createdAt: new Date('2024-01-02T00:00:00Z'),
619
+ },
620
+ ]);
621
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
622
+ expect(fetchSpy).toHaveBeenCalledWith(
623
+ 'https://api.github.com/repos/HiromiShikata/test-repository/issues/42/comments?per_page=100&page=1',
624
+ expect.objectContaining({ method: 'GET' }),
625
+ );
626
+ });
627
+
628
+ it('should paginate when a page returns exactly 100 entries', async () => {
629
+ const firstPage: {
630
+ user: { login: string };
631
+ body: string;
632
+ created_at: string;
633
+ }[] = [];
634
+ for (let i = 0; i < 100; i += 1) {
635
+ firstPage.push({
636
+ user: { login: `user${i}` },
637
+ body: `comment ${i}`,
638
+ created_at: '2024-01-01T00:00:00Z',
639
+ });
640
+ }
641
+ const secondPage = [
642
+ {
643
+ user: { login: 'last' },
644
+ body: 'last comment',
645
+ created_at: '2024-02-01T00:00:00Z',
646
+ },
647
+ ];
648
+ const fetchSpy = jest
649
+ .spyOn(global, 'fetch')
650
+ .mockResolvedValueOnce(
651
+ new Response(JSON.stringify(firstPage), {
652
+ status: 200,
653
+ headers: { 'Content-Type': 'application/json' },
654
+ }),
655
+ )
656
+ .mockResolvedValueOnce(
657
+ new Response(JSON.stringify(secondPage), {
658
+ status: 200,
659
+ headers: { 'Content-Type': 'application/json' },
660
+ }),
661
+ );
662
+
663
+ const { repository } = createApiV3CheerioRestIssueRepository();
664
+ const result = await repository.getIssueOrPullRequestComments(
665
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
666
+ );
667
+
668
+ expect(result).toHaveLength(101);
669
+ expect(result[100].author).toBe('last');
670
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
671
+ expect(fetchSpy).toHaveBeenNthCalledWith(
672
+ 2,
673
+ 'https://api.github.com/repos/HiromiShikata/test-repository/issues/42/comments?per_page=100&page=2',
674
+ expect.objectContaining({ method: 'GET' }),
675
+ );
676
+ });
677
+
678
+ it('should throw when the API responds with a non-2xx status', async () => {
679
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
680
+ new Response('Not Found', {
681
+ status: 404,
682
+ statusText: 'Not Found',
683
+ }),
684
+ );
685
+
686
+ const { repository } = createApiV3CheerioRestIssueRepository();
687
+ await expect(
688
+ repository.getIssueOrPullRequestComments(
689
+ 'https://github.com/HiromiShikata/test-repository/issues/42',
690
+ ),
691
+ ).rejects.toThrow('404');
692
+ });
693
+ });
694
+
695
+ describe('getPullRequestDetail', () => {
696
+ afterEach(() => {
697
+ jest.restoreAllMocks();
698
+ });
699
+
700
+ const detailResponse = {
701
+ title: 'PR title',
702
+ state: 'open',
703
+ merged: false,
704
+ draft: true,
705
+ additions: 10,
706
+ deletions: 3,
707
+ changed_files: 2,
708
+ head: { ref: 'feature/foo' },
709
+ base: { ref: 'main' },
710
+ user: { login: 'alice' },
711
+ body: 'pr body',
712
+ };
713
+
714
+ it('should fetch detail and paginated files for a pull request', async () => {
715
+ const fetchSpy = jest
716
+ .spyOn(global, 'fetch')
717
+ .mockResolvedValueOnce(
718
+ new Response(JSON.stringify(detailResponse), {
719
+ status: 200,
720
+ headers: { 'Content-Type': 'application/json' },
721
+ }),
722
+ )
723
+ .mockResolvedValueOnce(
724
+ new Response(
725
+ JSON.stringify([
726
+ {
727
+ filename: 'src/Foo.ts',
728
+ status: 'modified',
729
+ additions: 7,
730
+ deletions: 2,
731
+ patch: '@@ -1 +1 @@',
732
+ },
733
+ {
734
+ filename: 'src/Bar.ts',
735
+ status: 'added',
736
+ additions: 3,
737
+ deletions: 1,
738
+ },
739
+ ]),
740
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
741
+ ),
742
+ );
743
+
744
+ const { repository } = createApiV3CheerioRestIssueRepository();
745
+ const result = await repository.getPullRequestDetail(
746
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
747
+ );
748
+
749
+ expect(result).toEqual({
750
+ title: 'PR title',
751
+ state: 'open',
752
+ merged: false,
753
+ isDraft: true,
754
+ additions: 10,
755
+ deletions: 3,
756
+ changedFiles: 2,
757
+ headRefName: 'feature/foo',
758
+ baseRefName: 'main',
759
+ author: 'alice',
760
+ files: [
761
+ {
762
+ filename: 'src/Foo.ts',
763
+ status: 'modified',
764
+ additions: 7,
765
+ deletions: 2,
766
+ patch: '@@ -1 +1 @@',
767
+ },
768
+ {
769
+ filename: 'src/Bar.ts',
770
+ status: 'added',
771
+ additions: 3,
772
+ deletions: 1,
773
+ patch: null,
774
+ },
775
+ ],
776
+ });
777
+ expect(fetchSpy).toHaveBeenNthCalledWith(
778
+ 1,
779
+ 'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42',
780
+ expect.objectContaining({ method: 'GET' }),
781
+ );
782
+ expect(fetchSpy).toHaveBeenNthCalledWith(
783
+ 2,
784
+ 'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42/files?per_page=100&page=1',
785
+ expect.objectContaining({ method: 'GET' }),
786
+ );
787
+ });
788
+
789
+ it('should paginate the files list across two pages', async () => {
790
+ const firstPage: {
791
+ filename: string;
792
+ status: string;
793
+ additions: number;
794
+ deletions: number;
795
+ }[] = [];
796
+ for (let i = 0; i < 100; i += 1) {
797
+ firstPage.push({
798
+ filename: `src/file${i}.ts`,
799
+ status: 'modified',
800
+ additions: 1,
801
+ deletions: 0,
802
+ });
803
+ }
804
+ const secondPage = [
805
+ {
806
+ filename: 'src/extra.ts',
807
+ status: 'added',
808
+ additions: 2,
809
+ deletions: 0,
810
+ },
811
+ ];
812
+ const fetchSpy = jest
813
+ .spyOn(global, 'fetch')
814
+ .mockResolvedValueOnce(
815
+ new Response(JSON.stringify(detailResponse), {
816
+ status: 200,
817
+ headers: { 'Content-Type': 'application/json' },
818
+ }),
819
+ )
820
+ .mockResolvedValueOnce(
821
+ new Response(JSON.stringify(firstPage), {
822
+ status: 200,
823
+ headers: { 'Content-Type': 'application/json' },
824
+ }),
825
+ )
826
+ .mockResolvedValueOnce(
827
+ new Response(JSON.stringify(secondPage), {
828
+ status: 200,
829
+ headers: { 'Content-Type': 'application/json' },
830
+ }),
831
+ );
832
+
833
+ const { repository } = createApiV3CheerioRestIssueRepository();
834
+ const result = await repository.getPullRequestDetail(
835
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
836
+ );
837
+
838
+ expect(result?.files).toHaveLength(101);
839
+ expect(result?.files[100].filename).toBe('src/extra.ts');
840
+ expect(fetchSpy).toHaveBeenNthCalledWith(
841
+ 3,
842
+ 'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42/files?per_page=100&page=2',
843
+ expect.objectContaining({ method: 'GET' }),
844
+ );
845
+ });
846
+
847
+ it('should return null when the URL is not a pull request', async () => {
848
+ const fetchSpy = jest.spyOn(global, 'fetch');
849
+
850
+ const { repository } = createApiV3CheerioRestIssueRepository();
851
+ const result = await repository.getPullRequestDetail(
852
+ 'https://github.com/HiromiShikata/test-repository/issues/42',
853
+ );
854
+
855
+ expect(result).toBeNull();
856
+ expect(fetchSpy).not.toHaveBeenCalled();
857
+ });
858
+
859
+ it('should throw when the API responds with a non-2xx status', async () => {
860
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
861
+ new Response('Not Found', {
862
+ status: 404,
863
+ statusText: 'Not Found',
864
+ }),
865
+ );
866
+
867
+ const { repository } = createApiV3CheerioRestIssueRepository();
868
+ await expect(
869
+ repository.getPullRequestDetail(
870
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
871
+ ),
872
+ ).rejects.toThrow('404');
873
+ });
874
+ });
875
+
876
+ describe('getPullRequestCommits', () => {
877
+ afterEach(() => {
878
+ jest.restoreAllMocks();
879
+ });
880
+
881
+ it('should fetch a single page of commits', async () => {
882
+ const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValueOnce(
883
+ new Response(
884
+ JSON.stringify([
885
+ {
886
+ sha: 'abc123',
887
+ commit: {
888
+ message: 'first commit',
889
+ author: { name: 'Alice', date: '2024-01-01T00:00:00Z' },
890
+ },
891
+ },
892
+ ]),
893
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
894
+ ),
895
+ );
896
+
897
+ const { repository } = createApiV3CheerioRestIssueRepository();
898
+ const result = await repository.getPullRequestCommits(
899
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
900
+ );
901
+
902
+ expect(result).toEqual([
903
+ {
904
+ sha: 'abc123',
905
+ message: 'first commit',
906
+ author: 'Alice',
907
+ authoredAt: new Date('2024-01-01T00:00:00Z'),
908
+ },
909
+ ]);
910
+ expect(fetchSpy).toHaveBeenCalledWith(
911
+ 'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42/commits?per_page=100&page=1',
912
+ expect.objectContaining({ method: 'GET' }),
913
+ );
914
+ });
915
+
916
+ it('should paginate when a page returns exactly 100 entries', async () => {
917
+ const firstPage: {
918
+ sha: string;
919
+ commit: { message: string; author: { name: string; date: string } };
920
+ }[] = [];
921
+ for (let i = 0; i < 100; i += 1) {
922
+ firstPage.push({
923
+ sha: `sha${i}`,
924
+ commit: {
925
+ message: `commit ${i}`,
926
+ author: { name: 'Alice', date: '2024-01-01T00:00:00Z' },
927
+ },
928
+ });
929
+ }
930
+ const secondPage = [
931
+ {
932
+ sha: 'last-sha',
933
+ commit: {
934
+ message: 'last commit',
935
+ author: { name: 'Bob', date: '2024-02-01T00:00:00Z' },
936
+ },
937
+ },
938
+ ];
939
+ const fetchSpy = jest
940
+ .spyOn(global, 'fetch')
941
+ .mockResolvedValueOnce(
942
+ new Response(JSON.stringify(firstPage), {
943
+ status: 200,
944
+ headers: { 'Content-Type': 'application/json' },
945
+ }),
946
+ )
947
+ .mockResolvedValueOnce(
948
+ new Response(JSON.stringify(secondPage), {
949
+ status: 200,
950
+ headers: { 'Content-Type': 'application/json' },
951
+ }),
952
+ );
953
+
954
+ const { repository } = createApiV3CheerioRestIssueRepository();
955
+ const result = await repository.getPullRequestCommits(
956
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
957
+ );
958
+
959
+ expect(result).toHaveLength(101);
960
+ expect(result[100].sha).toBe('last-sha');
961
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
962
+ expect(fetchSpy).toHaveBeenNthCalledWith(
963
+ 2,
964
+ 'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42/commits?per_page=100&page=2',
965
+ expect.objectContaining({ method: 'GET' }),
966
+ );
967
+ });
968
+
969
+ it('should return an empty list when the URL is not a pull request', async () => {
970
+ const fetchSpy = jest.spyOn(global, 'fetch');
971
+
972
+ const { repository } = createApiV3CheerioRestIssueRepository();
973
+ const result = await repository.getPullRequestCommits(
974
+ 'https://github.com/HiromiShikata/test-repository/issues/42',
975
+ );
976
+
977
+ expect(result).toEqual([]);
978
+ expect(fetchSpy).not.toHaveBeenCalled();
979
+ });
980
+
981
+ it('should throw when the API responds with a non-2xx status', async () => {
982
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
983
+ new Response('Not Found', {
984
+ status: 404,
985
+ statusText: 'Not Found',
986
+ }),
987
+ );
988
+
989
+ const { repository } = createApiV3CheerioRestIssueRepository();
990
+ await expect(
991
+ repository.getPullRequestCommits(
992
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
993
+ ),
994
+ ).rejects.toThrow('404');
995
+ });
996
+ });
997
+
998
+ describe('getIssueOrPullRequestState', () => {
999
+ afterEach(() => {
1000
+ jest.restoreAllMocks();
1001
+ });
1002
+
1003
+ it('should fetch pull-request state and merged flag for a PR URL', async () => {
1004
+ const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValueOnce(
1005
+ new Response(
1006
+ JSON.stringify({
1007
+ title: 'PR title',
1008
+ state: 'closed',
1009
+ merged: true,
1010
+ draft: false,
1011
+ additions: 1,
1012
+ deletions: 1,
1013
+ changed_files: 1,
1014
+ head: { ref: 'feature/foo' },
1015
+ base: { ref: 'main' },
1016
+ user: { login: 'alice' },
1017
+ body: 'pr body',
1018
+ }),
1019
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
1020
+ ),
1021
+ );
1022
+
1023
+ const { repository } = createApiV3CheerioRestIssueRepository();
1024
+ const result = await repository.getIssueOrPullRequestState(
1025
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
1026
+ );
1027
+
1028
+ expect(result).toEqual({
1029
+ state: 'closed',
1030
+ merged: true,
1031
+ isPullRequest: true,
1032
+ });
1033
+ expect(fetchSpy).toHaveBeenCalledWith(
1034
+ 'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42',
1035
+ expect.objectContaining({ method: 'GET' }),
1036
+ );
1037
+ });
1038
+
1039
+ it('should fetch issue state with merged always false for an issue URL', async () => {
1040
+ const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValueOnce(
1041
+ new Response(JSON.stringify({ state: 'open' }), {
1042
+ status: 200,
1043
+ headers: { 'Content-Type': 'application/json' },
1044
+ }),
1045
+ );
1046
+
1047
+ const { repository } = createApiV3CheerioRestIssueRepository();
1048
+ const result = await repository.getIssueOrPullRequestState(
1049
+ 'https://github.com/HiromiShikata/test-repository/issues/42',
1050
+ );
1051
+
1052
+ expect(result).toEqual({
1053
+ state: 'open',
1054
+ merged: false,
1055
+ isPullRequest: false,
1056
+ });
1057
+ expect(fetchSpy).toHaveBeenCalledWith(
1058
+ 'https://api.github.com/repos/HiromiShikata/test-repository/issues/42',
1059
+ expect.objectContaining({ method: 'GET' }),
1060
+ );
1061
+ });
1062
+
1063
+ it('should throw when the API responds with a non-2xx status', async () => {
1064
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
1065
+ new Response('Not Found', {
1066
+ status: 404,
1067
+ statusText: 'Not Found',
1068
+ }),
1069
+ );
1070
+
1071
+ const { repository } = createApiV3CheerioRestIssueRepository();
1072
+ await expect(
1073
+ repository.getIssueOrPullRequestState(
1074
+ 'https://github.com/HiromiShikata/test-repository/issues/42',
1075
+ ),
1076
+ ).rejects.toThrow('404');
1077
+ });
1078
+ });
1079
+
1080
+ describe('getPullRequestSummary', () => {
1081
+ afterEach(() => {
1082
+ jest.restoreAllMocks();
1083
+ });
1084
+
1085
+ it('should fetch the title, body, and changed-line counts for a PR', async () => {
1086
+ const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValueOnce(
1087
+ new Response(
1088
+ JSON.stringify({
1089
+ title: 'PR title',
1090
+ state: 'open',
1091
+ merged: false,
1092
+ draft: false,
1093
+ additions: 12,
1094
+ deletions: 4,
1095
+ changed_files: 3,
1096
+ head: { ref: 'feature/foo' },
1097
+ base: { ref: 'main' },
1098
+ user: { login: 'alice' },
1099
+ body: 'pr body',
1100
+ }),
1101
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
1102
+ ),
1103
+ );
1104
+
1105
+ const { repository } = createApiV3CheerioRestIssueRepository();
1106
+ const result = await repository.getPullRequestSummary(
1107
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
1108
+ );
1109
+
1110
+ expect(result).toEqual({
1111
+ title: 'PR title',
1112
+ body: 'pr body',
1113
+ additions: 12,
1114
+ deletions: 4,
1115
+ changedFiles: 3,
1116
+ });
1117
+ expect(fetchSpy).toHaveBeenCalledWith(
1118
+ 'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42',
1119
+ expect.objectContaining({ method: 'GET' }),
1120
+ );
1121
+ });
1122
+
1123
+ it('should return null when the URL is not a pull request', async () => {
1124
+ const fetchSpy = jest.spyOn(global, 'fetch');
1125
+
1126
+ const { repository } = createApiV3CheerioRestIssueRepository();
1127
+ const result = await repository.getPullRequestSummary(
1128
+ 'https://github.com/HiromiShikata/test-repository/issues/42',
1129
+ );
1130
+
1131
+ expect(result).toBeNull();
1132
+ expect(fetchSpy).not.toHaveBeenCalled();
1133
+ });
1134
+
1135
+ it('should throw when the API responds with a non-2xx status', async () => {
1136
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
1137
+ new Response('Not Found', {
1138
+ status: 404,
1139
+ statusText: 'Not Found',
1140
+ }),
1141
+ );
1142
+
1143
+ const { repository } = createApiV3CheerioRestIssueRepository();
1144
+ await expect(
1145
+ repository.getPullRequestSummary(
1146
+ 'https://github.com/HiromiShikata/test-repository/pull/42',
1147
+ ),
1148
+ ).rejects.toThrow('404');
1149
+ });
1150
+ });
1151
+
522
1152
  const createApiV3CheerioRestIssueRepository = () => {
523
1153
  const apiV3IssueRepository = mock<ApiV3IssueRepository>();
524
1154
  const restIssueRepository = mock<RestIssueRepository>();