github-issue-tower-defence-management 1.172.2 → 1.173.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +2 -0
  3. package/bin/adapter/entry-points/console/consoleReadApi.js +126 -48
  4. package/bin/adapter/entry-points/console/consoleReadApi.js.map +1 -1
  5. package/bin/adapter/entry-points/console/ui-dist/assets/{index-ZYMkO0fm.js → index-BcnLfodS.js} +1 -1
  6. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
  8. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +19 -17
  10. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  11. package/bin/adapter/repositories/issue/githubRateLimitRetry.js +8 -1
  12. package/bin/adapter/repositories/issue/githubRateLimitRetry.js.map +1 -1
  13. package/bin/domain/usecases/HandleScheduledEventUseCase.js +17 -3
  14. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  15. package/bin/domain/usecases/QualityCheckAdvanceUseCase.js +50 -0
  16. package/bin/domain/usecases/QualityCheckAdvanceUseCase.js.map +1 -0
  17. package/package.json +1 -1
  18. package/src/adapter/entry-points/console/consoleReadApi.test.ts +254 -0
  19. package/src/adapter/entry-points/console/consoleReadApi.ts +133 -55
  20. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +10 -0
  21. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +14 -1
  22. package/src/adapter/entry-points/console/ui-dist/assets/{index-ZYMkO0fm.js → index-BcnLfodS.js} +1 -1
  23. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  24. package/src/adapter/entry-points/console/webServer.test.ts +35 -0
  25. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +6 -0
  26. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +47 -19
  27. package/src/adapter/repositories/issue/githubRateLimitRetry.ts +4 -0
  28. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +116 -0
  29. package/src/domain/usecases/HandleScheduledEventUseCase.ts +21 -0
  30. package/src/domain/usecases/QualityCheckAdvanceUseCase.test.ts +387 -0
  31. package/src/domain/usecases/QualityCheckAdvanceUseCase.ts +75 -0
  32. package/types/adapter/entry-points/console/consoleReadApi.d.ts +2 -0
  33. package/types/adapter/entry-points/console/consoleReadApi.d.ts.map +1 -1
  34. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  35. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
  36. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  37. package/types/adapter/repositories/issue/githubRateLimitRetry.d.ts +3 -0
  38. package/types/adapter/repositories/issue/githubRateLimitRetry.d.ts.map +1 -1
  39. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +5 -2
  40. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  41. package/types/domain/usecases/QualityCheckAdvanceUseCase.d.ts +14 -0
  42. package/types/domain/usecases/QualityCheckAdvanceUseCase.d.ts.map +1 -0
@@ -74,6 +74,7 @@ import { GitHubIssueCommentRepository } from '../../repositories/GitHubIssueComm
74
74
  import { ProjectRequiredFieldCreateUseCase } from '../../../domain/usecases/ProjectRequiredFieldCreateUseCase';
75
75
  import { SetupTowerDefenceProjectUseCase } from '../../../domain/usecases/SetupTowerDefenceProjectUseCase';
76
76
  import { DailySecurityScanUseCase } from '../../../domain/usecases/DailySecurityScanUseCase';
77
+ import { QualityCheckAdvanceUseCase } from '../../../domain/usecases/QualityCheckAdvanceUseCase';
77
78
  import { KyHttpRepository } from '../../repositories/KyHttpRepository';
78
79
  import { FileSystemKevReportWatermarkRepository } from '../../repositories/FileSystemKevReportWatermarkRepository';
79
80
  import {
@@ -507,6 +508,10 @@ export class HandleScheduledEventUseCaseHandler {
507
508
  )
508
509
  : null;
509
510
 
511
+ const qualityCheckAdvanceUseCase = new QualityCheckAdvanceUseCase(
512
+ issueRepository,
513
+ );
514
+
510
515
  const handleScheduledEventUseCase = new HandleScheduledEventUseCase(
511
516
  projectRequiredFieldCreateUseCase,
512
517
  setupTowerDefenceProjectUseCase,
@@ -531,6 +536,7 @@ export class HandleScheduledEventUseCaseHandler {
531
536
  agentDesignationLabelAdoptUseCase,
532
537
  updateRateLimitCacheUseCase,
533
538
  dailySecurityScanUseCase,
539
+ qualityCheckAdvanceUseCase,
534
540
  systemDateRepository,
535
541
  googleSpreadsheetRepository,
536
542
  projectRepository,
@@ -40,6 +40,7 @@ import {
40
40
  fetchWithGitHubRateLimitRetry,
41
41
  computeRateLimitResetIso,
42
42
  hasRateLimitSignals,
43
+ GitHubRateLimitError,
43
44
  } from './githubRateLimitRetry';
44
45
 
45
46
  export const FULL_ISSUE_FETCH_INTERVAL_MS = 60 * 60 * 1000;
@@ -593,6 +594,24 @@ export class ApiV3CheerioRestIssueRepository
593
594
  request: () => Promise<Response>,
594
595
  ): Promise<Response> => fetchWithGitHubRateLimitRetry(request, this.sleep);
595
596
 
597
+ private throwGitHubError = async (
598
+ prefix: string,
599
+ response: Response,
600
+ ): Promise<never> => {
601
+ const bodyText = await response.clone().text();
602
+ const isRateLimit = hasRateLimitSignals(
603
+ response.status,
604
+ response.headers,
605
+ bodyText,
606
+ );
607
+ const formatted = await this.formatGitHubErrorWithStatus(response);
608
+ const message = `${prefix}: ${formatted}`;
609
+ if (isRateLimit) {
610
+ throw new GitHubRateLimitError(message);
611
+ }
612
+ throw new Error(message);
613
+ };
614
+
596
615
  updateStatus: (
597
616
  project: Project,
598
617
  issue: Issue,
@@ -1786,8 +1805,9 @@ export class ApiV3CheerioRestIssueRepository
1786
1805
  );
1787
1806
 
1788
1807
  if (!response.ok) {
1789
- throw new Error(
1790
- `Failed to fetch issue timeline from GitHub GraphQL API: HTTP ${response.status}`,
1808
+ await this.throwGitHubError(
1809
+ 'Failed to fetch issue timeline from GitHub GraphQL API',
1810
+ response,
1791
1811
  );
1792
1812
  }
1793
1813
 
@@ -1992,9 +2012,9 @@ export class ApiV3CheerioRestIssueRepository
1992
2012
  return null;
1993
2013
  }
1994
2014
  if (!response.ok) {
1995
- const reason = await this.formatGitHubErrorWithStatus(response);
1996
- throw new Error(
1997
- `Failed to fetch pull request status for ${prUrl}: ${reason}`,
2015
+ await this.throwGitHubError(
2016
+ `Failed to fetch pull request status for ${prUrl}`,
2017
+ response,
1998
2018
  );
1999
2019
  }
2000
2020
  const body: unknown = await response.json();
@@ -2747,8 +2767,7 @@ export class ApiV3CheerioRestIssueRepository
2747
2767
  getIssueOrPullRequestBody = async (url: string): Promise<string> => {
2748
2768
  const response = await this.fetchIssueBodyResponse(url);
2749
2769
  if (!response.ok) {
2750
- const reason = await this.formatGitHubErrorWithStatus(response);
2751
- throw new Error(`Failed to fetch body for ${url}: ${reason}`);
2770
+ await this.throwGitHubError(`Failed to fetch body for ${url}`, response);
2752
2771
  }
2753
2772
  return this.parseIssueBodyResponse(url, response);
2754
2773
  };
@@ -2787,8 +2806,10 @@ export class ApiV3CheerioRestIssueRepository
2787
2806
  ),
2788
2807
  );
2789
2808
  if (!response.ok) {
2790
- const reason = await this.formatGitHubErrorWithStatus(response);
2791
- throw new Error(`Failed to fetch comments for ${url}: ${reason}`);
2809
+ await this.throwGitHubError(
2810
+ `Failed to fetch comments for ${url}`,
2811
+ response,
2812
+ );
2792
2813
  }
2793
2814
  const body: unknown = await response.json();
2794
2815
  if (!isIssueCommentsResponse(body)) {
@@ -2837,8 +2858,10 @@ export class ApiV3CheerioRestIssueRepository
2837
2858
  ),
2838
2859
  );
2839
2860
  if (!detailResponse.ok) {
2840
- const reason = await this.formatGitHubErrorWithStatus(detailResponse);
2841
- throw new Error(`Failed to fetch detail for PR ${prUrl}: ${reason}`);
2861
+ await this.throwGitHubError(
2862
+ `Failed to fetch detail for PR ${prUrl}`,
2863
+ detailResponse,
2864
+ );
2842
2865
  }
2843
2866
  const detailBody: unknown = await detailResponse.json();
2844
2867
  if (!isPullRequestDetailResponse(detailBody)) {
@@ -2891,8 +2914,10 @@ export class ApiV3CheerioRestIssueRepository
2891
2914
  ),
2892
2915
  );
2893
2916
  if (!response.ok) {
2894
- const reason = await this.formatGitHubErrorWithStatus(response);
2895
- throw new Error(`Failed to fetch files for PR ${prUrl}: ${reason}`);
2917
+ await this.throwGitHubError(
2918
+ `Failed to fetch files for PR ${prUrl}`,
2919
+ response,
2920
+ );
2896
2921
  }
2897
2922
  const body: unknown = await response.json();
2898
2923
  if (!isPullRequestDetailFilesResponse(body)) {
@@ -2952,8 +2977,10 @@ export class ApiV3CheerioRestIssueRepository
2952
2977
  ),
2953
2978
  );
2954
2979
  if (!response.ok) {
2955
- const reason = await this.formatGitHubErrorWithStatus(response);
2956
- throw new Error(`Failed to fetch commits for PR ${prUrl}: ${reason}`);
2980
+ await this.throwGitHubError(
2981
+ `Failed to fetch commits for PR ${prUrl}`,
2982
+ response,
2983
+ );
2957
2984
  }
2958
2985
  const body: unknown = await response.json();
2959
2986
  if (!isPullRequestCommitsResponse(body)) {
@@ -3001,8 +3028,10 @@ export class ApiV3CheerioRestIssueRepository
3001
3028
  ),
3002
3029
  );
3003
3030
  if (!response.ok) {
3004
- const reason = await this.formatGitHubErrorWithStatus(response);
3005
- throw new Error(`Failed to fetch state for ${url}: ${reason}`);
3031
+ await this.throwGitHubError(
3032
+ `Failed to fetch state for ${url}`,
3033
+ response,
3034
+ );
3006
3035
  }
3007
3036
  const body: unknown = await response.json();
3008
3037
  if (!isPullRequestDetailResponse(body)) {
@@ -3030,8 +3059,7 @@ export class ApiV3CheerioRestIssueRepository
3030
3059
  ),
3031
3060
  );
3032
3061
  if (!response.ok) {
3033
- const reason = await this.formatGitHubErrorWithStatus(response);
3034
- throw new Error(`Failed to fetch state for ${url}: ${reason}`);
3062
+ await this.throwGitHubError(`Failed to fetch state for ${url}`, response);
3035
3063
  }
3036
3064
  const body: unknown = await response.json();
3037
3065
  if (!isIssueOrPullRequestStateResponse(body)) {
@@ -1,3 +1,7 @@
1
+ export class GitHubRateLimitError extends Error {
2
+ readonly name = 'GitHubRateLimitError';
3
+ }
4
+
1
5
  export const RATE_LIMIT_MAX_RETRIES = 3;
2
6
  export const RATE_LIMIT_TOTAL_BACKOFF_CAP_MS = 5000;
3
7
  export const RATE_LIMIT_BASE_BACKOFF_MS = 250;
@@ -29,6 +29,7 @@ import { ProjectRequiredFieldCreateUseCase } from './ProjectRequiredFieldCreateU
29
29
  import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCase';
30
30
  import { UpdateRateLimitCacheUseCase } from './UpdateRateLimitCacheUseCase';
31
31
  import { DailySecurityScanUseCase } from './DailySecurityScanUseCase';
32
+ import { QualityCheckAdvanceUseCase } from './QualityCheckAdvanceUseCase';
32
33
 
33
34
  describe('HandleScheduledEventUseCase', () => {
34
35
  describe('createTargetDateTimes', () => {
@@ -128,6 +129,7 @@ describe('HandleScheduledEventUseCase', () => {
128
129
  mock<AgentDesignationLabelAdoptUseCase>();
129
130
  const mockUpdateRateLimitCacheUseCase = mock<UpdateRateLimitCacheUseCase>();
130
131
  const mockDailySecurityScanUseCase = mock<DailySecurityScanUseCase>();
132
+ const mockAdvanceQualityCheckUseCase = mock<QualityCheckAdvanceUseCase>();
131
133
  const mockDateRepository = mock<DateRepository>();
132
134
  const mockSpreadsheetRepository = mock<SpreadsheetRepository>();
133
135
  const mockProjectRepository = mock<ProjectRepository>();
@@ -157,6 +159,7 @@ describe('HandleScheduledEventUseCase', () => {
157
159
  mockAgentDesignationLabelAdoptUseCase,
158
160
  mockUpdateRateLimitCacheUseCase,
159
161
  mockDailySecurityScanUseCase,
162
+ mockAdvanceQualityCheckUseCase,
160
163
  mockDateRepository,
161
164
  mockSpreadsheetRepository,
162
165
  mockProjectRepository,
@@ -1473,5 +1476,118 @@ describe('HandleScheduledEventUseCase', () => {
1473
1476
  expect(lastExecutionWrite).toBeUndefined();
1474
1477
  });
1475
1478
  });
1479
+
1480
+ describe('autoAdvanceQualityCheckEnabled', () => {
1481
+ const baseInput = {
1482
+ projectName: 'test-project',
1483
+ org: 'test-org',
1484
+ projectUrl: 'https://github.com/test-org/test-project',
1485
+ manager: 'test-manager',
1486
+ workingReport: {
1487
+ repo: 'test-repo',
1488
+ members: ['member1'],
1489
+ spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
1490
+ },
1491
+ urlOfStoryView: 'https://github.com/test-org/test-project/issues',
1492
+ disabled: false,
1493
+ };
1494
+
1495
+ it('does not call advanceQualityCheckUseCase when autoAdvanceQualityCheckEnabled is absent', async () => {
1496
+ await useCase.run(baseInput);
1497
+
1498
+ expect(mockAdvanceQualityCheckUseCase.run).not.toHaveBeenCalled();
1499
+ });
1500
+
1501
+ it('does not call advanceQualityCheckUseCase when autoAdvanceQualityCheckEnabled is false', async () => {
1502
+ await useCase.run({
1503
+ ...baseInput,
1504
+ startPreparation: {
1505
+ defaultAgentName: 'agent1',
1506
+ configFilePath: '/path/to/config.yml',
1507
+ maximumPreparingIssuesCount: null,
1508
+ autoAdvanceQualityCheckEnabled: false,
1509
+ },
1510
+ });
1511
+
1512
+ expect(mockAdvanceQualityCheckUseCase.run).not.toHaveBeenCalled();
1513
+ });
1514
+
1515
+ it('calls advanceQualityCheckUseCase when autoAdvanceQualityCheckEnabled is true', async () => {
1516
+ const mockProject = mock<Project>();
1517
+ const mockIssues: Issue[] = [];
1518
+ const fixedNow = new Date('2024-06-15T12:00:00Z');
1519
+ mockDateRepository.now.mockResolvedValue(fixedNow);
1520
+ mockIssueRepository.getAllIssues.mockResolvedValue({
1521
+ issues: mockIssues,
1522
+ project: mockProject,
1523
+ cacheUsed: false,
1524
+ });
1525
+
1526
+ await useCase.run({
1527
+ ...baseInput,
1528
+ startPreparation: {
1529
+ defaultAgentName: 'agent1',
1530
+ configFilePath: '/path/to/config.yml',
1531
+ maximumPreparingIssuesCount: null,
1532
+ autoAdvanceQualityCheckEnabled: true,
1533
+ },
1534
+ });
1535
+
1536
+ expect(mockAdvanceQualityCheckUseCase.run).toHaveBeenCalledWith(
1537
+ expect.objectContaining({
1538
+ project: mockProject,
1539
+ issues: mockIssues,
1540
+ evaluatedAt: fixedNow,
1541
+ }),
1542
+ );
1543
+ });
1544
+
1545
+ it('passes awaitingQualityCheckStatus to advanceQualityCheckUseCase when configured', async () => {
1546
+ const mockProject = mock<Project>();
1547
+ mockIssueRepository.getAllIssues.mockResolvedValue({
1548
+ issues: [],
1549
+ project: mockProject,
1550
+ cacheUsed: false,
1551
+ });
1552
+
1553
+ await useCase.run({
1554
+ ...baseInput,
1555
+ startPreparation: {
1556
+ defaultAgentName: 'agent1',
1557
+ configFilePath: '/path/to/config.yml',
1558
+ maximumPreparingIssuesCount: null,
1559
+ autoAdvanceQualityCheckEnabled: true,
1560
+ awaitingQualityCheckStatus: 'Custom Review Status',
1561
+ },
1562
+ });
1563
+
1564
+ expect(mockAdvanceQualityCheckUseCase.run).toHaveBeenCalledWith(
1565
+ expect.objectContaining({
1566
+ awaitingQualityCheckStatusName: 'Custom Review Status',
1567
+ }),
1568
+ );
1569
+ });
1570
+
1571
+ it('continues to startPreparationUseCase when qualityCheckAdvanceUseCase.run rejects', async () => {
1572
+ mockAdvanceQualityCheckUseCase.run.mockRejectedValue(
1573
+ new AggregateError(
1574
+ [new Error('GitHub API rate limit')],
1575
+ 'Failed to advance 1 issue(s) from Awaiting Quality Check to Done',
1576
+ ),
1577
+ );
1578
+
1579
+ await useCase.run({
1580
+ ...baseInput,
1581
+ startPreparation: {
1582
+ defaultAgentName: 'agent1',
1583
+ configFilePath: '/path/to/config.yml',
1584
+ maximumPreparingIssuesCount: null,
1585
+ autoAdvanceQualityCheckEnabled: true,
1586
+ },
1587
+ });
1588
+
1589
+ expect(mockStartPreparationUseCase.run).toHaveBeenCalled();
1590
+ });
1591
+ });
1476
1592
  });
1477
1593
  });
@@ -37,6 +37,7 @@ import {
37
37
  DailySecurityScanConfig,
38
38
  DailySecurityScanUseCase,
39
39
  } from './DailySecurityScanUseCase';
40
+ import { QualityCheckAdvanceUseCase } from './QualityCheckAdvanceUseCase';
40
41
 
41
42
  export class ProjectNotFoundError extends Error {
42
43
  constructor(message: string) {
@@ -139,6 +140,7 @@ export class HandleScheduledEventUseCase {
139
140
  readonly agentDesignationLabelAdoptUseCase: AgentDesignationLabelAdoptUseCase,
140
141
  readonly updateRateLimitCacheUseCase: UpdateRateLimitCacheUseCase | null,
141
142
  readonly dailySecurityScanUseCase: DailySecurityScanUseCase | null,
143
+ readonly qualityCheckAdvanceUseCase: QualityCheckAdvanceUseCase,
142
144
  readonly dateRepository: DateRepository,
143
145
  readonly spreadsheetRepository: SpreadsheetRepository,
144
146
  readonly projectRepository: ProjectRepository,
@@ -177,6 +179,7 @@ export class HandleScheduledEventUseCase {
177
179
  awLogDirectoryPath?: string;
178
180
  awLogStaleThresholdMinutes?: number;
179
181
  awaitingQualityCheckStatus?: string | null;
182
+ autoAdvanceQualityCheckEnabled?: boolean;
180
183
  labelsAsLlmAgentName?: string[] | null;
181
184
  } | null;
182
185
  thresholdForAutoReject?: number;
@@ -333,6 +336,7 @@ export class HandleScheduledEventUseCase {
333
336
  targetDateTimes,
334
337
  storyIssues,
335
338
  runSlowSweep,
339
+ now,
336
340
  );
337
341
  rotationOrder = useCaseResult.rotationOrder;
338
342
  } catch (e) {
@@ -392,6 +396,7 @@ ${JSON.stringify(e)}
392
396
  targetDateTimes: Date[],
393
397
  storyObjectMap: StoryObjectMap,
394
398
  runSlowSweep: boolean,
399
+ now: Date,
395
400
  ): Promise<{ rotationOrder: RotationOrderEntry[] | null }> => {
396
401
  if (runSlowSweep) {
397
402
  await this.runSlowSweepUseCases(
@@ -462,6 +467,22 @@ ${JSON.stringify(e)}
462
467
  allowedIssueAuthors,
463
468
  });
464
469
  }
470
+ if (input.startPreparation.autoAdvanceQualityCheckEnabled) {
471
+ try {
472
+ await this.qualityCheckAdvanceUseCase.run({
473
+ project,
474
+ issues,
475
+ awaitingQualityCheckStatusName:
476
+ input.startPreparation.awaitingQualityCheckStatus ?? undefined,
477
+ evaluatedAt: now,
478
+ });
479
+ } catch (advanceError) {
480
+ console.error(
481
+ `[HandleScheduledEvent] Failed to advance quality check items for project ${project.url}: ${advanceError instanceof Error ? advanceError.message : String(advanceError)}`,
482
+ advanceError,
483
+ );
484
+ }
485
+ }
465
486
  const preparationResult = await this.startPreparationUseCase.run({
466
487
  projectUrl: input.projectUrl,
467
488
  defaultAgentName: input.startPreparation.defaultAgentName,