github-issue-tower-defence-management 1.125.4 → 1.126.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 (36) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/bin/adapter/entry-points/handlers/situationFileWriter.js +2 -1
  3. package/bin/adapter/entry-points/handlers/situationFileWriter.js.map +1 -1
  4. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +1 -1
  5. package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
  6. package/bin/adapter/repositories/githubGraphqlClient.js +3 -1
  7. package/bin/adapter/repositories/githubGraphqlClient.js.map +1 -1
  8. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +6 -3
  9. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  10. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +10 -1
  11. package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
  12. package/bin/domain/usecases/StartPreparationUseCase.js +20 -0
  13. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  14. package/package.json +1 -1
  15. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +36 -0
  16. package/src/adapter/entry-points/handlers/situationFileWriter.ts +1 -0
  17. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +36 -0
  18. package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +5 -2
  19. package/src/adapter/repositories/githubGraphqlClient.test.ts +55 -0
  20. package/src/adapter/repositories/githubGraphqlClient.ts +6 -0
  21. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +82 -1
  22. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +18 -10
  23. package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +54 -0
  24. package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +9 -0
  25. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +18 -0
  26. package/src/domain/usecases/StartPreparationUseCase.test.ts +244 -0
  27. package/src/domain/usecases/StartPreparationUseCase.ts +29 -0
  28. package/types/adapter/entry-points/handlers/situationFileWriter.d.ts.map +1 -1
  29. package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
  30. package/types/adapter/repositories/githubGraphqlClient.d.ts +2 -0
  31. package/types/adapter/repositories/githubGraphqlClient.d.ts.map +1 -1
  32. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  33. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +1 -0
  34. package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
  35. package/types/domain/usecases/StartPreparationUseCase.d.ts +1 -1
  36. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.125.4",
3
+ "version": "1.126.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -121,6 +121,42 @@ describe('writeSituationFile', () => {
121
121
  );
122
122
  });
123
123
 
124
+ it('excludes closed items without reactivation triggers from immediately actionable counts', async () => {
125
+ const issues = [
126
+ createIssue({
127
+ status: 'Awaiting quality check',
128
+ state: 'CLOSED',
129
+ isClosed: true,
130
+ }),
131
+ createIssue({
132
+ status: 'Awaiting quality check',
133
+ state: 'MERGED',
134
+ isClosed: true,
135
+ isPr: true,
136
+ }),
137
+ createIssue({ status: 'Awaiting quality check' }),
138
+ createIssue({
139
+ status: 'Awaiting workspace',
140
+ state: 'CLOSED',
141
+ isClosed: true,
142
+ }),
143
+ createIssue({ status: 'Awaiting workspace' }),
144
+ ];
145
+
146
+ await writeSituationFile({ ...baseParams, issues });
147
+
148
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
149
+ expect.any(String),
150
+ expect.stringContaining(
151
+ '"awaitingQualityCheckImmediatelyActionable":1',
152
+ ),
153
+ );
154
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
155
+ expect.any(String),
156
+ expect.stringContaining('"awaitingWorkspaceImmediatelyActionable":1'),
157
+ );
158
+ });
159
+
124
160
  it('counts preparation total correctly', async () => {
125
161
  const issues = [
126
162
  createIssue({ status: 'Preparation' }),
@@ -48,6 +48,7 @@ const toPercent = (used: number, total: number): number =>
48
48
  total > 0 ? Math.round((used / total) * 1000) / 10 : 0;
49
49
 
50
50
  const isImmediatelyActionable = (issue: Issue): boolean =>
51
+ !issue.isClosed &&
51
52
  issue.dependedIssueUrls.length === 0 &&
52
53
  issue.nextActionDate === null &&
53
54
  issue.nextActionHour === null;
@@ -71,6 +71,42 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
71
71
  expect(section).not.toContain('\u{FE0F}');
72
72
  });
73
73
 
74
+ it('appends the self-diagnosis guidance to the configured main template', () => {
75
+ const fallback = createFallback();
76
+ const composer = new ConfigurableSilentSessionMessageComposer(
77
+ {
78
+ ...noTemplates,
79
+ mainStalledMessage: 'CUSTOM_MAIN',
80
+ },
81
+ fallback,
82
+ );
83
+ const section = composer.composeMainStalledSection(600);
84
+ expect(section).toContain(
85
+ 'This reminder is delivered only to sessions that have no registered unanswered owner-call.',
86
+ );
87
+ expect(section).toContain(
88
+ 're-raise the pending request as a new owner-call in that format',
89
+ );
90
+ });
91
+
92
+ it('omits the self-diagnosis guidance from the configured stale-owner-call template section', () => {
93
+ const fallback = createFallback();
94
+ const composer = new ConfigurableSilentSessionMessageComposer(
95
+ {
96
+ ...noTemplates,
97
+ mainStalledStaleOwnerCallMessage: 'CUSTOM_STALE',
98
+ },
99
+ fallback,
100
+ );
101
+ const section = composer.composeMainStalledWithStaleOwnerCallSection(
102
+ 600,
103
+ 3600,
104
+ );
105
+ expect(section).not.toContain(
106
+ 'This reminder is delivered only to sessions that have no registered unanswered owner-call.',
107
+ );
108
+ });
109
+
74
110
  it('uses the fallback stale-owner-call section when no stale-owner-call template is configured', () => {
75
111
  const fallback = createFallback();
76
112
  const composer = new ConfigurableSilentSessionMessageComposer(
@@ -1,5 +1,8 @@
1
1
  import { SubAgentActivity } from '../../domain/entities/LiveSessionActivitySnapshot';
2
- import { composeOwnerCallFormatGuidance } from '../../domain/usecases/DefaultSilentSessionMessageComposer';
2
+ import {
3
+ composeMainStalledSelfDiagnosisGuidance,
4
+ composeOwnerCallFormatGuidance,
5
+ } from '../../domain/usecases/DefaultSilentSessionMessageComposer';
3
6
  import {
4
7
  SilentSessionMessageComposer,
5
8
  SubAgentStallSections,
@@ -36,7 +39,7 @@ export class ConfigurableSilentSessionMessageComposer implements SilentSessionMe
36
39
  return this.fallback.composeMainStalledSection(mainSilentSeconds);
37
40
  }
38
41
  return withReminderSentinel(
39
- `${this.templates.mainStalledMessage} ${composeOwnerCallFormatGuidance()}`,
42
+ `${this.templates.mainStalledMessage} ${composeOwnerCallFormatGuidance()} ${composeMainStalledSelfDiagnosisGuidance()}`,
40
43
  );
41
44
  };
42
45
 
@@ -14,8 +14,10 @@ jest.mock('ky', () => ({
14
14
  __esModule: true,
15
15
  }));
16
16
 
17
+ import net from 'node:net';
17
18
  import {
18
19
  GITHUB_GRAPHQL_ENDPOINT,
20
+ GITHUB_GRAPHQL_REQUEST_TIMEOUT_MS,
19
21
  RATE_LIMIT_SELECTION,
20
22
  extractGraphqlOperationName,
21
23
  fetchGithubGraphql,
@@ -247,5 +249,58 @@ describe('githubGraphqlClient', () => {
247
249
  expect(response.status).toBe(403);
248
250
  expect(consoleLogSpy).not.toHaveBeenCalled();
249
251
  });
252
+
253
+ it('has a default request timeout bound of 120 seconds', () => {
254
+ expect(GITHUB_GRAPHQL_REQUEST_TIMEOUT_MS).toBe(120_000);
255
+ });
256
+
257
+ it('passes an abort signal to fetch by default', async () => {
258
+ const fetchMock = jest
259
+ .fn()
260
+ .mockResolvedValue(new Response('{}', { status: 200 }));
261
+ global.fetch = fetchMock;
262
+ await fetchGithubGraphql({
263
+ ghToken: 'token-b',
264
+ query: 'query PullRequestStatus($a: Int!) { x }',
265
+ });
266
+ const call = getMockCallArguments(fetchMock, 0);
267
+ const init = expectRecord(call[1]);
268
+ expect(init.signal).toBeInstanceOf(AbortSignal);
269
+ });
270
+
271
+ it('rejects with a TimeoutError within the bound when the server never responds', async () => {
272
+ const realFetch = originalFetch;
273
+ const openSockets = new Set<net.Socket>();
274
+ const server = net.createServer((socket) => {
275
+ openSockets.add(socket);
276
+ socket.on('close', () => openSockets.delete(socket));
277
+ });
278
+ await new Promise<void>((resolve) =>
279
+ server.listen(0, '127.0.0.1', resolve),
280
+ );
281
+ const address = server.address();
282
+ if (address === null || typeof address === 'string') {
283
+ throw new Error('Expected the server to listen on a TCP port');
284
+ }
285
+ const localEndpoint = `http://127.0.0.1:${address.port}/`;
286
+ global.fetch = (_input: RequestInfo | URL, init?: RequestInit) =>
287
+ realFetch(localEndpoint, init);
288
+ try {
289
+ const startedAt = Date.now();
290
+ await expect(
291
+ fetchGithubGraphql({
292
+ ghToken: 'token-b',
293
+ query: 'query PullRequestStatus($a: Int!) { x }',
294
+ timeoutMs: 300,
295
+ }),
296
+ ).rejects.toMatchObject({ name: 'TimeoutError' });
297
+ expect(Date.now() - startedAt).toBeLessThan(4000);
298
+ } finally {
299
+ for (const socket of openSockets) {
300
+ socket.destroy();
301
+ }
302
+ await new Promise<void>((resolve) => server.close(() => resolve()));
303
+ }
304
+ });
250
305
  });
251
306
  });
@@ -2,6 +2,8 @@ import ky from 'ky';
2
2
 
3
3
  export const GITHUB_GRAPHQL_ENDPOINT = 'https://api.github.com/graphql';
4
4
 
5
+ export const GITHUB_GRAPHQL_REQUEST_TIMEOUT_MS = 120_000;
6
+
5
7
  export const RATE_LIMIT_SELECTION = 'rateLimit { cost remaining }';
6
8
 
7
9
  export type GithubGraphqlRateLimit = {
@@ -99,6 +101,7 @@ export const fetchGithubGraphql = async (params: {
99
101
  ghToken: string;
100
102
  query: string;
101
103
  variables?: Record<string, unknown>;
104
+ timeoutMs?: number;
102
105
  }): Promise<Response> => {
103
106
  const response = await fetch(GITHUB_GRAPHQL_ENDPOINT, {
104
107
  method: 'POST',
@@ -110,6 +113,9 @@ export const fetchGithubGraphql = async (params: {
110
113
  query: injectRateLimitSelection(params.query),
111
114
  variables: params.variables,
112
115
  }),
116
+ signal: AbortSignal.timeout(
117
+ params.timeoutMs ?? GITHUB_GRAPHQL_REQUEST_TIMEOUT_MS,
118
+ ),
113
119
  });
114
120
  if (response.ok) {
115
121
  const responseBody: unknown = await response
@@ -1981,6 +1981,8 @@ describe('ApiV3CheerioRestIssueRepository', () => {
1981
1981
  timeline?: () => Response | object;
1982
1982
  mergeability?: () => Response | object;
1983
1983
  slimPullRequest?: (variables: {
1984
+ owner?: string;
1985
+ repo?: string;
1984
1986
  prNumber: number;
1985
1987
  reviewThreadsAfter: string | null;
1986
1988
  }) => Response | object;
@@ -2003,6 +2005,8 @@ describe('ApiV3CheerioRestIssueRepository', () => {
2003
2005
  type GraphqlRequestBody = {
2004
2006
  query: string;
2005
2007
  variables: {
2008
+ owner?: string;
2009
+ repo?: string;
2006
2010
  prNumber: number;
2007
2011
  reviewThreadsAfter: string | null;
2008
2012
  };
@@ -2024,6 +2028,14 @@ describe('ApiV3CheerioRestIssueRepository', () => {
2024
2028
  typeof rawVariables === 'object' && rawVariables !== null
2025
2029
  ? rawVariables
2026
2030
  : {};
2031
+ const owner =
2032
+ 'owner' in variables && typeof variables.owner === 'string'
2033
+ ? variables.owner
2034
+ : undefined;
2035
+ const repo =
2036
+ 'repo' in variables && typeof variables.repo === 'string'
2037
+ ? variables.repo
2038
+ : undefined;
2027
2039
  const prNumber =
2028
2040
  'prNumber' in variables && typeof variables.prNumber === 'number'
2029
2041
  ? variables.prNumber
@@ -2033,7 +2045,10 @@ describe('ApiV3CheerioRestIssueRepository', () => {
2033
2045
  typeof variables.reviewThreadsAfter === 'string'
2034
2046
  ? variables.reviewThreadsAfter
2035
2047
  : null;
2036
- return { query: parsed.query, variables: { prNumber, reviewThreadsAfter } };
2048
+ return {
2049
+ query: parsed.query,
2050
+ variables: { owner, repo, prNumber, reviewThreadsAfter },
2051
+ };
2037
2052
  };
2038
2053
 
2039
2054
  const mockFetchRoutes = (routes: FetchRoutes) =>
@@ -2966,6 +2981,72 @@ describe('ApiV3CheerioRestIssueRepository', () => {
2966
2981
  });
2967
2982
  });
2968
2983
 
2984
+ describe('findRelatedOpenPRs cross-repo PR', () => {
2985
+ afterEach(() => {
2986
+ jest.restoreAllMocks();
2987
+ });
2988
+
2989
+ it('queries the PR repository owner and name when the PR is in a different repository than the issue', async () => {
2990
+ const capturedSlimVariables: Array<{
2991
+ owner?: string;
2992
+ repo?: string;
2993
+ prNumber: number;
2994
+ }> = [];
2995
+
2996
+ mockFetchRoutes({
2997
+ timeline: () => ({
2998
+ data: {
2999
+ repository: {
3000
+ issue: {
3001
+ timelineItems: {
3002
+ pageInfo: { endCursor: null, hasNextPage: false },
3003
+ nodes: [
3004
+ {
3005
+ __typename: 'CrossReferencedEvent',
3006
+ willCloseTarget: true,
3007
+ source: {
3008
+ __typename: 'PullRequest',
3009
+ url: 'https://github.com/HiromiShikata/secretary/pull/2751',
3010
+ number: 2751,
3011
+ state: 'OPEN',
3012
+ createdAt: '2024-01-01T00:00:00Z',
3013
+ isDraft: false,
3014
+ mergeable: 'MERGEABLE',
3015
+ headRefName: 'close-ufw-port-9981-i30106',
3016
+ baseRefName: 'main',
3017
+ baseRef: { name: 'main' },
3018
+ },
3019
+ },
3020
+ ],
3021
+ },
3022
+ },
3023
+ },
3024
+ },
3025
+ }),
3026
+ slimPullRequest: (variables) => {
3027
+ capturedSlimVariables.push(variables);
3028
+ return buildSlimPullRequestResponse({
3029
+ url: 'https://github.com/HiromiShikata/secretary/pull/2751',
3030
+ });
3031
+ },
3032
+ });
3033
+
3034
+ const { repository } = createApiV3CheerioRestIssueRepository();
3035
+ const result = await repository.findRelatedOpenPRs(
3036
+ 'https://github.com/HiromiShikata/umino-corporait-operation/issues/30106',
3037
+ );
3038
+
3039
+ expect(result).toHaveLength(1);
3040
+ expect(result[0].url).toBe(
3041
+ 'https://github.com/HiromiShikata/secretary/pull/2751',
3042
+ );
3043
+ expect(capturedSlimVariables).toHaveLength(1);
3044
+ expect(capturedSlimVariables[0].owner).toBe('HiromiShikata');
3045
+ expect(capturedSlimVariables[0].repo).toBe('secretary');
3046
+ expect(capturedSlimVariables[0].prNumber).toBe(2751);
3047
+ });
3048
+ });
3049
+
2969
3050
  const createApiV3CheerioRestIssueRepository = () => {
2970
3051
  const apiV3IssueRepository = mock<ApiV3IssueRepository>();
2971
3052
  const restIssueRepository = mock<RestIssueRepository>();
@@ -1493,6 +1493,10 @@ export class ApiV3CheerioRestIssueRepository
1493
1493
  const pr = item.source;
1494
1494
  const prUrl = pr.url || '';
1495
1495
 
1496
+ if (!prUrl) continue;
1497
+
1498
+ const { owner: prOwner, repo: prRepo } = this.parseIssueUrl(prUrl);
1499
+
1496
1500
  let isConflicted = pr.mergeable === 'CONFLICTING';
1497
1501
  let mergeable: string | null = pr.mergeable ?? null;
1498
1502
  if (
@@ -1507,8 +1511,8 @@ export class ApiV3CheerioRestIssueRepository
1507
1511
  } | null;
1508
1512
  try {
1509
1513
  resolved = await this.resolveMergeabilityWithRetry(
1510
- owner,
1511
- repo,
1514
+ prOwner,
1515
+ prRepo,
1512
1516
  pr.number,
1513
1517
  );
1514
1518
  } catch (error) {
@@ -1538,8 +1542,8 @@ export class ApiV3CheerioRestIssueRepository
1538
1542
  let prStatus: RelatedPullRequest;
1539
1543
  try {
1540
1544
  const slimPullRequest = await this.fetchSlimPullRequest(
1541
- owner,
1542
- repo,
1545
+ prOwner,
1546
+ prRepo,
1543
1547
  pr.number,
1544
1548
  );
1545
1549
  if (!slimPullRequest || slimPullRequest.state !== 'OPEN') {
@@ -1550,12 +1554,16 @@ export class ApiV3CheerioRestIssueRepository
1550
1554
  }
1551
1555
  const baseRefName =
1552
1556
  slimPullRequest.baseRefName ?? pr.baseRefName ?? pr.baseRef?.name;
1553
- prStatus = await this.buildRelatedPullRequestFromSlim(owner, repo, {
1554
- ...slimPullRequest,
1555
- url: slimPullRequest.url || prUrl,
1556
- headRefName: slimPullRequest.headRefName ?? pr.headRefName,
1557
- baseRefName,
1558
- });
1557
+ prStatus = await this.buildRelatedPullRequestFromSlim(
1558
+ prOwner,
1559
+ prRepo,
1560
+ {
1561
+ ...slimPullRequest,
1562
+ url: slimPullRequest.url || prUrl,
1563
+ headRefName: slimPullRequest.headRefName ?? pr.headRefName,
1564
+ baseRefName,
1565
+ },
1566
+ );
1559
1567
  } catch (error) {
1560
1568
  const errorMessage =
1561
1569
  error instanceof Error ? error.message : String(error);
@@ -99,6 +99,60 @@ describe('DefaultSilentSessionMessageComposer', () => {
99
99
  expect(section).not.toContain('>');
100
100
  });
101
101
 
102
+ it('explains in the main-stalled section that the reminder reaches only sessions without a registered unanswered owner-call', () => {
103
+ const section = composer.composeMainStalledSection(600);
104
+ expect(section).toContain(
105
+ 'This reminder is delivered only to sessions that have no registered unanswered owner-call.',
106
+ );
107
+ });
108
+
109
+ it('explains in the main-stalled section that receiving the reminder while believing an owner-call is pending means the call was not registered and the owner was not notified', () => {
110
+ const section = composer.composeMainStalledSection(600);
111
+ expect(section).toContain(
112
+ 'If you believe you have already raised an owner-call and are waiting for the owner',
113
+ );
114
+ expect(section).toContain(
115
+ 'receiving this reminder means that call was not registered',
116
+ );
117
+ expect(section).toContain('the owner has not been notified');
118
+ });
119
+
120
+ it('instructs in the main-stalled section to review the documented owner-call format and re-raise the pending request', () => {
121
+ const section = composer.composeMainStalledSection(600);
122
+ expect(section).toContain(
123
+ 'please review the documented owner-call format for this session',
124
+ );
125
+ expect(section).toContain(
126
+ 're-raise the pending request as a new owner-call in that format',
127
+ );
128
+ });
129
+
130
+ it('omits the self-diagnosis guidance from the stale-owner-call section', () => {
131
+ const section = composer.composeMainStalledWithStaleOwnerCallSection(
132
+ 600,
133
+ 3600,
134
+ );
135
+ expect(section).not.toContain(
136
+ 'This reminder is delivered only to sessions that have no registered unanswered owner-call.',
137
+ );
138
+ });
139
+
140
+ it('omits the self-diagnosis guidance from the sub-agent sections', () => {
141
+ const subAgent = {
142
+ label: 'sub-process-1',
143
+ silentSeconds: 360,
144
+ runningSeconds: 1200,
145
+ waitingOnExternalProcess: false,
146
+ };
147
+ const section = composer.composeSubAgentSection({
148
+ idleSubAgents: [subAgent],
149
+ longRunningSubAgents: [subAgent],
150
+ });
151
+ expect(section).not.toContain(
152
+ 'This reminder is delivered only to sessions that have no registered unanswered owner-call.',
153
+ );
154
+ });
155
+
102
156
  it('embeds the reminder sentinel in the stale-owner-call main-stalled section', () => {
103
157
  const section = composer.composeMainStalledWithStaleOwnerCallSection(
104
158
  600,
@@ -46,6 +46,14 @@ export const composeOwnerCallFormatGuidance = (): string => {
46
46
  return 'Please share it through a new owner-call in the format documented for this session, written to be self-contained so the owner can understand the situation from that single message.';
47
47
  };
48
48
 
49
+ export const composeMainStalledSelfDiagnosisGuidance = (): string => {
50
+ return [
51
+ 'This reminder is delivered only to sessions that have no registered unanswered owner-call.',
52
+ "If you believe you have already raised an owner-call and are waiting for the owner's reply, receiving this reminder means that call was not registered — its format or delivery method was incorrect — and the owner has not been notified.",
53
+ 'In that case, please review the documented owner-call format for this session and re-raise the pending request as a new owner-call in that format.',
54
+ ].join(' ');
55
+ };
56
+
49
57
  const composeMainStalledMessage = (mainSilentSeconds: number): string => {
50
58
  const minutes = Math.floor(mainSilentSeconds / 60);
51
59
  return [
@@ -54,6 +62,7 @@ const composeMainStalledMessage = (mainSilentSeconds: number): string => {
54
62
  `2. Run independent pieces of work in parallel across sub-agents.`,
55
63
  `3. Keep a monitor in place that notices when a sub-agent has produced no output for about 5 minutes.`,
56
64
  `4. When an owner decision is needed, or when work the owner asked for has been completed or answered, please share it through a new owner-call — a completion still awaits the owner's acknowledgment, so it is not a no-action case. The owner is notified only when an owner-call is raised. ${composeOwnerCallFormatGuidance()}`,
65
+ composeMainStalledSelfDiagnosisGuidance(),
57
66
  `Please also include in your next output an estimate of the remaining minutes to finish all tasks.`,
58
67
  ].join('\n');
59
68
  };
@@ -1056,6 +1056,24 @@ describe('HandleScheduledEventUseCase', () => {
1056
1056
  expect(mockIssueRepository.createCommentByUrl).not.toHaveBeenCalled();
1057
1057
  });
1058
1058
 
1059
+ it('should not create or comment an incident issue for a fetch AbortSignal timeout DOMException', async () => {
1060
+ const timeoutError = new DOMException(
1061
+ 'The operation was aborted due to timeout',
1062
+ 'TimeoutError',
1063
+ );
1064
+ mockRevertNotReadyReviewQueueIssueUseCase.run.mockRejectedValueOnce(
1065
+ timeoutError,
1066
+ );
1067
+
1068
+ await expect(useCase.run(errorInput)).rejects.toThrow(
1069
+ 'The operation was aborted due to timeout',
1070
+ );
1071
+
1072
+ expect(mockIssueRepository.searchIssue).not.toHaveBeenCalled();
1073
+ expect(mockIssueRepository.createNewIssue).not.toHaveBeenCalled();
1074
+ expect(mockIssueRepository.createCommentByUrl).not.toHaveBeenCalled();
1075
+ });
1076
+
1059
1077
  it('should not create or comment an incident issue for a ky TimeoutError (matched by error name)', async () => {
1060
1078
  const timeoutError = new Error(
1061
1079
  'Request timed out: POST https://api.github.com/graphql',