github-issue-tower-defence-management 2.7.0 → 2.9.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.
Files changed (80) hide show
  1. package/.github/workflows/umino-project.yml +0 -8
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +1 -1
  4. package/bin/adapter/entry-points/cli/index.js +3 -1
  5. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  6. package/bin/adapter/entry-points/console/consoleAirplaneSnapshotApi.js +347 -0
  7. package/bin/adapter/entry-points/console/consoleAirplaneSnapshotApi.js.map +1 -0
  8. package/bin/adapter/entry-points/console/consoleOperationApi.js +12 -0
  9. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  10. package/bin/adapter/entry-points/console/ui-dist/assets/index-BSa2JTna.js +88 -0
  11. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  12. package/bin/adapter/entry-points/console/webServer.js +28 -0
  13. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  14. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +5 -1
  15. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  16. package/bin/adapter/repositories/projectFieldDefinition.js +1 -0
  17. package/bin/adapter/repositories/projectFieldDefinition.js.map +1 -1
  18. package/jest.config.js +1 -0
  19. package/package.json +1 -1
  20. package/src/adapter/entry-points/cli/index.ts +4 -1
  21. package/src/adapter/entry-points/console/consoleAirplaneSnapshotApi.test.ts +658 -0
  22. package/src/adapter/entry-points/console/consoleAirplaneSnapshotApi.ts +482 -0
  23. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +84 -0
  24. package/src/adapter/entry-points/console/consoleOperationApi.ts +17 -0
  25. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +6 -0
  26. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +5 -1
  27. package/src/adapter/entry-points/console/ui/jest-globals.cjs +26 -0
  28. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleFileDiff.test.tsx +10 -0
  29. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleFileDiff.tsx +23 -16
  30. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleAirplaneModeButton.stories.tsx +56 -0
  31. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleAirplaneModeButton.test.tsx +111 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleAirplaneModeButton.tsx +92 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +15 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +19 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.tsx +30 -1
  36. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleErrorToast.stories.tsx +10 -0
  37. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleNextActionDateActions.test.tsx +7 -1
  38. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleNextActionDateActions.tsx +14 -0
  39. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleUndoToast.test.tsx +1 -3
  40. package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleUndoToast.tsx +8 -7
  41. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useAirplaneMode.test.ts +304 -0
  42. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useAirplaneMode.ts +167 -0
  43. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.test.ts +15 -0
  44. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.ts +6 -1
  45. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.test.ts +77 -0
  46. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +48 -3
  47. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleFeaturesConfig.test.ts +51 -0
  48. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleFeaturesConfig.ts +39 -0
  49. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +215 -0
  50. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +20 -1
  51. package/src/adapter/entry-points/console/ui/src/features/console/lib/airplaneSnapshot.test.ts +361 -0
  52. package/src/adapter/entry-points/console/ui/src/features/console/lib/airplaneSnapshot.ts +401 -0
  53. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.ts +4 -0
  54. package/src/adapter/entry-points/console/ui/src/features/console/logic/actionToast.test.ts +22 -0
  55. package/src/adapter/entry-points/console/ui/src/features/console/logic/actionToast.ts +6 -0
  56. package/src/adapter/entry-points/console/ui/src/features/console/logic/operations.ts +5 -1
  57. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +209 -0
  58. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +27 -2
  59. package/src/adapter/entry-points/console/ui-dist/assets/index-BSa2JTna.js +88 -0
  60. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  61. package/src/adapter/entry-points/console/webServer.test.ts +189 -0
  62. package/src/adapter/entry-points/console/webServer.ts +38 -0
  63. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +63 -8
  64. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +7 -1
  65. package/src/adapter/repositories/projectFieldDefinition.test.ts +1 -0
  66. package/src/adapter/repositories/projectFieldDefinition.ts +1 -0
  67. package/src/domain/entities/Project.ts +1 -0
  68. package/src/domain/usecases/ClearPastNextActionDateHourUseCase.test.ts +1 -0
  69. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  70. package/types/adapter/entry-points/console/consoleAirplaneSnapshotApi.d.ts +90 -0
  71. package/types/adapter/entry-points/console/consoleAirplaneSnapshotApi.d.ts.map +1 -0
  72. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  73. package/types/adapter/entry-points/console/webServer.d.ts +1 -0
  74. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  75. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  76. package/types/adapter/repositories/projectFieldDefinition.d.ts.map +1 -1
  77. package/types/domain/entities/Project.d.ts +1 -0
  78. package/types/domain/entities/Project.d.ts.map +1 -1
  79. package/bin/adapter/entry-points/console/ui-dist/assets/index-CaxYBHiT.js +0 -86
  80. package/src/adapter/entry-points/console/ui-dist/assets/index-CaxYBHiT.js +0 -86
@@ -0,0 +1,51 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { useConsoleFeaturesConfig } from './useConsoleFeaturesConfig';
3
+
4
+ describe('useConsoleFeaturesConfig', () => {
5
+ afterEach(() => {
6
+ jest.restoreAllMocks();
7
+ });
8
+
9
+ it('returns airplaneMode false while loading (initial state)', () => {
10
+ global.fetch = jest.fn().mockReturnValue(new Promise(() => {}));
11
+ const { result } = renderHook(() => useConsoleFeaturesConfig());
12
+ expect(result.current.airplaneMode).toBe(false);
13
+ });
14
+
15
+ it('returns airplaneMode true when /api/features responds { airplaneMode: true }', async () => {
16
+ global.fetch = jest.fn().mockResolvedValue({
17
+ ok: true,
18
+ json: async () => ({ airplaneMode: true }),
19
+ });
20
+ const { result } = renderHook(() => useConsoleFeaturesConfig());
21
+ await waitFor(() => expect(result.current.airplaneMode).toBe(true));
22
+ });
23
+
24
+ it('returns airplaneMode false when /api/features responds { airplaneMode: false }', async () => {
25
+ global.fetch = jest.fn().mockResolvedValue({
26
+ ok: true,
27
+ json: async () => ({ airplaneMode: false }),
28
+ });
29
+ const { result } = renderHook(() => useConsoleFeaturesConfig());
30
+ await waitFor(() => expect(global.fetch).toHaveBeenCalled());
31
+ expect(result.current.airplaneMode).toBe(false);
32
+ });
33
+
34
+ it('returns airplaneMode false on fetch failure', async () => {
35
+ global.fetch = jest.fn().mockRejectedValue(new Error('network error'));
36
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
37
+ const { result } = renderHook(() => useConsoleFeaturesConfig());
38
+ await waitFor(() => expect(consoleSpy).toHaveBeenCalled());
39
+ expect(result.current.airplaneMode).toBe(false);
40
+ });
41
+
42
+ it('returns airplaneMode false when response is not ok', async () => {
43
+ global.fetch = jest.fn().mockResolvedValue({
44
+ ok: false,
45
+ json: async () => ({ airplaneMode: true }),
46
+ });
47
+ const { result } = renderHook(() => useConsoleFeaturesConfig());
48
+ await waitFor(() => expect(global.fetch).toHaveBeenCalled());
49
+ expect(result.current.airplaneMode).toBe(false);
50
+ });
51
+ });
@@ -0,0 +1,39 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export type ConsoleFeaturesConfig = {
4
+ airplaneMode: boolean;
5
+ };
6
+
7
+ const FEATURES_PATH = '/api/features';
8
+
9
+ export const useConsoleFeaturesConfig = (): ConsoleFeaturesConfig => {
10
+ const [config, setConfig] = useState<ConsoleFeaturesConfig>({
11
+ airplaneMode: false,
12
+ });
13
+
14
+ useEffect(() => {
15
+ fetch(FEATURES_PATH)
16
+ .then(async (response) => {
17
+ if (!response.ok) {
18
+ return;
19
+ }
20
+ const data: unknown = await response.json();
21
+ if (
22
+ data !== null &&
23
+ typeof data === 'object' &&
24
+ !Array.isArray(data) &&
25
+ 'airplaneMode' in data &&
26
+ typeof (data as { airplaneMode: unknown }).airplaneMode === 'boolean'
27
+ ) {
28
+ setConfig({
29
+ airplaneMode: (data as { airplaneMode: boolean }).airplaneMode,
30
+ });
31
+ }
32
+ })
33
+ .catch((err: unknown) => {
34
+ console.warn('Failed to fetch /api/features', err);
35
+ });
36
+ }, []);
37
+
38
+ return config;
39
+ };
@@ -1,4 +1,5 @@
1
1
  import { act, renderHook, waitFor } from '@testing-library/react';
2
+ import type { AirplaneSnapshot } from '../lib/airplaneSnapshot';
2
3
  import { CONSOLE_TABS } from '../logic/types';
3
4
  import {
4
5
  CONSOLE_TAB_REFRESH_INTERVAL_MS,
@@ -257,4 +258,218 @@ describe('useConsoleTabData', () => {
257
258
  expect(result.current.error).toBe('No project specified in the URL path.');
258
259
  expect(fetchMock).not.toHaveBeenCalled();
259
260
  });
261
+
262
+ it('renders board items from airplane snapshot without fetching when network rejects', async () => {
263
+ const fetchMock = jest.fn().mockRejectedValue(new Error('network down'));
264
+ global.fetch = fetchMock as unknown as typeof fetch;
265
+
266
+ const airplaneSnapshot: AirplaneSnapshot = {
267
+ capturedAt: '2026-08-01T10:00:00Z',
268
+ failures: [],
269
+ items: {},
270
+ tabs: {
271
+ acme: {
272
+ prs: {
273
+ generatedAt: '2026-08-01T10:00:00Z',
274
+ statusOptions: [],
275
+ storyOptions: [],
276
+ storyColors: {},
277
+ stories: [],
278
+ defaultNameWithOwner: null,
279
+ fromCache: false,
280
+ storyOrder: [],
281
+ items: [
282
+ {
283
+ number: 1,
284
+ title: 'Offline PR',
285
+ url: 'https://github.com/o/r/pull/1',
286
+ repo: 'o/r',
287
+ nameWithOwner: 'o/r',
288
+ projectItemId: 'PVTI_1',
289
+ itemId: 'PVTI_1',
290
+ isPr: true,
291
+ story: 'regular',
292
+ status: 'Awaiting Quality Check',
293
+ nextActionDate: null,
294
+ nextActionHour: null,
295
+ dependedIssueUrls: [],
296
+ labels: [],
297
+ createdAt: '2026-08-01T00:00:00Z',
298
+ relatedOpenPullRequestUrls: [],
299
+ agent: null,
300
+ },
301
+ ],
302
+ },
303
+ 'workflow-blocker': {
304
+ generatedAt: '2026-08-01T10:00:00Z',
305
+ statusOptions: [],
306
+ storyOptions: [],
307
+ storyColors: {},
308
+ items: [],
309
+ stories: [],
310
+ defaultNameWithOwner: null,
311
+ fromCache: false,
312
+ storyOrder: [],
313
+ },
314
+ 'failed-preparation': {
315
+ generatedAt: '2026-08-01T10:00:00Z',
316
+ statusOptions: [],
317
+ storyOptions: [],
318
+ storyColors: {},
319
+ items: [],
320
+ stories: [],
321
+ defaultNameWithOwner: null,
322
+ fromCache: false,
323
+ storyOrder: [],
324
+ },
325
+ 'todo-by-human': {
326
+ generatedAt: '2026-08-01T10:00:00Z',
327
+ statusOptions: [],
328
+ storyOptions: [],
329
+ storyColors: {},
330
+ items: [],
331
+ stories: [],
332
+ defaultNameWithOwner: null,
333
+ fromCache: false,
334
+ storyOrder: [],
335
+ },
336
+ 'todo-by-agent': {
337
+ generatedAt: '2026-08-01T10:00:00Z',
338
+ statusOptions: [],
339
+ storyOptions: [],
340
+ storyColors: {},
341
+ items: [],
342
+ stories: [],
343
+ defaultNameWithOwner: null,
344
+ fromCache: false,
345
+ storyOrder: [],
346
+ },
347
+ stories: {
348
+ generatedAt: '2026-08-01T10:00:00Z',
349
+ statusOptions: [],
350
+ storyOptions: [],
351
+ storyColors: {},
352
+ items: [],
353
+ stories: [],
354
+ defaultNameWithOwner: null,
355
+ fromCache: false,
356
+ storyOrder: [],
357
+ },
358
+ },
359
+ },
360
+ };
361
+
362
+ const { result } = renderHook(() =>
363
+ useConsoleTabData('acme', airplaneSnapshot),
364
+ );
365
+
366
+ await waitFor(() => {
367
+ expect(result.current.isLoading).toBe(false);
368
+ });
369
+
370
+ expect(fetchMock).not.toHaveBeenCalled();
371
+ expect(result.current.error).toBeNull();
372
+ expect(result.current.snapshots.prs?.items).toHaveLength(1);
373
+ expect(result.current.snapshots.prs?.items[0].title).toBe('Offline PR');
374
+ expect(result.current.snapshots.prs?.generatedAt).toBe(
375
+ '2026-08-01T10:00:00Z',
376
+ );
377
+ });
378
+
379
+ it('stops the 60-second periodic refresh while an airplane snapshot is in use', async () => {
380
+ jest.useFakeTimers();
381
+ const fetchMock = jest.fn().mockRejectedValue(new Error('network down'));
382
+ global.fetch = fetchMock as unknown as typeof fetch;
383
+
384
+ const airplaneSnapshot: AirplaneSnapshot = {
385
+ capturedAt: '2026-08-01T10:00:00Z',
386
+ failures: [],
387
+ items: {},
388
+ tabs: {
389
+ acme: {
390
+ prs: {
391
+ generatedAt: '2026-08-01T10:00:00Z',
392
+ statusOptions: [],
393
+ storyOptions: [],
394
+ storyColors: {},
395
+ items: [],
396
+ stories: [],
397
+ defaultNameWithOwner: null,
398
+ fromCache: false,
399
+ storyOrder: [],
400
+ },
401
+ 'workflow-blocker': {
402
+ generatedAt: '2026-08-01T10:00:00Z',
403
+ statusOptions: [],
404
+ storyOptions: [],
405
+ storyColors: {},
406
+ items: [],
407
+ stories: [],
408
+ defaultNameWithOwner: null,
409
+ fromCache: false,
410
+ storyOrder: [],
411
+ },
412
+ 'failed-preparation': {
413
+ generatedAt: '2026-08-01T10:00:00Z',
414
+ statusOptions: [],
415
+ storyOptions: [],
416
+ storyColors: {},
417
+ items: [],
418
+ stories: [],
419
+ defaultNameWithOwner: null,
420
+ fromCache: false,
421
+ storyOrder: [],
422
+ },
423
+ 'todo-by-human': {
424
+ generatedAt: '2026-08-01T10:00:00Z',
425
+ statusOptions: [],
426
+ storyOptions: [],
427
+ storyColors: {},
428
+ items: [],
429
+ stories: [],
430
+ defaultNameWithOwner: null,
431
+ fromCache: false,
432
+ storyOrder: [],
433
+ },
434
+ 'todo-by-agent': {
435
+ generatedAt: '2026-08-01T10:00:00Z',
436
+ statusOptions: [],
437
+ storyOptions: [],
438
+ storyColors: {},
439
+ items: [],
440
+ stories: [],
441
+ defaultNameWithOwner: null,
442
+ fromCache: false,
443
+ storyOrder: [],
444
+ },
445
+ stories: {
446
+ generatedAt: '2026-08-01T10:00:00Z',
447
+ statusOptions: [],
448
+ storyOptions: [],
449
+ storyColors: {},
450
+ items: [],
451
+ stories: [],
452
+ defaultNameWithOwner: null,
453
+ fromCache: false,
454
+ storyOrder: [],
455
+ },
456
+ },
457
+ },
458
+ };
459
+
460
+ const { result } = renderHook(() =>
461
+ useConsoleTabData('acme', airplaneSnapshot),
462
+ );
463
+
464
+ await waitFor(() => {
465
+ expect(result.current.isLoading).toBe(false);
466
+ });
467
+
468
+ act(() => {
469
+ jest.advanceTimersByTime(CONSOLE_TAB_REFRESH_INTERVAL_MS * 3);
470
+ });
471
+
472
+ expect(fetchMock).not.toHaveBeenCalled();
473
+ jest.useRealTimers();
474
+ });
260
475
  });
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
+ import type { AirplaneSnapshot } from '../lib/airplaneSnapshot';
2
3
  import type {
3
4
  ConsoleFieldOption,
4
5
  ConsoleListItem,
@@ -205,6 +206,7 @@ const fetchSnapshots = async (
205
206
 
206
207
  export const useConsoleTabData = (
207
208
  pjcode: string | null,
209
+ airplaneSnapshot: AirplaneSnapshot | null = null,
208
210
  ): ConsoleTabDataState => {
209
211
  const [snapshots, setSnapshots] =
210
212
  useState<Record<ConsoleTabName, ConsoleTabSnapshot | null>>(emptySnapshots);
@@ -212,6 +214,23 @@ export const useConsoleTabData = (
212
214
  const [error, setError] = useState<string | null>(null);
213
215
 
214
216
  useEffect(() => {
217
+ if (airplaneSnapshot !== null && pjcode !== null) {
218
+ const pjTabs = airplaneSnapshot.tabs[pjcode];
219
+ if (pjTabs !== undefined) {
220
+ const next = emptySnapshots();
221
+ for (const tab of CONSOLE_TABS) {
222
+ const stored = pjTabs[tab.name];
223
+ if (stored !== undefined) {
224
+ next[tab.name] = stored;
225
+ }
226
+ }
227
+ setSnapshots(next);
228
+ setIsLoading(false);
229
+ setError(null);
230
+ return;
231
+ }
232
+ }
233
+
215
234
  let cancelled = false;
216
235
  setIsLoading(true);
217
236
  setError(null);
@@ -251,7 +270,7 @@ export const useConsoleTabData = (
251
270
  cancelled = true;
252
271
  clearInterval(timer);
253
272
  };
254
- }, [pjcode]);
273
+ }, [pjcode, airplaneSnapshot]);
255
274
 
256
275
  return { snapshots, isLoading, error };
257
276
  };