github-issue-tower-defence-management 1.151.1 → 1.151.2

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 (23) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/adapter/entry-points/console/consoleDoneStore.js +19 -1
  3. package/bin/adapter/entry-points/console/consoleDoneStore.js.map +1 -1
  4. package/bin/adapter/entry-points/console/consoleOperationApi.js +17 -5
  5. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  6. package/bin/adapter/entry-points/console/ui-dist/assets/index-CY96HROQ.js +83 -0
  7. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  8. package/package.json +1 -1
  9. package/src/adapter/entry-points/console/consoleDoneStore.test.ts +21 -0
  10. package/src/adapter/entry-points/console/consoleDoneStore.ts +25 -0
  11. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +58 -7
  12. package/src/adapter/entry-points/console/consoleOperationApi.ts +43 -6
  13. package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.test.ts +79 -1
  14. package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.ts +20 -0
  15. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +42 -0
  16. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +6 -2
  17. package/src/adapter/entry-points/console/ui-dist/assets/index-CY96HROQ.js +83 -0
  18. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  19. package/types/adapter/entry-points/console/consoleDoneStore.d.ts +3 -0
  20. package/types/adapter/entry-points/console/consoleDoneStore.d.ts.map +1 -1
  21. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  22. package/bin/adapter/entry-points/console/ui-dist/assets/index-UQt5L756.js +0 -83
  23. package/src/adapter/entry-points/console/ui-dist/assets/index-UQt5L756.js +0 -83
@@ -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-UQt5L756.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-CY96HROQ.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-Dihcph6j.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.151.1",
3
+ "version": "1.151.2",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -3,6 +3,8 @@ import * as os from 'os';
3
3
  import * as path from 'path';
4
4
  import {
5
5
  CONSOLE_DONE_FILE_NAME,
6
+ CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
7
+ CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
6
8
  CONSOLE_DONE_TAB_NAMES,
7
9
  doneFilePathForTab,
8
10
  readDoneProjectItemIds,
@@ -145,4 +147,23 @@ describe('consoleDoneStore', () => {
145
147
  }
146
148
  });
147
149
  });
150
+
151
+ describe('selector partitions of the console done tabs', () => {
152
+ it('covers every console done tab exactly once across the status and story partitions', () => {
153
+ expect(
154
+ [
155
+ ...CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
156
+ ...CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
157
+ ].sort(),
158
+ ).toEqual([...CONSOLE_DONE_TAB_NAMES].sort());
159
+ });
160
+
161
+ it('places no console done tab in both the status and the story partition', () => {
162
+ expect(
163
+ CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES.filter((tab) =>
164
+ CONSOLE_DONE_STORY_SELECTED_TAB_NAMES.includes(tab),
165
+ ),
166
+ ).toEqual([]);
167
+ });
168
+ });
148
169
  });
@@ -89,6 +89,31 @@ export const CONSOLE_DONE_TAB_NAMES: string[] = [
89
89
  'in-tmux-by-human',
90
90
  ];
91
91
 
92
+ export const CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES: string[] = [
93
+ 'prs',
94
+ 'unread',
95
+ 'failed-preparation',
96
+ 'todo-by-human',
97
+ 'todo-by-agent',
98
+ 'in-tmux-by-human',
99
+ ];
100
+
101
+ export const CONSOLE_DONE_STORY_SELECTED_TAB_NAMES: string[] = [
102
+ 'workflow-blocker',
103
+ 'triage',
104
+ ];
105
+
106
+ export const recordDoneProjectItemIdForTabs = (
107
+ consoleDataOutputDir: string,
108
+ pjcode: string,
109
+ projectItemId: string,
110
+ tabNames: string[],
111
+ ): void => {
112
+ for (const tab of tabNames) {
113
+ recordDoneProjectItemId(consoleDataOutputDir, pjcode, tab, projectItemId);
114
+ }
115
+ };
116
+
92
117
  export const recordDoneProjectItemIdAcrossTabs = (
93
118
  consoleDataOutputDir: string,
94
119
  pjcode: string,
@@ -18,6 +18,8 @@ import {
18
18
  import {
19
19
  CONSOLE_DONE_TAB_NAMES,
20
20
  readDoneProjectItemIds,
21
+ CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
22
+ CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
21
23
  } from './consoleDoneStore';
22
24
 
23
25
  describe('consoleOperationApi', () => {
@@ -104,6 +106,23 @@ describe('consoleOperationApi', () => {
104
106
  }
105
107
  };
106
108
 
109
+ const expectRecordedOnlyIn = (
110
+ projectItemId: string,
111
+ recordedTabs: string[],
112
+ ): void => {
113
+ for (const tab of CONSOLE_DONE_TAB_NAMES) {
114
+ if (recordedTabs.includes(tab)) {
115
+ expect(readDoneProjectItemIds(baseDir, 'acme', tab)).toContain(
116
+ projectItemId,
117
+ );
118
+ continue;
119
+ }
120
+ expect(readDoneProjectItemIds(baseDir, 'acme', tab)).not.toContain(
121
+ projectItemId,
122
+ );
123
+ }
124
+ };
125
+
107
126
  describe('handleReview', () => {
108
127
  it('approves and sets Awaiting workspace then records done', async () => {
109
128
  const response = await handleReview(context, {
@@ -125,7 +144,7 @@ describe('consoleOperationApi', () => {
125
144
  'status_aw',
126
145
  );
127
146
  expect(issueRepository.get).not.toHaveBeenCalled();
128
- expectRecordedAcrossTabs('PVTI_a');
147
+ expectRecordedOnlyIn('PVTI_a', CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES);
129
148
  });
130
149
 
131
150
  it('resolves the issue repository from the url of the operated pull request', async () => {
@@ -173,7 +192,7 @@ describe('consoleOperationApi', () => {
173
192
  side: 'RIGHT',
174
193
  },
175
194
  );
176
- expectRecordedAcrossTabs('PVTI_b');
195
+ expectRecordedOnlyIn('PVTI_b', CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES);
177
196
  });
178
197
 
179
198
  it('requests changes without a line anchor when line and side are missing', async () => {
@@ -194,7 +213,7 @@ describe('consoleOperationApi', () => {
194
213
  'please fix',
195
214
  null,
196
215
  );
197
- expectRecordedAcrossTabs('PVTI_b');
216
+ expectRecordedOnlyIn('PVTI_b', CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES);
198
217
  });
199
218
 
200
219
  it('rejects request_changes without a comment body', async () => {
@@ -430,7 +449,37 @@ describe('consoleOperationApi', () => {
430
449
  'status_todo',
431
450
  );
432
451
  expect(issueRepository.get).not.toHaveBeenCalled();
433
- expectRecordedAcrossTabs('PVTI_d');
452
+ expectRecordedOnlyIn('PVTI_d', CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES);
453
+ });
454
+
455
+ it('keeps a status change out of the story-selected tabs', async () => {
456
+ const response = await handleTriage(context, {
457
+ pjcode: 'acme',
458
+ action: 'set_status',
459
+ issueUrl: 'https://github.com/o/r/issues/1',
460
+ projectItemId: 'PVTI_status_only',
461
+ statusName: 'Todo',
462
+ });
463
+ expect(response.statusCode).toBe(200);
464
+ expectRecordedOnlyIn(
465
+ 'PVTI_status_only',
466
+ CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
467
+ );
468
+ });
469
+
470
+ it('keeps a story change out of the status-selected tabs', async () => {
471
+ const response = await handleTriage(context, {
472
+ pjcode: 'acme',
473
+ action: 'set_story',
474
+ issueUrl: 'https://github.com/o/r/issues/1',
475
+ projectItemId: 'PVTI_story_only',
476
+ storyOptionId: 'story_opt_1',
477
+ });
478
+ expect(response.statusCode).toBe(200);
479
+ expectRecordedOnlyIn(
480
+ 'PVTI_story_only',
481
+ CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
482
+ );
434
483
  });
435
484
 
436
485
  it('rejects an unknown status name', async () => {
@@ -460,7 +509,7 @@ describe('consoleOperationApi', () => {
460
509
  'story_opt_1',
461
510
  );
462
511
  expect(issueRepository.get).not.toHaveBeenCalled();
463
- expectRecordedAcrossTabs('PVTI_e');
512
+ expectRecordedOnlyIn('PVTI_e', CONSOLE_DONE_STORY_SELECTED_TAB_NAMES);
464
513
  });
465
514
 
466
515
  it('snoozes for one day via updateNextActionDate', async () => {
@@ -794,7 +843,7 @@ describe('consoleOperationApi', () => {
794
843
  'status_intmux',
795
844
  );
796
845
  expect(issueRepository.get).not.toHaveBeenCalled();
797
- expectRecordedAcrossTabs('PVTI_i');
846
+ expectRecordedOnlyIn('PVTI_i', CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES);
798
847
  });
799
848
 
800
849
  it('rejects an unknown intmux action', async () => {
@@ -907,10 +956,12 @@ describe('consoleOperationApi', () => {
907
956
  expect.objectContaining({ itemId: 'PVTI_x' }),
908
957
  'status_todo',
909
958
  );
910
- for (const tab of CONSOLE_DONE_TAB_NAMES) {
959
+ for (const tab of CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES) {
911
960
  expect(readDoneProjectItemIds(baseDir, 'globex', tab)).toContain(
912
961
  'PVTI_x',
913
962
  );
963
+ }
964
+ for (const tab of CONSOLE_DONE_TAB_NAMES) {
914
965
  expect(readDoneProjectItemIds(baseDir, 'acme', tab)).not.toContain(
915
966
  'PVTI_x',
916
967
  );
@@ -5,7 +5,12 @@ import {
5
5
  import { IssueAttachmentRepository } from '../../../domain/usecases/adapter-interfaces/IssueAttachmentRepository';
6
6
  import { Project } from '../../../domain/entities/Project';
7
7
  import { Issue } from '../../../domain/entities/Issue';
8
- import { recordDoneProjectItemIdAcrossTabs } from './consoleDoneStore';
8
+ import {
9
+ recordDoneProjectItemIdAcrossTabs,
10
+ recordDoneProjectItemIdForTabs,
11
+ CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
12
+ CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
13
+ } from './consoleDoneStore';
9
14
  import { findConsoleItemUrl } from './consoleItemUrlLookup';
10
15
 
11
16
  export const AWAITING_WORKSPACE_STATUS_NAME = 'awaiting workspace';
@@ -135,6 +140,38 @@ const recordDone = (
135
140
  );
136
141
  };
137
142
 
143
+ const recordDoneForStatusChange = (
144
+ context: ConsoleOperationContext,
145
+ pjcode: string,
146
+ projectItemId: string,
147
+ ): void => {
148
+ if (context.consoleDataOutputDir === null) {
149
+ return;
150
+ }
151
+ recordDoneProjectItemIdForTabs(
152
+ context.consoleDataOutputDir,
153
+ pjcode,
154
+ projectItemId,
155
+ CONSOLE_DONE_STATUS_SELECTED_TAB_NAMES,
156
+ );
157
+ };
158
+
159
+ const recordDoneForStoryChange = (
160
+ context: ConsoleOperationContext,
161
+ pjcode: string,
162
+ projectItemId: string,
163
+ ): void => {
164
+ if (context.consoleDataOutputDir === null) {
165
+ return;
166
+ }
167
+ recordDoneProjectItemIdForTabs(
168
+ context.consoleDataOutputDir,
169
+ pjcode,
170
+ projectItemId,
171
+ CONSOLE_DONE_STORY_SELECTED_TAB_NAMES,
172
+ );
173
+ };
174
+
138
175
  const resolveBinding = async (
139
176
  context: ConsoleOperationContext,
140
177
  body: Record<string, unknown>,
@@ -298,7 +335,7 @@ export const handleReview = async (
298
335
  if (failure !== null) {
299
336
  return failure;
300
337
  }
301
- recordDone(context, pjcode, projectItemId);
338
+ recordDoneForStatusChange(context, pjcode, projectItemId);
302
339
  return ok();
303
340
  }
304
341
 
@@ -334,7 +371,7 @@ export const handleReview = async (
334
371
  if (failure !== null) {
335
372
  return failure;
336
373
  }
337
- recordDone(context, pjcode, projectItemId);
374
+ recordDoneForStatusChange(context, pjcode, projectItemId);
338
375
  return ok();
339
376
  }
340
377
 
@@ -403,7 +440,7 @@ export const handleTriage = async (
403
440
  if (failure !== null) {
404
441
  return failure;
405
442
  }
406
- recordDone(context, pjcode, projectItemId);
443
+ recordDoneForStatusChange(context, pjcode, projectItemId);
407
444
  return ok();
408
445
  }
409
446
 
@@ -422,7 +459,7 @@ export const handleTriage = async (
422
459
  projectItemReference(issueUrl, projectItemId),
423
460
  storyOptionId,
424
461
  );
425
- recordDone(context, pjcode, projectItemId);
462
+ recordDoneForStoryChange(context, pjcode, projectItemId);
426
463
  return ok();
427
464
  }
428
465
 
@@ -590,6 +627,6 @@ export const handleIntmux = async (
590
627
  if (failure !== null) {
591
628
  return failure;
592
629
  }
593
- recordDone(context, pjcode, projectItemId);
630
+ recordDoneForStatusChange(context, pjcode, projectItemId);
594
631
  return ok();
595
632
  };
@@ -4,10 +4,15 @@ import {
4
4
  isOverlayEntryActed,
5
5
  overlayEntriesActedSinceSnapshot,
6
6
  overlayKeyForItem,
7
+ overlayStatusSinceSnapshot,
7
8
  overlayStorageKey,
8
9
  writeOverlayEntry,
9
10
  } from './overlay';
10
- import type { ConsoleListItem, ConsoleOverlay } from './types';
11
+ import type {
12
+ ConsoleListItem,
13
+ ConsoleOverlay,
14
+ ConsoleOverlayStatus,
15
+ } from './types';
11
16
 
12
17
  const item = (number: number): ConsoleListItem => ({
13
18
  number,
@@ -230,3 +235,76 @@ describe('writeOverlayEntry', () => {
230
235
  });
231
236
  });
232
237
  });
238
+
239
+ describe('overlayStatusSinceSnapshot', () => {
240
+ const snapshotGeneratedAt = '2026-08-13T21:52:01.000Z';
241
+ const snapshotEpochMs = Date.parse(snapshotGeneratedAt);
242
+ const inTmuxByHuman: ConsoleOverlayStatus = {
243
+ name: 'In Tmux by human',
244
+ color: 'RED',
245
+ };
246
+
247
+ const overlayWithStatusAt = (epochMs: number): ConsoleOverlay => ({
248
+ [overlayKeyForItem(item(1))]: {
249
+ status: inTmuxByHuman,
250
+ ts: epochMs,
251
+ mode: 'todo-by-human',
252
+ },
253
+ });
254
+
255
+ it('shows a status the owner set after the snapshot was generated', () => {
256
+ expect(
257
+ overlayStatusSinceSnapshot(
258
+ overlayWithStatusAt(snapshotEpochMs + 1000),
259
+ item(1),
260
+ snapshotGeneratedAt,
261
+ ),
262
+ ).toEqual(inTmuxByHuman);
263
+ });
264
+
265
+ it('drops a status the snapshot already superseded', () => {
266
+ expect(
267
+ overlayStatusSinceSnapshot(
268
+ overlayWithStatusAt(snapshotEpochMs - 1000),
269
+ item(1),
270
+ snapshotGeneratedAt,
271
+ ),
272
+ ).toBeNull();
273
+ });
274
+
275
+ it('drops every status when the snapshot time is unusable', () => {
276
+ expect(
277
+ overlayStatusSinceSnapshot(
278
+ overlayWithStatusAt(snapshotEpochMs + 1000),
279
+ item(1),
280
+ null,
281
+ ),
282
+ ).toBeNull();
283
+ });
284
+
285
+ it('shows a status set at the exact moment the snapshot was generated', () => {
286
+ expect(
287
+ overlayStatusSinceSnapshot(
288
+ overlayWithStatusAt(snapshotEpochMs),
289
+ item(1),
290
+ snapshotGeneratedAt,
291
+ ),
292
+ ).toEqual(inTmuxByHuman);
293
+ });
294
+
295
+ it('drops every status when the snapshot time cannot be parsed', () => {
296
+ expect(
297
+ overlayStatusSinceSnapshot(
298
+ overlayWithStatusAt(snapshotEpochMs + 1000),
299
+ item(1),
300
+ 'not a timestamp',
301
+ ),
302
+ ).toBeNull();
303
+ });
304
+
305
+ it('returns null when the item has no overlay entry', () => {
306
+ expect(
307
+ overlayStatusSinceSnapshot({}, item(1), snapshotGeneratedAt),
308
+ ).toBeNull();
309
+ });
310
+ });
@@ -2,6 +2,7 @@ import type {
2
2
  ConsoleListItem,
3
3
  ConsoleOverlay,
4
4
  ConsoleOverlayEntry,
5
+ ConsoleOverlayStatus,
5
6
  ConsoleTabName,
6
7
  } from './types';
7
8
 
@@ -47,6 +48,25 @@ export const overlayEntriesActedSinceSnapshot = (
47
48
  return acted;
48
49
  };
49
50
 
51
+ export const overlayStatusSinceSnapshot = (
52
+ overlay: ConsoleOverlay,
53
+ item: ConsoleListItem,
54
+ snapshotGeneratedAt: string | null,
55
+ ): ConsoleOverlayStatus | null => {
56
+ if (snapshotGeneratedAt === null) {
57
+ return null;
58
+ }
59
+ const snapshotGeneratedAtMs = Date.parse(snapshotGeneratedAt);
60
+ if (Number.isNaN(snapshotGeneratedAtMs)) {
61
+ return null;
62
+ }
63
+ const entry = overlay[overlayKeyForItem(item)];
64
+ if (entry === undefined || entry.ts < snapshotGeneratedAtMs) {
65
+ return null;
66
+ }
67
+ return entry.status ?? null;
68
+ };
69
+
50
70
  export const writeOverlayEntry = (
51
71
  overlay: ConsoleOverlay,
52
72
  key: string,
@@ -106,6 +106,48 @@ describe('ConsolePage', () => {
106
106
  ).toContain('TDPM Console port');
107
107
  });
108
108
 
109
+ it('keeps a status the snapshot already superseded out of the detail header', async () => {
110
+ localStorage.setItem(
111
+ 'pv_overlay_acme',
112
+ JSON.stringify({
113
+ PVTI_1: {
114
+ status: { name: 'In Tmux by human', color: 'RED' },
115
+ ts: Date.parse('2026-06-18T00:00:00.000Z'),
116
+ mode: 'prs',
117
+ },
118
+ }),
119
+ );
120
+ const { getByText, findByText, container } = render(<ConsolePage />);
121
+ await waitFor(() => {
122
+ expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
123
+ });
124
+ fireEvent.click(getByText('Add serveConsole subcommand'));
125
+ expect(await findByText('Approve')).toBeInTheDocument();
126
+ expect(container.querySelector('.console-detail-status-chip')).toBeNull();
127
+ });
128
+
129
+ it('shows a status the owner set after the snapshot was generated in the detail header', async () => {
130
+ localStorage.setItem(
131
+ 'pv_overlay_acme',
132
+ JSON.stringify({
133
+ PVTI_1: {
134
+ status: { name: 'In Tmux by human', color: 'RED' },
135
+ ts: Date.parse('2026-06-19T00:10:00.000Z'),
136
+ mode: 'prs',
137
+ },
138
+ }),
139
+ );
140
+ const { getByText, findByText, container } = render(<ConsolePage />);
141
+ await waitFor(() => {
142
+ expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
143
+ });
144
+ fireEvent.click(getByText('Add serveConsole subcommand'));
145
+ expect(await findByText('Approve')).toBeInTheDocument();
146
+ expect(
147
+ container.querySelector('.console-detail-status-chip')?.textContent,
148
+ ).toBe('In Tmux by human');
149
+ });
150
+
109
151
  it('opens the detail view when an item is selected', async () => {
110
152
  const { getByText, findByText } = render(<ConsolePage />);
111
153
  await waitFor(() => {
@@ -30,6 +30,7 @@ import {
30
30
  filterPendingItems,
31
31
  overlayEntriesActedSinceSnapshot,
32
32
  overlayKeyForItem,
33
+ overlayStatusSinceSnapshot,
33
34
  } from '../logic/overlay';
34
35
  import type { ConsoleSwipeDirection } from '../logic/swipe';
35
36
  import { findNextNonEmptyTabToRight } from '../logic/tabAdvance';
@@ -164,8 +165,11 @@ export const ConsolePage = () => {
164
165
  if (selectedItem === null) {
165
166
  return null;
166
167
  }
167
- const entry = overlayState.overlay[overlayKeyForItem(selectedItem)];
168
- return entry?.status ?? null;
168
+ return overlayStatusSinceSnapshot(
169
+ overlayState.overlay,
170
+ selectedItem,
171
+ snapshots[activeTab]?.generatedAt ?? null,
172
+ );
169
173
  })();
170
174
 
171
175
  const storyNameForSelected =