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.
- package/CHANGELOG.md +21 -0
- package/bin/adapter/entry-points/handlers/situationFileWriter.js +2 -1
- package/bin/adapter/entry-points/handlers/situationFileWriter.js.map +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
- package/bin/adapter/repositories/githubGraphqlClient.js +3 -1
- package/bin/adapter/repositories/githubGraphqlClient.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +6 -3
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +10 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +20 -0
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +36 -0
- package/src/adapter/entry-points/handlers/situationFileWriter.ts +1 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +36 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +5 -2
- package/src/adapter/repositories/githubGraphqlClient.test.ts +55 -0
- package/src/adapter/repositories/githubGraphqlClient.ts +6 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +82 -1
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +18 -10
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +54 -0
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +9 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +18 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +244 -0
- package/src/domain/usecases/StartPreparationUseCase.ts +29 -0
- package/types/adapter/entry-points/handlers/situationFileWriter.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/adapter/repositories/githubGraphqlClient.d.ts +2 -0
- package/types/adapter/repositories/githubGraphqlClient.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +1 -0
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
1511
|
-
|
|
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
|
-
|
|
1542
|
-
|
|
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(
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
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',
|