github-issue-tower-defence-management 1.180.1 → 1.181.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 +13 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BUFcVUVD.js +86 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/repositories/GoogleSpreadsheetRepository.js +41 -12
- package/bin/adapter/repositories/GoogleSpreadsheetRepository.js.map +1 -1
- package/bin/domain/usecases/extractFencedJsonBlocks.js +18 -0
- package/bin/domain/usecases/extractFencedJsonBlocks.js.map +1 -0
- package/bin/domain/usecases/extractNextStepAgent.js +14 -23
- package/bin/domain/usecases/extractNextStepAgent.js.map +1 -1
- package/bin/domain/usecases/isPullRequestDeclaredUnnecessary.js +10 -17
- package/bin/domain/usecases/isPullRequestDeclaredUnnecessary.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOkAndAwaitingWorkspaceActions.stories.tsx +25 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOkAndAwaitingWorkspaceActions.test.tsx +50 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOkAndAwaitingWorkspaceActions.tsx +41 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.stories.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.test.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleOperationMenu.tsx +5 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +70 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +29 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/actionToast.test.ts +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/actionToast.ts +5 -1
- package/src/adapter/entry-points/console/ui/src/features/console/logic/operations.ts +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +7 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-BUFcVUVD.js +86 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/repositories/GoogleSpreadsheetRepository.test.ts +47 -0
- package/src/adapter/repositories/GoogleSpreadsheetRepository.ts +76 -36
- package/src/domain/usecases/extractFencedJsonBlocks.test.ts +40 -0
- package/src/domain/usecases/extractFencedJsonBlocks.ts +21 -0
- package/src/domain/usecases/extractNextStepAgent.test.ts +19 -0
- package/src/domain/usecases/extractNextStepAgent.ts +15 -27
- package/src/domain/usecases/isPullRequestDeclaredUnnecessary.test.ts +33 -0
- package/src/domain/usecases/isPullRequestDeclaredUnnecessary.ts +13 -21
- package/types/adapter/repositories/GoogleSpreadsheetRepository.d.ts +3 -1
- package/types/adapter/repositories/GoogleSpreadsheetRepository.d.ts.map +1 -1
- package/types/domain/usecases/extractFencedJsonBlocks.d.ts +2 -0
- package/types/domain/usecases/extractFencedJsonBlocks.d.ts.map +1 -0
- package/types/domain/usecases/extractNextStepAgent.d.ts.map +1 -1
- package/types/domain/usecases/isPullRequestDeclaredUnnecessary.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-I_2Yu8fM.js +0 -86
- package/src/adapter/entry-points/console/ui-dist/assets/index-I_2Yu8fM.js +0 -86
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>TDPM Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-BUFcVUVD.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-DeZcPNCr.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -215,6 +215,53 @@ describe('GoogleSpreadsheetRepository', () => {
|
|
|
215
215
|
|
|
216
216
|
expect(result).toEqual([['1', 'true', 'text']]);
|
|
217
217
|
});
|
|
218
|
+
|
|
219
|
+
test('retries once after a quota error and returns the value on success', async () => {
|
|
220
|
+
mockSpreadsheetsGet
|
|
221
|
+
.mockRejectedValueOnce(
|
|
222
|
+
new Error('Quota exceeded for quota metric Read requests'),
|
|
223
|
+
)
|
|
224
|
+
.mockResolvedValueOnce({
|
|
225
|
+
status: 200,
|
|
226
|
+
data: {
|
|
227
|
+
sheets: [{ properties: { title: 'RetrySheet' } }],
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
mockSpreadsheetsValuesGet.mockResolvedValue({
|
|
231
|
+
status: 200,
|
|
232
|
+
data: { values: [['retried']] },
|
|
233
|
+
});
|
|
234
|
+
const repositoryWithFastSleep = new GoogleSpreadsheetRepository(
|
|
235
|
+
localStorageRepository,
|
|
236
|
+
'dummy-service-account-key',
|
|
237
|
+
() => mockSheetsClient,
|
|
238
|
+
() => Promise.resolve(),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const result = await repositoryWithFastSleep.getSheet(
|
|
242
|
+
spreadsheetUrl,
|
|
243
|
+
'RetrySheet',
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
expect(result).toEqual([['retried']]);
|
|
247
|
+
expect(mockSpreadsheetsGet).toHaveBeenCalledTimes(2);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('throws after exhausting all retries', async () => {
|
|
251
|
+
mockSpreadsheetsGet.mockRejectedValue(
|
|
252
|
+
new Error('Quota exceeded for quota metric Read requests'),
|
|
253
|
+
);
|
|
254
|
+
const repositoryWithFastSleep = new GoogleSpreadsheetRepository(
|
|
255
|
+
localStorageRepository,
|
|
256
|
+
'dummy-service-account-key',
|
|
257
|
+
() => mockSheetsClient,
|
|
258
|
+
() => Promise.resolve(),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
await expect(
|
|
262
|
+
repositoryWithFastSleep.getSheet(spreadsheetUrl, 'AnySheet'),
|
|
263
|
+
).rejects.toThrow('Quota exceeded');
|
|
264
|
+
});
|
|
218
265
|
});
|
|
219
266
|
|
|
220
267
|
describe('updateCell', () => {
|
|
@@ -44,13 +44,17 @@ interface SheetsApiClient {
|
|
|
44
44
|
export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
45
45
|
keyFile = './tmp/service-account-key.json';
|
|
46
46
|
private readonly sheetsClient: SheetsApiClient;
|
|
47
|
+
private readonly sleepFn: (ms: number) => Promise<void>;
|
|
47
48
|
|
|
48
49
|
constructor(
|
|
49
50
|
readonly localStorageRepository: LocalStorageRepository,
|
|
50
51
|
serviceAccountKey: string = process.env.GOOGLE_SERVICE_ACCOUNT_KEY ||
|
|
51
52
|
'dummy',
|
|
52
53
|
sheetsClientFactory?: () => SheetsApiClient,
|
|
54
|
+
sleepFn: (ms: number) => Promise<void> = (ms) =>
|
|
55
|
+
new Promise((resolve) => setTimeout(resolve, ms)),
|
|
53
56
|
) {
|
|
57
|
+
this.sleepFn = sleepFn;
|
|
54
58
|
this.localStorageRepository.write(this.keyFile, serviceAccountKey);
|
|
55
59
|
this.sheetsClient = sheetsClientFactory
|
|
56
60
|
? sheetsClientFactory()
|
|
@@ -93,6 +97,30 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
|
93
97
|
})();
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
private callWithRetry = async <T>(fn: () => Promise<T>): Promise<T> => {
|
|
101
|
+
const backoffMs = [1000, 2000, 4000];
|
|
102
|
+
let lastError: unknown;
|
|
103
|
+
for (let attempt = 0; attempt <= backoffMs.length; attempt++) {
|
|
104
|
+
try {
|
|
105
|
+
return await fn();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
lastError = error;
|
|
108
|
+
const isQuotaError = (() => {
|
|
109
|
+
if (!(error instanceof Error)) return false;
|
|
110
|
+
if (error.message.includes('Quota exceeded')) return true;
|
|
111
|
+
if ('status' in error && error.status === 429) return true;
|
|
112
|
+
if ('code' in error && error.code === 429) return true;
|
|
113
|
+
return false;
|
|
114
|
+
})();
|
|
115
|
+
if (!isQuotaError || attempt === backoffMs.length) {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
await this.sleepFn(backoffMs[attempt]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
throw lastError;
|
|
122
|
+
};
|
|
123
|
+
|
|
96
124
|
getSpreadsheetId = (spreadsheetUrl: string): string => {
|
|
97
125
|
const url = new URL(spreadsheetUrl);
|
|
98
126
|
return url.pathname.split('/')[3];
|
|
@@ -103,9 +131,11 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
|
103
131
|
): Promise<string[][] | null> => {
|
|
104
132
|
const sheets = this.sheetsClient;
|
|
105
133
|
const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
|
|
106
|
-
const responseSheet = await
|
|
107
|
-
|
|
108
|
-
|
|
134
|
+
const responseSheet = await this.callWithRetry(() =>
|
|
135
|
+
sheets.spreadsheets.get({
|
|
136
|
+
spreadsheetId,
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
109
139
|
if (responseSheet.status !== 200) {
|
|
110
140
|
throw new Error(
|
|
111
141
|
`Failed to get sheet: ${responseSheet.status}. ${JSON.stringify(responseSheet.data)}`,
|
|
@@ -117,10 +147,12 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
|
117
147
|
if (!sheet) {
|
|
118
148
|
return null;
|
|
119
149
|
}
|
|
120
|
-
const response = await
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
150
|
+
const response = await this.callWithRetry(() =>
|
|
151
|
+
sheets.spreadsheets.values.get({
|
|
152
|
+
spreadsheetId,
|
|
153
|
+
range: sheetName,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
124
156
|
if (response.status !== 200) {
|
|
125
157
|
throw new Error(
|
|
126
158
|
`Failed to get sheet: ${response.status}. ${JSON.stringify(response.data)}`,
|
|
@@ -141,14 +173,16 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
|
141
173
|
const sheets = this.sheetsClient;
|
|
142
174
|
const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
|
|
143
175
|
await this.createNewSheetIfNotExists(spreadsheetUrl, sheetName);
|
|
144
|
-
const response = await
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
176
|
+
const response = await this.callWithRetry(() =>
|
|
177
|
+
sheets.spreadsheets.values.update({
|
|
178
|
+
spreadsheetId,
|
|
179
|
+
range: `${sheetName}!${String.fromCharCode(65 + column)}${row + 1}`,
|
|
180
|
+
valueInputOption: 'RAW',
|
|
181
|
+
requestBody: {
|
|
182
|
+
values: [[value]],
|
|
183
|
+
},
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
152
186
|
if (response.status !== 200) {
|
|
153
187
|
throw new Error(
|
|
154
188
|
`Failed to update cell: ${response.status}. ${JSON.stringify(response.data)}`,
|
|
@@ -161,7 +195,9 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
|
161
195
|
): Promise<boolean> => {
|
|
162
196
|
const sheets = this.sheetsClient;
|
|
163
197
|
const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
|
|
164
|
-
const response = await
|
|
198
|
+
const response = await this.callWithRetry(() =>
|
|
199
|
+
sheets.spreadsheets.get({ spreadsheetId }),
|
|
200
|
+
);
|
|
165
201
|
if (response.status !== 200) {
|
|
166
202
|
throw new Error(
|
|
167
203
|
`Failed to get sheet: ${response.status}. ${JSON.stringify(response.data)}`,
|
|
@@ -180,20 +216,22 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
|
180
216
|
if (await this.sheetExists(spreadsheetUrl, sheetName)) {
|
|
181
217
|
return;
|
|
182
218
|
}
|
|
183
|
-
const response = await
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
219
|
+
const response = await this.callWithRetry(() =>
|
|
220
|
+
sheets.spreadsheets.batchUpdate({
|
|
221
|
+
spreadsheetId,
|
|
222
|
+
requestBody: {
|
|
223
|
+
requests: [
|
|
224
|
+
{
|
|
225
|
+
addSheet: {
|
|
226
|
+
properties: {
|
|
227
|
+
title: sheetName,
|
|
228
|
+
},
|
|
191
229
|
},
|
|
192
230
|
},
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
},
|
|
196
|
-
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
197
235
|
if (response.status !== 200) {
|
|
198
236
|
throw new Error(
|
|
199
237
|
`Failed to create sheet: ${response.status}. ${JSON.stringify(response.data)}`,
|
|
@@ -211,14 +249,16 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
|
211
249
|
await this.createNewSheetIfNotExists(spreadsheetUrl, sheetName);
|
|
212
250
|
const sheet = await this.getSheet(spreadsheetUrl, sheetName);
|
|
213
251
|
const range = `${sheetName}!A${sheet ? sheet.length + 1 : 1}:A`;
|
|
214
|
-
const response = await
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
252
|
+
const response = await this.callWithRetry(() =>
|
|
253
|
+
sheets.spreadsheets.values.append({
|
|
254
|
+
spreadsheetId,
|
|
255
|
+
range: range,
|
|
256
|
+
valueInputOption: 'RAW',
|
|
257
|
+
requestBody: {
|
|
258
|
+
values,
|
|
259
|
+
},
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
222
262
|
if (response.status !== 200) {
|
|
223
263
|
throw new Error(
|
|
224
264
|
`Failed to append values: ${response.status}. ${JSON.stringify(response.data)}`,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { extractFencedJsonBlocks } from './extractFencedJsonBlocks';
|
|
2
|
+
|
|
3
|
+
describe('extractFencedJsonBlocks', () => {
|
|
4
|
+
it('returns every fenced json block in the order they appear', () => {
|
|
5
|
+
expect(
|
|
6
|
+
extractFencedJsonBlocks(
|
|
7
|
+
'From: :robot: agent (model)\n\n```json\n{ "pullRequestRequired": false }\n```\n\ntext\n\n```json\n{ "nextStepAgent": "developer" }\n```\n',
|
|
8
|
+
'test',
|
|
9
|
+
),
|
|
10
|
+
).toEqual([{ pullRequestRequired: false }, { nextStepAgent: 'developer' }]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('returns an empty list when the body carries no fenced json block', () => {
|
|
14
|
+
expect(extractFencedJsonBlocks('Please go ahead.', 'test')).toEqual([]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('skips an unparseable block, reports it, and still returns the parseable ones', () => {
|
|
18
|
+
const consoleWarn = jest.spyOn(console, 'warn').mockImplementation();
|
|
19
|
+
expect(
|
|
20
|
+
extractFencedJsonBlocks(
|
|
21
|
+
'From: :robot: agent (model)\n\n```json\n{ "broken": \n```\n\n```json\n{ "ok": true }\n```\n',
|
|
22
|
+
'test',
|
|
23
|
+
),
|
|
24
|
+
).toEqual([{ ok: true }]);
|
|
25
|
+
expect(consoleWarn).toHaveBeenCalledWith(
|
|
26
|
+
'Invalid JSON in report body while checking test:',
|
|
27
|
+
expect.anything(),
|
|
28
|
+
);
|
|
29
|
+
consoleWarn.mockRestore();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('reads blocks whose code fence backticks are backslash escaped', () => {
|
|
33
|
+
expect(
|
|
34
|
+
extractFencedJsonBlocks(
|
|
35
|
+
'From: :robot: agent (model)\n\n\\`\\`\\`json\n{ "nextStepAgent": "developer" }\n\\`\\`\\`\n',
|
|
36
|
+
'test',
|
|
37
|
+
),
|
|
38
|
+
).toEqual([{ nextStepAgent: 'developer' }]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { normalizeReportBody } from './normalizeReportBody';
|
|
2
|
+
|
|
3
|
+
export const extractFencedJsonBlocks = (
|
|
4
|
+
body: string,
|
|
5
|
+
context: string,
|
|
6
|
+
): unknown[] => {
|
|
7
|
+
const blocks: unknown[] = [];
|
|
8
|
+
for (const match of normalizeReportBody(body).matchAll(
|
|
9
|
+
/```json\n([\s\S]*?)\n```/g,
|
|
10
|
+
)) {
|
|
11
|
+
try {
|
|
12
|
+
blocks.push(JSON.parse(match[1]));
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.warn(
|
|
15
|
+
`Invalid JSON in report body while checking ${context}:`,
|
|
16
|
+
error,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return blocks;
|
|
21
|
+
};
|
|
@@ -36,4 +36,23 @@ describe('extractNextStepAgent', () => {
|
|
|
36
36
|
it('returns null when the body carries no fenced json block', () => {
|
|
37
37
|
expect(extractNextStepAgent('Please go ahead with that.')).toBeNull();
|
|
38
38
|
});
|
|
39
|
+
|
|
40
|
+
it('returns the declared agent when the routing block is not the first fenced json block', () => {
|
|
41
|
+
expect(
|
|
42
|
+
extractNextStepAgent(
|
|
43
|
+
'From: :robot: agent (model)\n\n```json\n{ "pullRequestRequired": false }\n```\n\n## review result\n\n```json\n{ "pullRequestRequired": false, "nextStepAgent": "systems-analyst" }\n```\n',
|
|
44
|
+
),
|
|
45
|
+
).toBe('systems-analyst');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns the declared agent from a later block and reports the earlier unparseable block', () => {
|
|
49
|
+
const consoleWarn = jest.spyOn(console, 'warn').mockImplementation();
|
|
50
|
+
expect(
|
|
51
|
+
extractNextStepAgent(
|
|
52
|
+
'From: :robot: agent (model)\n\n```json\n{ "pullRequestRequired": false,\n```\n\n```json\n{ "nextStepAgent": "developer" }\n```\n',
|
|
53
|
+
),
|
|
54
|
+
).toBe('developer');
|
|
55
|
+
expect(consoleWarn).toHaveBeenCalled();
|
|
56
|
+
consoleWarn.mockRestore();
|
|
57
|
+
});
|
|
39
58
|
});
|
|
@@ -1,30 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { extractFencedJsonBlocks } from './extractFencedJsonBlocks';
|
|
2
|
+
|
|
2
3
|
export const extractNextStepAgent = (body: string): string | null => {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
for (const block of extractFencedJsonBlocks(body, 'nextStepAgent')) {
|
|
5
|
+
if (typeof block !== 'object' || block === null) {
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
8
|
+
if (!('nextStepAgent' in block)) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const value = Reflect.get(block, 'nextStepAgent');
|
|
12
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
return value.trim();
|
|
8
16
|
}
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
reportJson = JSON.parse(reportMatch[1]);
|
|
12
|
-
} catch (error) {
|
|
13
|
-
console.warn(
|
|
14
|
-
'Invalid JSON in report body while checking nextStepAgent:',
|
|
15
|
-
error,
|
|
16
|
-
);
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
if (typeof reportJson !== 'object' || reportJson === null) {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
if (!('nextStepAgent' in reportJson)) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
const value = Reflect.get(reportJson, 'nextStepAgent');
|
|
26
|
-
if (typeof value !== 'string' || value.trim() === '') {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
return value.trim();
|
|
17
|
+
return null;
|
|
30
18
|
};
|
|
@@ -126,4 +126,37 @@ describe('isPullRequestDeclaredUnnecessary', () => {
|
|
|
126
126
|
),
|
|
127
127
|
).toBe(false);
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
it('returns true when pullRequestRequired is declared in a later fenced json block', () => {
|
|
131
|
+
expect(
|
|
132
|
+
isPullRequestDeclaredUnnecessary(
|
|
133
|
+
[
|
|
134
|
+
{
|
|
135
|
+
author: 'agent-bot',
|
|
136
|
+
content:
|
|
137
|
+
'From: :robot: agent report\n\n```json\n{ "nextStepAgent": "pr-reviewer" }\n```\n\n## summary\n\n```json\n{ "pullRequestRequired": false }\n```',
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
trustEveryAuthor,
|
|
141
|
+
),
|
|
142
|
+
).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('reads a later block and reports the earlier unparseable block', () => {
|
|
146
|
+
const consoleWarn = jest.spyOn(console, 'warn').mockImplementation();
|
|
147
|
+
expect(
|
|
148
|
+
isPullRequestDeclaredUnnecessary(
|
|
149
|
+
[
|
|
150
|
+
{
|
|
151
|
+
author: 'agent-bot',
|
|
152
|
+
content:
|
|
153
|
+
'From: :robot: agent report\n\n```json\n{ "pullRequestRequired": false,\n```\n\n```json\n{ "pullRequestRequired": false }\n```',
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
trustEveryAuthor,
|
|
157
|
+
),
|
|
158
|
+
).toBe(true);
|
|
159
|
+
expect(consoleWarn).toHaveBeenCalled();
|
|
160
|
+
consoleWarn.mockRestore();
|
|
161
|
+
});
|
|
129
162
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { extractFencedJsonBlocks } from './extractFencedJsonBlocks';
|
|
1
2
|
import { isAgentReportBody } from './isAgentReportBody';
|
|
2
|
-
import { normalizeReportBody } from './normalizeReportBody';
|
|
3
3
|
|
|
4
4
|
export const isPullRequestDeclaredUnnecessary = (
|
|
5
5
|
comments: { author: string; content: string }[],
|
|
@@ -13,25 +13,17 @@ export const isPullRequestDeclaredUnnecessary = (
|
|
|
13
13
|
) {
|
|
14
14
|
return false;
|
|
15
15
|
}
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
'Invalid JSON in report body while checking pullRequestRequired:',
|
|
28
|
-
error,
|
|
29
|
-
);
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
if (typeof reportJson !== 'object' || reportJson === null) {
|
|
33
|
-
return false;
|
|
16
|
+
for (const block of extractFencedJsonBlocks(
|
|
17
|
+
lastComment.content,
|
|
18
|
+
'pullRequestRequired',
|
|
19
|
+
)) {
|
|
20
|
+
if (typeof block !== 'object' || block === null) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const report: Record<string, unknown> = { ...block };
|
|
24
|
+
if (report.pullRequestRequired === false) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
34
27
|
}
|
|
35
|
-
|
|
36
|
-
return report.pullRequestRequired === false;
|
|
28
|
+
return false;
|
|
37
29
|
};
|
|
@@ -68,7 +68,9 @@ export declare class GoogleSpreadsheetRepository implements SpreadsheetRepositor
|
|
|
68
68
|
readonly localStorageRepository: LocalStorageRepository;
|
|
69
69
|
keyFile: string;
|
|
70
70
|
private readonly sheetsClient;
|
|
71
|
-
|
|
71
|
+
private readonly sleepFn;
|
|
72
|
+
constructor(localStorageRepository: LocalStorageRepository, serviceAccountKey?: string, sheetsClientFactory?: () => SheetsApiClient, sleepFn?: (ms: number) => Promise<void>);
|
|
73
|
+
private callWithRetry;
|
|
72
74
|
getSpreadsheetId: (spreadsheetUrl: string) => string;
|
|
73
75
|
getSheet: (spreadsheetUrl: string, sheetName: string) => Promise<string[][] | null>;
|
|
74
76
|
updateCell: (spreadsheetUrl: string, sheetName: string, row: number, column: number, value: string) => Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GoogleSpreadsheetRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/GoogleSpreadsheetRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,gEAAgE,CAAC;AAEvG,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAIlE,UAAU,eAAe;IACvB,YAAY,EAAE;QACZ,GAAG,CAAC,MAAM,EAAE;YAAE,aAAa,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC;YAC9C,MAAM,EAAE,MAAM,CAAC;YACf,IAAI,EAAE;gBACJ,MAAM,CAAC,EAAE,KAAK,CAAC;oBACb,UAAU,CAAC,EAAE;wBAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;qBAAE,GAAG,IAAI,CAAC;iBAC/C,CAAC,GAAG,IAAI,CAAC;aACX,CAAC;SACH,CAAC,CAAC;QACH,MAAM,EAAE;YACN,GAAG,CAAC,MAAM,EAAE;gBAAE,aAAa,EAAE,MAAM,CAAC;gBAAC,KAAK,EAAE,MAAM,CAAA;aAAE,GAAG,OAAO,CAAC;gBAC7D,MAAM,EAAE,MAAM,CAAC;gBACf,IAAI,EAAE;oBAAE,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,GAAG,IAAI,CAAA;iBAAE,CAAC;aACvC,CAAC,CAAC;YACH,MAAM,CAAC,MAAM,EAAE;gBACb,aAAa,EAAE,MAAM,CAAC;gBACtB,KAAK,EAAE,MAAM,CAAC;gBACd,gBAAgB,EAAE,MAAM,CAAC;gBACzB,WAAW,EAAE;oBAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAAA;iBAAE,CAAC;aACrC,GAAG,OAAO,CAAC;gBAAE,MAAM,EAAE,MAAM,CAAC;gBAAC,IAAI,EAAE,OAAO,CAAA;aAAE,CAAC,CAAC;YAC/C,MAAM,CAAC,MAAM,EAAE;gBACb,aAAa,EAAE,MAAM,CAAC;gBACtB,KAAK,EAAE,MAAM,CAAC;gBACd,gBAAgB,EAAE,MAAM,CAAC;gBACzB,WAAW,EAAE;oBAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAAA;iBAAE,CAAC;aACrC,GAAG,OAAO,CAAC;gBAAE,MAAM,EAAE,MAAM,CAAC;gBAAC,IAAI,EAAE,OAAO,CAAA;aAAE,CAAC,CAAC;SAChD,CAAC;QACF,WAAW,CAAC,MAAM,EAAE;YAClB,aAAa,EAAE,MAAM,CAAC;YACtB,WAAW,EAAE;gBACX,QAAQ,EAAE,KAAK,CAAC;oBAAE,QAAQ,CAAC,EAAE;wBAAE,UAAU,CAAC,EAAE;4BAAE,KAAK,CAAC,EAAE,MAAM,CAAA;yBAAE,CAAA;qBAAE,CAAA;iBAAE,CAAC,CAAC;aACrE,CAAC;SACH,GAAG,OAAO,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,OAAO,CAAA;SAAE,CAAC,CAAC;KAChD,CAAC;CACH;AAED,qBAAa,2BAA4B,YAAW,qBAAqB;
|
|
1
|
+
{"version":3,"file":"GoogleSpreadsheetRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/GoogleSpreadsheetRepository.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extractFencedJsonBlocks.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/extractFencedJsonBlocks.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,uBAAuB,GAClC,MAAM,MAAM,EACZ,SAAS,MAAM,KACd,OAAO,EAeT,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extractNextStepAgent.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/extractNextStepAgent.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"extractNextStepAgent.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/extractNextStepAgent.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,MAAM,GAAG,IAe5D,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"isPullRequestDeclaredUnnecessary.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/isPullRequestDeclaredUnnecessary.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,gCAAgC,GAC3C,UAAU;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,EAC/C,iBAAiB,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,KAC3C,
|
|
1
|
+
{"version":3,"file":"isPullRequestDeclaredUnnecessary.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/isPullRequestDeclaredUnnecessary.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,gCAAgC,GAC3C,UAAU;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,EAC/C,iBAAiB,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,KAC3C,OAsBF,CAAC"}
|