github-issue-tower-defence-management 1.91.1 → 1.91.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [1.91.2](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.91.1...v1.91.2) (2026-06-20)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **console:** apply processed-item overlay to all tab badges so a zeroed tab stays zero ([#871](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/871)) ([4eda1ab](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/4eda1ab29cce54969c5fcc2b8a5ff838013978f7)), closes [#868](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/868)
7
+
1
8
  ## [1.91.1](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.91.0...v1.91.1) (2026-06-20)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.91.1",
3
+ "version": "1.91.2",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -1,12 +1,9 @@
1
1
  import {
2
2
  countPendingItems,
3
3
  filterPendingItems,
4
- getOverlayEntry,
5
- isOverlayEntryActedForMode,
6
- isOverlayEntryExpiredForMode,
4
+ isOverlayEntryActed,
7
5
  overlayKeyForItem,
8
6
  overlayStorageKey,
9
- parseGeneratedAtMs,
10
7
  writeOverlayEntry,
11
8
  } from './overlay';
12
9
  import type { ConsoleListItem, ConsoleOverlay } from './types';
@@ -34,68 +31,71 @@ describe('overlay helpers', () => {
34
31
  expect(overlayKeyForItem(item(5))).toBe('PVTI_5');
35
32
  });
36
33
 
37
- it('parses a generatedAt timestamp', () => {
38
- expect(parseGeneratedAtMs('2026-06-10T00:00:00.000Z')).toBe(
39
- Date.parse('2026-06-10T00:00:00.000Z'),
40
- );
41
- expect(parseGeneratedAtMs('not-a-date')).toBe(0);
34
+ it('falls back to the itemId when the projectItemId is empty', () => {
35
+ expect(overlayKeyForItem({ ...item(5), projectItemId: '' })).toBe('PVTI_5');
42
36
  });
43
37
  });
44
38
 
45
- describe('mode-aware expiry', () => {
46
- it('expires a same-mode entry written before the snapshot', () => {
47
- expect(
48
- isOverlayEntryExpiredForMode(
49
- { ts: 100, mode: 'prs', done: true },
50
- 200,
51
- 'prs',
52
- ),
53
- ).toBe(true);
39
+ describe('isOverlayEntryActed', () => {
40
+ it('treats a done entry as acted', () => {
41
+ expect(isOverlayEntryActed({ ts: 100, mode: 'prs', done: true })).toBe(
42
+ true,
43
+ );
54
44
  });
55
45
 
56
- it('does not expire an entry written in a different mode', () => {
57
- expect(
58
- isOverlayEntryExpiredForMode(
59
- { ts: 100, mode: 'triage', done: true },
60
- 200,
61
- 'prs',
62
- ),
63
- ).toBe(false);
46
+ it('treats a missing entry as not acted', () => {
47
+ expect(isOverlayEntryActed(undefined)).toBe(false);
64
48
  });
65
49
 
66
- it('does not expire an entry written after the snapshot', () => {
50
+ it('treats an entry without done as not acted', () => {
67
51
  expect(
68
- isOverlayEntryExpiredForMode(
69
- { ts: 300, mode: 'prs', done: true },
70
- 200,
71
- 'prs',
72
- ),
52
+ isOverlayEntryActed({
53
+ ts: 100,
54
+ mode: 'prs',
55
+ story: { name: 'Story', color: 'BLUE' },
56
+ }),
73
57
  ).toBe(false);
74
58
  });
59
+
60
+ it('treats a done entry as acted regardless of the mode it was written in', () => {
61
+ expect(isOverlayEntryActed({ ts: 100, mode: 'triage', done: true })).toBe(
62
+ true,
63
+ );
64
+ });
75
65
  });
76
66
 
77
67
  describe('counts driven to zero do not revive on tab switch', () => {
78
- it('keeps a done item subtracted in its own mode', () => {
68
+ it('keeps a done item subtracted in the tab it was processed in', () => {
79
69
  const overlay: ConsoleOverlay = {
80
70
  PVTI_1: { ts: 500, mode: 'prs', done: true },
81
71
  };
82
- const generatedAtMs = 400;
83
- expect(countPendingItems([item(1)], overlay, generatedAtMs, 'prs')).toBe(0);
72
+ expect(countPendingItems([item(1)], overlay)).toBe(0);
84
73
  });
85
74
 
86
- it('treats a done entry from another mode as still acted', () => {
75
+ it('keeps a done item subtracted from every tab regardless of its mode', () => {
87
76
  const overlay: ConsoleOverlay = {
88
77
  PVTI_1: { ts: 100, mode: 'triage', done: true },
89
78
  };
90
- expect(isOverlayEntryActedForMode(overlay.PVTI_1, 999, 'prs')).toBe(true);
91
- expect(countPendingItems([item(1)], overlay, 999, 'prs')).toBe(0);
79
+ expect(countPendingItems([item(1)], overlay)).toBe(0);
80
+ });
81
+
82
+ it('does not revive a done item even when an entry was processed before the snapshot it still appears in', () => {
83
+ const overlay: ConsoleOverlay = {
84
+ PVTI_1: { ts: 100, mode: 'prs', done: true },
85
+ };
86
+ expect(countPendingItems([item(1)], overlay)).toBe(0);
92
87
  });
93
88
 
94
- it('revives the count only when a newer same-mode snapshot supersedes the entry', () => {
89
+ it('does not revive a done item when it appears in a tab other than the one it was processed in', () => {
95
90
  const overlay: ConsoleOverlay = {
96
91
  PVTI_1: { ts: 100, mode: 'prs', done: true },
97
92
  };
98
- expect(countPendingItems([item(1)], overlay, 200, 'prs')).toBe(1);
93
+ expect(countPendingItems([item(1)], overlay)).toBe(0);
94
+ expect(filterPendingItems([item(1)], overlay)).toEqual([]);
95
+ });
96
+
97
+ it('counts an item that has no done entry', () => {
98
+ expect(countPendingItems([item(1)], {})).toBe(1);
99
99
  });
100
100
  });
101
101
 
@@ -104,21 +104,43 @@ describe('filterPendingItems', () => {
104
104
  const overlay: ConsoleOverlay = {
105
105
  PVTI_1: { ts: 500, mode: 'prs', done: true },
106
106
  };
107
- const result = filterPendingItems([item(1), item(2)], overlay, 400, 'prs');
107
+ const result = filterPendingItems([item(1), item(2)], overlay);
108
108
  expect(result.map((entry) => entry.number)).toEqual([2]);
109
109
  });
110
- });
111
110
 
112
- describe('getOverlayEntry and writeOverlayEntry', () => {
113
- it('returns null for an expired entry', () => {
111
+ it('keeps the badge count and the filtered list consistent', () => {
114
112
  const overlay: ConsoleOverlay = {
115
- PVTI_1: { ts: 100, mode: 'prs', done: true },
113
+ PVTI_1: { ts: 500, mode: 'prs', done: true },
116
114
  };
117
- expect(getOverlayEntry(overlay, item(1), 200, 'prs')).toBeNull();
115
+ const items = [item(1), item(2)];
116
+ expect(countPendingItems(items, overlay)).toBe(
117
+ filterPendingItems(items, overlay).length,
118
+ );
118
119
  });
120
+ });
119
121
 
122
+ describe('writeOverlayEntry', () => {
120
123
  it('stamps the timestamp and mode on write', () => {
121
124
  const next = writeOverlayEntry({}, 'PVTI_1', { done: true }, 'prs', 1234);
122
125
  expect(next.PVTI_1).toEqual({ done: true, ts: 1234, mode: 'prs' });
123
126
  });
127
+
128
+ it('merges a patch into an existing entry while refreshing ts and mode', () => {
129
+ const overlay: ConsoleOverlay = {
130
+ PVTI_1: { ts: 100, mode: 'prs', done: true },
131
+ };
132
+ const next = writeOverlayEntry(
133
+ overlay,
134
+ 'PVTI_1',
135
+ { story: { name: 'New Story', color: 'GREEN' } },
136
+ 'triage',
137
+ 999,
138
+ );
139
+ expect(next.PVTI_1).toEqual({
140
+ done: true,
141
+ story: { name: 'New Story', color: 'GREEN' },
142
+ ts: 999,
143
+ mode: 'triage',
144
+ });
145
+ });
124
146
  });
@@ -11,76 +11,23 @@ export const overlayStorageKey = (pjcode: string): string =>
11
11
  export const overlayKeyForItem = (item: ConsoleListItem): string =>
12
12
  item.projectItemId !== '' ? item.projectItemId : item.itemId;
13
13
 
14
- export const parseGeneratedAtMs = (generatedAt: string): number => {
15
- const parsed = Date.parse(generatedAt);
16
- return Number.isNaN(parsed) ? 0 : parsed;
17
- };
18
-
19
- export const isOverlayEntryExpiredForMode = (
20
- entry: ConsoleOverlayEntry,
21
- generatedAtMs: number,
22
- mode: ConsoleTabName,
23
- ): boolean =>
24
- entry.ts > 0 &&
25
- generatedAtMs > 0 &&
26
- entry.mode === mode &&
27
- entry.ts < generatedAtMs;
28
-
29
- export const getOverlayEntry = (
30
- overlay: ConsoleOverlay,
31
- item: ConsoleListItem,
32
- generatedAtMs: number,
33
- mode: ConsoleTabName,
34
- ): ConsoleOverlayEntry | null => {
35
- const entry = overlay[overlayKeyForItem(item)];
36
- if (entry === undefined) {
37
- return null;
38
- }
39
- if (isOverlayEntryExpiredForMode(entry, generatedAtMs, mode)) {
40
- return null;
41
- }
42
- return entry;
43
- };
44
-
45
- export const isOverlayEntryActedForMode = (
14
+ export const isOverlayEntryActed = (
46
15
  entry: ConsoleOverlayEntry | undefined,
47
- generatedAtMs: number,
48
- mode: ConsoleTabName,
49
- ): boolean => {
50
- if (entry === undefined || entry.done !== true) {
51
- return false;
52
- }
53
- return !isOverlayEntryExpiredForMode(entry, generatedAtMs, mode);
54
- };
16
+ ): boolean => entry !== undefined && entry.done === true;
55
17
 
56
18
  export const countPendingItems = (
57
19
  items: ConsoleListItem[],
58
20
  overlay: ConsoleOverlay,
59
- generatedAtMs: number,
60
- mode: ConsoleTabName,
61
21
  ): number =>
62
- items.filter(
63
- (item) =>
64
- !isOverlayEntryActedForMode(
65
- overlay[overlayKeyForItem(item)],
66
- generatedAtMs,
67
- mode,
68
- ),
69
- ).length;
22
+ items.filter((item) => !isOverlayEntryActed(overlay[overlayKeyForItem(item)]))
23
+ .length;
70
24
 
71
25
  export const filterPendingItems = (
72
26
  items: ConsoleListItem[],
73
27
  overlay: ConsoleOverlay,
74
- generatedAtMs: number,
75
- mode: ConsoleTabName,
76
28
  ): ConsoleListItem[] =>
77
29
  items.filter(
78
- (item) =>
79
- !isOverlayEntryActedForMode(
80
- overlay[overlayKeyForItem(item)],
81
- generatedAtMs,
82
- mode,
83
- ),
30
+ (item) => !isOverlayEntryActed(overlay[overlayKeyForItem(item)]),
84
31
  );
85
32
 
86
33
  export const writeOverlayEntry = (
@@ -28,7 +28,23 @@ const listPayload = (tab: string) => ({
28
28
  createdAt: '2026-06-17T00:00:00.000Z',
29
29
  },
30
30
  ]
31
- : [],
31
+ : tab === 'unread'
32
+ ? [
33
+ {
34
+ number: 866,
35
+ title: 'Notify finished issue preparation',
36
+ url: 'https://github.com/o/r/issues/866',
37
+ repo: 'o/r',
38
+ nameWithOwner: 'o/r',
39
+ projectItemId: 'PVTI_2',
40
+ itemId: 'PVTI_2',
41
+ isPr: false,
42
+ story: 'TDPM Console port',
43
+ labels: [],
44
+ createdAt: '2026-06-18T00:00:00.000Z',
45
+ },
46
+ ]
47
+ : [],
32
48
  });
33
49
 
34
50
  const installFetch = (): void => {
@@ -71,4 +87,42 @@ describe('ConsolePage', () => {
71
87
  expect(await findByText('← Back to list')).toBeInTheDocument();
72
88
  expect(getByText('Approve')).toBeInTheDocument();
73
89
  });
90
+
91
+ it('keeps a tab driven to zero at zero and does not revive its badge after switching tabs', async () => {
92
+ const { getByText, queryByText, findByText } = render(<ConsolePage />);
93
+ await waitFor(() => {
94
+ expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
95
+ });
96
+ expect(
97
+ getByText('Awaiting Quality Check')
98
+ .closest('button')
99
+ ?.querySelector('.console-tab-badge')?.textContent,
100
+ ).toBe('1');
101
+
102
+ fireEvent.click(getByText('Add serveConsole subcommand'));
103
+ expect(await findByText('← Back to list')).toBeInTheDocument();
104
+ fireEvent.click(getByText('Approve'));
105
+
106
+ await waitFor(() => {
107
+ expect(
108
+ getByText('Awaiting Quality Check')
109
+ .closest('button')
110
+ ?.querySelector('.console-tab-badge')?.textContent,
111
+ ).toBe('0');
112
+ });
113
+
114
+ fireEvent.click(getByText('Unread'));
115
+ await waitFor(() => {
116
+ expect(
117
+ getByText('Notify finished issue preparation'),
118
+ ).toBeInTheDocument();
119
+ });
120
+
121
+ const prsTabButton = queryByText('Awaiting Quality Check')?.closest(
122
+ 'button',
123
+ );
124
+ const prsBadge =
125
+ prsTabButton?.querySelector('.console-tab-badge')?.textContent ?? '0';
126
+ expect(prsBadge).toBe('0');
127
+ });
74
128
  });
@@ -12,7 +12,6 @@ import {
12
12
  countPendingItems,
13
13
  filterPendingItems,
14
14
  overlayKeyForItem,
15
- parseGeneratedAtMs,
16
15
  } from '../logic/overlay';
17
16
  import type {
18
17
  ConsoleListItem,
@@ -57,8 +56,6 @@ export const ConsolePage = () => {
57
56
  result[tab.name] = countPendingItems(
58
57
  snapshot.items,
59
58
  overlayState.overlay,
60
- parseGeneratedAtMs(snapshot.generatedAt),
61
- tab.name,
62
59
  );
63
60
  }
64
61
  return result;
@@ -69,13 +66,8 @@ export const ConsolePage = () => {
69
66
  if (activeSnapshot === null) {
70
67
  return [];
71
68
  }
72
- return filterPendingItems(
73
- activeSnapshot.items,
74
- overlayState.overlay,
75
- parseGeneratedAtMs(activeSnapshot.generatedAt),
76
- activeTab,
77
- );
78
- }, [activeSnapshot, overlayState.overlay, activeTab]);
69
+ return filterPendingItems(activeSnapshot.items, overlayState.overlay);
70
+ }, [activeSnapshot, overlayState.overlay]);
79
71
 
80
72
  const rows = useMemo(
81
73
  () => buildConsoleListRows(pendingItems, overlayState.overlay),