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.
@@ -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-CPY6wV8C.js"></script>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.146.10",
3
+ "version": "1.146.12",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -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('Triage', { timeout: 8000 });
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
- 'Add Sonnet to Opus weekly-limit fallback routing per token',
92
+ 'Scaffold React console UI under entry-points with build bundling',
93
93
  ).click();
94
94
  await processSelectedItemViaStatus(page);
95
- await expect(tabBadge(page, 'Triage')).toHaveText('1', { timeout: 8000 });
96
- await tabByLabel(page, 'Triage').click();
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
- 'Publish the generated documentation site to GitHub Pages',
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('Unread', { timeout: 8000 });
105
- await expect(tabByLabel(page, 'Triage')).toHaveCount(0, {
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, 'Triage')).toHaveCount(0);
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, 'Unread').click();
115
- await expect(activeTabLabel(page)).toHaveText('Unread');
116
- await expect(tabByLabel(page, 'Triage')).toHaveCount(0);
117
- await expect(tabBadge(page, 'Unread')).toHaveText('2');
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 ({
@@ -72,7 +72,7 @@ describe('useConsoleOperations', () => {
72
72
  });
73
73
  });
74
74
 
75
- it('posts an unnecessary review as a close with the unnecessary comment body', async () => {
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: 'close',
86
+ action: 'unnecessary',
87
+ issueUrl: prItem.url,
87
88
  commentBody: 'This pull request is unnecessary.',
88
89
  });
89
90
  });
@@ -114,9 +114,10 @@ const reviewRequest = (
114
114
  }
115
115
  return {
116
116
  pjcode,
117
- action: 'close',
117
+ action: 'unnecessary',
118
118
  prUrl,
119
119
  projectItemId: item.projectItemId,
120
+ issueUrl: item.url,
120
121
  commentBody: UNNECESSARY_COMMENT_BODY,
121
122
  };
122
123
  };
@@ -23,6 +23,7 @@ export type ConsoleReviewRequest = {
23
23
  action: string;
24
24
  prUrl: string;
25
25
  projectItemId: string;
26
+ issueUrl?: string;
26
27
  commentBody?: string;
27
28
  changedFilePath?: string;
28
29
  line?: number;
@@ -2,7 +2,7 @@ import {
2
2
  findNextNonEmptyTabToRight,
3
3
  resolveDefaultActiveTab,
4
4
  } from './tabAdvance';
5
- import type { ConsoleTabName } from './types';
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('triage');
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' },