github-issue-tower-defence-management 1.146.10 → 1.146.12
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 +14 -0
- package/bin/adapter/entry-points/console/consoleOperationApi.js +38 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-CPY6wV8C.js → index-DcDM50gs.js} +2 -2
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +86 -0
- package/src/adapter/entry-points/console/consoleOperationApi.ts +51 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +12 -12
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +3 -2
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +2 -1
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/tabAdvance.test.ts +9 -2
- package/src/adapter/entry-points/console/ui/src/features/console/logic/types.ts +1 -1
- package/src/adapter/entry-points/console/ui-dist/assets/{index-CPY6wV8C.js → index-DcDM50gs.js} +2 -2
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +1 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
|
@@ -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-DcDM50gs.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-CKX2ny-z.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/package.json
CHANGED
|
@@ -229,6 +229,92 @@ describe('consoleOperationApi', () => {
|
|
|
229
229
|
expectRecordedAcrossTabs('PVTI_c');
|
|
230
230
|
});
|
|
231
231
|
|
|
232
|
+
it('marks a pull request unnecessary by closing it, labelling the item chore and moving it to Awaiting workspace', async () => {
|
|
233
|
+
issueRepository.getIssueByUrl.mockResolvedValue({
|
|
234
|
+
...issue,
|
|
235
|
+
url: 'https://github.com/o/r/issues/7',
|
|
236
|
+
org: 'o',
|
|
237
|
+
repo: 'r',
|
|
238
|
+
number: 7,
|
|
239
|
+
labels: ['bug'],
|
|
240
|
+
});
|
|
241
|
+
const response = await handleReview(context, {
|
|
242
|
+
pjcode: 'acme',
|
|
243
|
+
action: 'unnecessary',
|
|
244
|
+
prUrl: 'https://github.com/o/r/pull/1',
|
|
245
|
+
issueUrl: 'https://github.com/o/r/issues/7',
|
|
246
|
+
projectItemId: 'PVTI_unnecessary',
|
|
247
|
+
commentBody: 'This pull request is unnecessary.',
|
|
248
|
+
});
|
|
249
|
+
expect(response.statusCode).toBe(200);
|
|
250
|
+
expect(issueRepository.closePullRequest).toHaveBeenCalledWith(
|
|
251
|
+
'https://github.com/o/r/pull/1',
|
|
252
|
+
);
|
|
253
|
+
expect(issueRepository.createCommentByUrl).toHaveBeenCalledWith(
|
|
254
|
+
'https://github.com/o/r/pull/1',
|
|
255
|
+
'This pull request is unnecessary.',
|
|
256
|
+
);
|
|
257
|
+
expect(issueRepository.updateLabels).toHaveBeenCalledWith(
|
|
258
|
+
expect.objectContaining({ url: 'https://github.com/o/r/issues/7' }),
|
|
259
|
+
['bug', 'chore'],
|
|
260
|
+
);
|
|
261
|
+
expect(issueRepository.updateStatus).toHaveBeenCalledWith(
|
|
262
|
+
project,
|
|
263
|
+
expect.objectContaining({ itemId: 'PVTI_unnecessary' }),
|
|
264
|
+
'status_aw',
|
|
265
|
+
);
|
|
266
|
+
expectRecordedAcrossTabs('PVTI_unnecessary');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('keeps the chore label once when the unnecessary item already carries it', async () => {
|
|
270
|
+
issueRepository.getIssueByUrl.mockResolvedValue({
|
|
271
|
+
...issue,
|
|
272
|
+
url: 'https://github.com/o/r/issues/8',
|
|
273
|
+
org: 'o',
|
|
274
|
+
repo: 'r',
|
|
275
|
+
number: 8,
|
|
276
|
+
labels: ['chore'],
|
|
277
|
+
});
|
|
278
|
+
const response = await handleReview(context, {
|
|
279
|
+
pjcode: 'acme',
|
|
280
|
+
action: 'unnecessary',
|
|
281
|
+
prUrl: 'https://github.com/o/r/pull/2',
|
|
282
|
+
issueUrl: 'https://github.com/o/r/issues/8',
|
|
283
|
+
projectItemId: 'PVTI_unnecessary_dup',
|
|
284
|
+
});
|
|
285
|
+
expect(response.statusCode).toBe(200);
|
|
286
|
+
expect(issueRepository.updateLabels).not.toHaveBeenCalled();
|
|
287
|
+
expect(issueRepository.updateStatus).toHaveBeenCalledWith(
|
|
288
|
+
project,
|
|
289
|
+
expect.objectContaining({ itemId: 'PVTI_unnecessary_dup' }),
|
|
290
|
+
'status_aw',
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('rejects an unnecessary action without an issueUrl', async () => {
|
|
295
|
+
const response = await handleReview(context, {
|
|
296
|
+
pjcode: 'acme',
|
|
297
|
+
action: 'unnecessary',
|
|
298
|
+
prUrl: 'https://github.com/o/r/pull/1',
|
|
299
|
+
projectItemId: 'PVTI_unnecessary_missing',
|
|
300
|
+
});
|
|
301
|
+
expect(response.statusCode).toBe(400);
|
|
302
|
+
expect(issueRepository.closePullRequest).not.toHaveBeenCalled();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('leaves labels and status untouched for the plain close action', async () => {
|
|
306
|
+
const response = await handleReview(context, {
|
|
307
|
+
pjcode: 'acme',
|
|
308
|
+
action: 'close',
|
|
309
|
+
prUrl: 'https://github.com/o/r/pull/1',
|
|
310
|
+
projectItemId: 'PVTI_plain_close',
|
|
311
|
+
commentBody: 'closing',
|
|
312
|
+
});
|
|
313
|
+
expect(response.statusCode).toBe(200);
|
|
314
|
+
expect(issueRepository.updateLabels).not.toHaveBeenCalled();
|
|
315
|
+
expect(issueRepository.updateStatus).not.toHaveBeenCalled();
|
|
316
|
+
});
|
|
317
|
+
|
|
232
318
|
it('rejects an unknown review action', async () => {
|
|
233
319
|
const response = await handleReview(context, {
|
|
234
320
|
pjcode: 'acme',
|
|
@@ -10,6 +10,7 @@ import { findConsoleItemUrl } from './consoleItemUrlLookup';
|
|
|
10
10
|
|
|
11
11
|
export const AWAITING_WORKSPACE_STATUS_NAME = 'awaiting workspace';
|
|
12
12
|
export const IN_TMUX_BY_HUMAN_STATUS_NAME = 'in tmux by human';
|
|
13
|
+
export const CHORE_LABEL_NAME = 'chore';
|
|
13
14
|
|
|
14
15
|
export type ConsoleProjectBinding = {
|
|
15
16
|
pjcode: string;
|
|
@@ -192,6 +193,24 @@ const updateStatusByName = async (
|
|
|
192
193
|
return null;
|
|
193
194
|
};
|
|
194
195
|
|
|
196
|
+
const addChoreLabel = async (
|
|
197
|
+
issueRepository: IssueRepository,
|
|
198
|
+
issueUrl: string,
|
|
199
|
+
): Promise<ConsoleOperationResponse | null> => {
|
|
200
|
+
const issue = await issueRepository.getIssueByUrl(issueUrl);
|
|
201
|
+
if (issue === null) {
|
|
202
|
+
return badGateway(`issue "${issueUrl}" could not be loaded`);
|
|
203
|
+
}
|
|
204
|
+
if (issue.labels.includes(CHORE_LABEL_NAME)) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
await issueRepository.updateLabels(issue, [
|
|
208
|
+
...issue.labels,
|
|
209
|
+
CHORE_LABEL_NAME,
|
|
210
|
+
]);
|
|
211
|
+
return null;
|
|
212
|
+
};
|
|
213
|
+
|
|
195
214
|
export const handleReview = async (
|
|
196
215
|
context: ConsoleOperationContext,
|
|
197
216
|
body: Record<string, unknown>,
|
|
@@ -230,6 +249,38 @@ export const handleReview = async (
|
|
|
230
249
|
}
|
|
231
250
|
const { project, pjcode } = binding;
|
|
232
251
|
|
|
252
|
+
if (action === 'unnecessary') {
|
|
253
|
+
const issueUrl = body.issueUrl;
|
|
254
|
+
if (!isNonEmptyString(issueUrl)) {
|
|
255
|
+
return badRequest('issueUrl is required for unnecessary');
|
|
256
|
+
}
|
|
257
|
+
await context.resolveIssueRepository(prUrl).closePullRequest(prUrl);
|
|
258
|
+
if (isNonEmptyString(body.commentBody)) {
|
|
259
|
+
await context
|
|
260
|
+
.resolveIssueRepository(prUrl)
|
|
261
|
+
.createCommentByUrl(prUrl, body.commentBody);
|
|
262
|
+
}
|
|
263
|
+
const labelFailure = await addChoreLabel(
|
|
264
|
+
context.resolveIssueRepository(issueUrl),
|
|
265
|
+
issueUrl,
|
|
266
|
+
);
|
|
267
|
+
if (labelFailure !== null) {
|
|
268
|
+
return labelFailure;
|
|
269
|
+
}
|
|
270
|
+
const failure = await updateStatusByName(
|
|
271
|
+
context.resolveIssueRepository(issueUrl),
|
|
272
|
+
project,
|
|
273
|
+
issueUrl,
|
|
274
|
+
projectItemId,
|
|
275
|
+
AWAITING_WORKSPACE_STATUS_NAME,
|
|
276
|
+
);
|
|
277
|
+
if (failure !== null) {
|
|
278
|
+
return failure;
|
|
279
|
+
}
|
|
280
|
+
recordDone(context, pjcode, projectItemId);
|
|
281
|
+
return ok();
|
|
282
|
+
}
|
|
283
|
+
|
|
233
284
|
if (action === 'approve') {
|
|
234
285
|
await context.resolveIssueRepository(prUrl).approvePullRequest(prUrl);
|
|
235
286
|
const failure = await updateStatusByName(
|
|
@@ -82,39 +82,39 @@ test('processing tabs drives auto-advance and keeps emptied badges at zero', asy
|
|
|
82
82
|
await expect(approveButton).toBeVisible();
|
|
83
83
|
await approveButton.click();
|
|
84
84
|
|
|
85
|
-
await expect(activeTabLabel(page)).toHaveText('
|
|
85
|
+
await expect(activeTabLabel(page)).toHaveText('Unread', { timeout: 8000 });
|
|
86
86
|
await expect(tabByLabel(page, 'Awaiting Quality Check')).toHaveCount(0, {
|
|
87
87
|
timeout: 8000,
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
await itemRowByText(
|
|
91
91
|
page,
|
|
92
|
-
'
|
|
92
|
+
'Scaffold React console UI under entry-points with build bundling',
|
|
93
93
|
).click();
|
|
94
94
|
await processSelectedItemViaStatus(page);
|
|
95
|
-
await expect(tabBadge(page, '
|
|
96
|
-
await tabByLabel(page, '
|
|
95
|
+
await expect(tabBadge(page, 'Unread')).toHaveText('1', { timeout: 8000 });
|
|
96
|
+
await tabByLabel(page, 'Unread').click();
|
|
97
97
|
|
|
98
98
|
await itemRowByText(
|
|
99
99
|
page,
|
|
100
|
-
'
|
|
100
|
+
'Add server-side console API handlers for read and operation endpoints',
|
|
101
101
|
).click();
|
|
102
102
|
await processSelectedItemViaStatus(page);
|
|
103
103
|
|
|
104
|
-
await expect(activeTabLabel(page)).toHaveText('
|
|
105
|
-
await expect(tabByLabel(page, '
|
|
104
|
+
await expect(activeTabLabel(page)).toHaveText('Triage', { timeout: 8000 });
|
|
105
|
+
await expect(tabByLabel(page, 'Unread')).toHaveCount(0, {
|
|
106
106
|
timeout: 8000,
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
await tabByLabel(page, 'Todo by human').click();
|
|
110
110
|
await expect(activeTabLabel(page)).toHaveText('Todo by human');
|
|
111
|
-
await expect(tabByLabel(page, '
|
|
111
|
+
await expect(tabByLabel(page, 'Unread')).toHaveCount(0);
|
|
112
112
|
await expect(tabBadge(page, 'Todo by human')).toHaveText('1');
|
|
113
113
|
|
|
114
|
-
await tabByLabel(page, '
|
|
115
|
-
await expect(activeTabLabel(page)).toHaveText('
|
|
116
|
-
await expect(tabByLabel(page, '
|
|
117
|
-
await expect(tabBadge(page, '
|
|
114
|
+
await tabByLabel(page, 'Triage').click();
|
|
115
|
+
await expect(activeTabLabel(page)).toHaveText('Triage');
|
|
116
|
+
await expect(tabByLabel(page, 'Unread')).toHaveCount(0);
|
|
117
|
+
await expect(tabBadge(page, 'Triage')).toHaveText('2');
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
test('renders the Workflow Blocker tab leftmost and shows its detail operations', async ({
|
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts
CHANGED
|
@@ -72,7 +72,7 @@ describe('useConsoleOperations', () => {
|
|
|
72
72
|
});
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
it('posts an unnecessary review
|
|
75
|
+
it('posts an unnecessary review carrying the item url so the server can label it chore', async () => {
|
|
76
76
|
const fetchMock = captureFetch();
|
|
77
77
|
const { result } = setup();
|
|
78
78
|
await act(async () => {
|
|
@@ -83,7 +83,8 @@ describe('useConsoleOperations', () => {
|
|
|
83
83
|
);
|
|
84
84
|
});
|
|
85
85
|
expect(lastBody(fetchMock)).toMatchObject({
|
|
86
|
-
action: '
|
|
86
|
+
action: 'unnecessary',
|
|
87
|
+
issueUrl: prItem.url,
|
|
87
88
|
commentBody: 'This pull request is unnecessary.',
|
|
88
89
|
});
|
|
89
90
|
});
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
findNextNonEmptyTabToRight,
|
|
3
3
|
resolveDefaultActiveTab,
|
|
4
4
|
} from './tabAdvance';
|
|
5
|
-
import type
|
|
5
|
+
import { CONSOLE_TABS, type ConsoleTabName } from './types';
|
|
6
6
|
|
|
7
7
|
const counts = (
|
|
8
8
|
overrides: Partial<Record<ConsoleTabName, number>>,
|
|
@@ -17,6 +17,13 @@ const counts = (
|
|
|
17
17
|
...overrides,
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
describe('CONSOLE_TABS ordering', () => {
|
|
21
|
+
it('places Unread before Triage so unseen items are worked first', () => {
|
|
22
|
+
const names = CONSOLE_TABS.map((tab) => tab.name);
|
|
23
|
+
expect(names.indexOf('unread')).toBeLessThan(names.indexOf('triage'));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
20
27
|
describe('findNextNonEmptyTabToRight', () => {
|
|
21
28
|
it('returns the first non-empty tab to the right of the active tab', () => {
|
|
22
29
|
expect(findNextNonEmptyTabToRight('prs', counts({ unread: 7 }))).toBe(
|
|
@@ -45,7 +52,7 @@ describe('findNextNonEmptyTabToRight', () => {
|
|
|
45
52
|
it('returns the immediately adjacent tab when it is non-empty', () => {
|
|
46
53
|
expect(
|
|
47
54
|
findNextNonEmptyTabToRight('prs', counts({ triage: 12, unread: 7 })),
|
|
48
|
-
).toBe('
|
|
55
|
+
).toBe('unread');
|
|
49
56
|
});
|
|
50
57
|
|
|
51
58
|
it('returns null when no tab to the right has any items', () => {
|
|
@@ -157,8 +157,8 @@ export type ConsoleTab = {
|
|
|
157
157
|
export const CONSOLE_TABS: ConsoleTab[] = [
|
|
158
158
|
{ name: 'workflow-blocker', label: 'Workflow Blocker' },
|
|
159
159
|
{ name: 'prs', label: 'Awaiting Quality Check' },
|
|
160
|
-
{ name: 'triage', label: 'Triage' },
|
|
161
160
|
{ name: 'unread', label: 'Unread' },
|
|
161
|
+
{ name: 'triage', label: 'Triage' },
|
|
162
162
|
{ name: 'failed-preparation', label: 'Failed Preparation' },
|
|
163
163
|
{ name: 'todo-by-human', label: 'Todo by human' },
|
|
164
164
|
{ name: 'todo-by-agent', label: 'Todo by agent' },
|