github-issue-tower-defence-management 1.95.0 → 1.97.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.
- package/.github/workflows/console-ui.yml +17 -0
- package/CHANGELOG.md +14 -0
- package/README.md +20 -0
- package/bin/adapter/entry-points/cli/index.js +21 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-BeJzGnfH.js → index-B3F4E8LD.js} +33 -33
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +18 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js +77 -0
- package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js.map +1 -0
- package/bin/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.js +19 -0
- package/bin/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.js.map +1 -0
- package/bin/adapter/repositories/NodeTmuxSessionRepository.js +44 -0
- package/bin/adapter/repositories/NodeTmuxSessionRepository.js.map +1 -0
- package/bin/adapter/repositories/ProcClaudeLiveSessionRepository.js +139 -0
- package/bin/adapter/repositories/ProcClaudeLiveSessionRepository.js.map +1 -0
- package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js +59 -0
- package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/TmuxSessionRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/TmuxSessionRepository.js.map +1 -0
- package/bin/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.js +41 -0
- package/bin/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.js.map +1 -0
- package/package.json +4 -1
- package/src/adapter/entry-points/cli/index.ts +38 -0
- package/src/adapter/entry-points/console/ui/biome.json +7 -1
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +93 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +380 -0
- package/src/adapter/entry-points/console/ui/e2e/playwright.config.ts +30 -0
- package/src/adapter/entry-points/console/ui/e2e/tsconfig.json +18 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +2 -4
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +5 -6
- package/src/adapter/entry-points/console/ui-dist/assets/{index-BeJzGnfH.js → index-B3F4E8LD.js} +33 -33
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +17 -0
- package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.test.ts +268 -0
- package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.ts +116 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +123 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.ts +29 -0
- package/src/adapter/repositories/NodeTmuxSessionRepository.test.ts +100 -0
- package/src/adapter/repositories/NodeTmuxSessionRepository.ts +53 -0
- package/src/adapter/repositories/ProcClaudeLiveSessionRepository.test.ts +146 -0
- package/src/adapter/repositories/ProcClaudeLiveSessionRepository.ts +119 -0
- package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.test.ts +147 -0
- package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.ts +100 -0
- package/src/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.ts +8 -0
- package/src/domain/usecases/adapter-interfaces/TmuxSessionRepository.ts +9 -0
- package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +186 -0
- package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.ts +75 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.d.ts +20 -0
- package/types/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.d.ts.map +1 -0
- package/types/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.d.ts +10 -0
- package/types/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.d.ts.map +1 -0
- package/types/adapter/repositories/NodeTmuxSessionRepository.d.ts +10 -0
- package/types/adapter/repositories/NodeTmuxSessionRepository.d.ts.map +1 -0
- package/types/adapter/repositories/ProcClaudeLiveSessionRepository.d.ts +11 -0
- package/types/adapter/repositories/ProcClaudeLiveSessionRepository.d.ts.map +1 -0
- package/types/domain/usecases/LiveSessionOauthTokenSelectUseCase.d.ts +23 -0
- package/types/domain/usecases/LiveSessionOauthTokenSelectUseCase.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.d.ts +8 -0
- package/types/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/TmuxSessionRepository.d.ts +6 -0
- package/types/domain/usecases/adapter-interfaces/TmuxSessionRepository.d.ts.map +1 -0
- package/types/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.d.ts +19 -0
- package/types/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.d.ts.map +1 -0
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
createConsoleProjectResolver,
|
|
43
43
|
} from '../console/consoleProjectResolver';
|
|
44
44
|
import { OauthTokenSelectHandler } from '../handlers/OauthTokenSelectHandler';
|
|
45
|
+
import { LiveSessionOauthTokenSelectHandler } from '../handlers/LiveSessionOauthTokenSelectHandler';
|
|
45
46
|
|
|
46
47
|
type StartDaemonOptions = {
|
|
47
48
|
projectUrl?: string;
|
|
@@ -89,6 +90,11 @@ type SelectOauthTokenOptions = {
|
|
|
89
90
|
cacheDir?: string;
|
|
90
91
|
};
|
|
91
92
|
|
|
93
|
+
type SelectLiveSessionOauthTokenOptions = {
|
|
94
|
+
tokenListJsonPath?: string;
|
|
95
|
+
cacheDir?: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
92
98
|
const buildGithubRepositoryParams = (
|
|
93
99
|
localStorageRepository: LocalStorageRepository,
|
|
94
100
|
token: string,
|
|
@@ -757,6 +763,38 @@ program
|
|
|
757
763
|
process.stdout.write(`${output.selectedToken}\n`);
|
|
758
764
|
});
|
|
759
765
|
|
|
766
|
+
program
|
|
767
|
+
.command('selectLiveSessionOauthToken')
|
|
768
|
+
.description(
|
|
769
|
+
'Print exactly one Claude Code OAuth token chosen for a new live interactive session. Among rate-limit-eligible tokens it prefers the one with the fewest current live sessions (by distinct CLAUDE_CODE_SESSION_ID found in running Claude Code processes), tiebreaking on the soonest 7d reset. The token string is written to stdout (pipeable); the per-candidate decision trace is written to stderr. Exits non-zero when no token passes the filter.',
|
|
770
|
+
)
|
|
771
|
+
.option(
|
|
772
|
+
'--tokenListJsonPath <path>',
|
|
773
|
+
'Path to the JSON array of { name, token } records. Falls back to the CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH environment variable.',
|
|
774
|
+
)
|
|
775
|
+
.option(
|
|
776
|
+
'--cacheDir <path>',
|
|
777
|
+
'Directory holding per-token rate-limit cache files. Falls back to the TDPM_RATELIMIT_CACHE_DIR environment variable, then to ${XDG_CACHE_HOME:-~/.cache}/tdpm/ratelimit.',
|
|
778
|
+
)
|
|
779
|
+
.action((options: SelectLiveSessionOauthTokenOptions) => {
|
|
780
|
+
const handler = new LiveSessionOauthTokenSelectHandler();
|
|
781
|
+
const output = handler.handle({
|
|
782
|
+
tokenListJsonPath: options.tokenListJsonPath ?? null,
|
|
783
|
+
cacheDirectory: options.cacheDir ?? null,
|
|
784
|
+
nowEpochSeconds: Date.now() / 1000,
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
for (const line of output.diagnostics) {
|
|
788
|
+
console.error(line);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (output.selectedToken === null) {
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
process.stdout.write(`${output.selectedToken}\n`);
|
|
796
|
+
});
|
|
797
|
+
|
|
760
798
|
/* istanbul ignore next */
|
|
761
799
|
if (process.argv && require.main === module) {
|
|
762
800
|
program.parse(process.argv);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { expect, type Page, test } from '@playwright/test';
|
|
2
|
+
import {
|
|
3
|
+
type ConsoleE2eHarness,
|
|
4
|
+
startConsoleE2eHarness,
|
|
5
|
+
} from './consoleTestHarness';
|
|
6
|
+
|
|
7
|
+
let harness: ConsoleE2eHarness;
|
|
8
|
+
|
|
9
|
+
test.beforeAll(async () => {
|
|
10
|
+
harness = await startConsoleE2eHarness();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test.afterAll(async () => {
|
|
14
|
+
if (harness !== undefined) {
|
|
15
|
+
await harness.stop();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const activeTabLabel = (page: Page) =>
|
|
20
|
+
page.locator('.console-tab[data-active="true"] .console-tab-label');
|
|
21
|
+
|
|
22
|
+
const tabByLabel = (page: Page, label: string) =>
|
|
23
|
+
page.locator('.console-tab', { hasText: label });
|
|
24
|
+
|
|
25
|
+
const tabBadge = (page: Page, label: string) =>
|
|
26
|
+
tabByLabel(page, label).locator('.console-tab-badge');
|
|
27
|
+
|
|
28
|
+
const itemRowByText = (page: Page, text: string) =>
|
|
29
|
+
page.locator('.console-item-row', { hasText: text });
|
|
30
|
+
|
|
31
|
+
const processSelectedItemViaStatus = async (page: Page): Promise<void> => {
|
|
32
|
+
const statusButton = page
|
|
33
|
+
.locator('.console-op-button', { hasText: 'Awaiting Workspace' })
|
|
34
|
+
.first();
|
|
35
|
+
await expect(statusButton).toBeVisible();
|
|
36
|
+
await statusButton.click();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
test('processing tabs drives auto-advance and keeps emptied badges at zero', async ({
|
|
40
|
+
page,
|
|
41
|
+
}) => {
|
|
42
|
+
await page.goto(harness.appUrl);
|
|
43
|
+
|
|
44
|
+
await expect(activeTabLabel(page)).toHaveText('Awaiting Quality Check');
|
|
45
|
+
await expect(tabBadge(page, 'Awaiting Quality Check')).toHaveText('1');
|
|
46
|
+
await expect(tabBadge(page, 'Unread')).toHaveText('2');
|
|
47
|
+
await expect(tabBadge(page, 'Triage')).toHaveText('2');
|
|
48
|
+
await expect(tabBadge(page, 'Todo by human')).toHaveText('1');
|
|
49
|
+
|
|
50
|
+
await itemRowByText(
|
|
51
|
+
page,
|
|
52
|
+
'Serve the committed console UI bundle from serveConsole',
|
|
53
|
+
).click();
|
|
54
|
+
const approveButton = page
|
|
55
|
+
.locator('.console-op-button', { hasText: 'Approve' })
|
|
56
|
+
.first();
|
|
57
|
+
await expect(approveButton).toBeVisible();
|
|
58
|
+
await approveButton.click();
|
|
59
|
+
|
|
60
|
+
await expect(activeTabLabel(page)).toHaveText('Triage', { timeout: 8000 });
|
|
61
|
+
await expect(tabByLabel(page, 'Awaiting Quality Check')).toHaveCount(0, {
|
|
62
|
+
timeout: 8000,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await itemRowByText(
|
|
66
|
+
page,
|
|
67
|
+
'Add Sonnet to Opus weekly-limit fallback routing per token',
|
|
68
|
+
).click();
|
|
69
|
+
await processSelectedItemViaStatus(page);
|
|
70
|
+
await expect(tabBadge(page, 'Triage')).toHaveText('1', { timeout: 8000 });
|
|
71
|
+
await page.locator('.console-back-button').click();
|
|
72
|
+
|
|
73
|
+
await itemRowByText(
|
|
74
|
+
page,
|
|
75
|
+
'Publish the generated documentation site to GitHub Pages',
|
|
76
|
+
).click();
|
|
77
|
+
await processSelectedItemViaStatus(page);
|
|
78
|
+
|
|
79
|
+
await expect(activeTabLabel(page)).toHaveText('Unread', { timeout: 8000 });
|
|
80
|
+
await expect(tabByLabel(page, 'Triage')).toHaveCount(0, {
|
|
81
|
+
timeout: 8000,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await tabByLabel(page, 'Todo by human').click();
|
|
85
|
+
await expect(activeTabLabel(page)).toHaveText('Todo by human');
|
|
86
|
+
await expect(tabByLabel(page, 'Triage')).toHaveCount(0);
|
|
87
|
+
await expect(tabBadge(page, 'Todo by human')).toHaveText('1');
|
|
88
|
+
|
|
89
|
+
await tabByLabel(page, 'Unread').click();
|
|
90
|
+
await expect(activeTabLabel(page)).toHaveText('Unread');
|
|
91
|
+
await expect(tabByLabel(page, 'Triage')).toHaveCount(0);
|
|
92
|
+
await expect(tabBadge(page, 'Unread')).toHaveText('2');
|
|
93
|
+
});
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import type * as http from 'node:http';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import type { Issue } from '../../../../../domain/entities/Issue';
|
|
6
|
+
import type { Project } from '../../../../../domain/entities/Project';
|
|
7
|
+
import type {
|
|
8
|
+
IssueComment,
|
|
9
|
+
IssueRepository,
|
|
10
|
+
PullRequestCommit,
|
|
11
|
+
PullRequestDetail,
|
|
12
|
+
RelatedPullRequest,
|
|
13
|
+
} from '../../../../../domain/usecases/adapter-interfaces/IssueRepository';
|
|
14
|
+
import type { ConsoleProjectBinding } from '../../consoleOperationApi';
|
|
15
|
+
import { IssueTitleStateCache } from '../../consoleReadApi';
|
|
16
|
+
import { startConsoleServer } from '../../consoleServer';
|
|
17
|
+
|
|
18
|
+
export const CONSOLE_E2E_PJCODE = 'umino';
|
|
19
|
+
export const CONSOLE_E2E_TOKEN = 'console-e2e-fixture-token-3f9c1a';
|
|
20
|
+
|
|
21
|
+
type ConsoleFixtureListItem = {
|
|
22
|
+
number: number;
|
|
23
|
+
title: string;
|
|
24
|
+
url: string;
|
|
25
|
+
repo: string;
|
|
26
|
+
nameWithOwner: string;
|
|
27
|
+
projectItemId: string;
|
|
28
|
+
itemId: string;
|
|
29
|
+
isPr: boolean;
|
|
30
|
+
story: string;
|
|
31
|
+
labels: string[];
|
|
32
|
+
createdAt: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type ConsoleFixtureFieldOption = {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
color: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type ConsoleFixtureSnapshot = {
|
|
42
|
+
pjcode: string;
|
|
43
|
+
generatedAt: string;
|
|
44
|
+
statusOptions: ConsoleFixtureFieldOption[];
|
|
45
|
+
storyOptions: ConsoleFixtureFieldOption[];
|
|
46
|
+
storyColors: Record<string, { color: string }>;
|
|
47
|
+
items: ConsoleFixtureListItem[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const REPO_NAME_WITH_OWNER =
|
|
51
|
+
'HiromiShikata/npm-cli-github-issue-tower-defence-management';
|
|
52
|
+
|
|
53
|
+
const AWAITING_WORKSPACE_OPTION: ConsoleFixtureFieldOption = {
|
|
54
|
+
id: 'd1c19cce',
|
|
55
|
+
name: 'Awaiting Workspace',
|
|
56
|
+
color: 'BLUE',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const STATUS_OPTIONS: ConsoleFixtureFieldOption[] = [
|
|
60
|
+
{ id: 'f75ad846', name: 'Unread', color: 'ORANGE' },
|
|
61
|
+
AWAITING_WORKSPACE_OPTION,
|
|
62
|
+
{ id: 'f57f1ce9', name: 'Preparation', color: 'YELLOW' },
|
|
63
|
+
{ id: 'fd313492', name: 'Failed Preparation', color: 'RED' },
|
|
64
|
+
{ id: 'e9931e57', name: 'Todo by human', color: 'PINK' },
|
|
65
|
+
{ id: 'c2d278b2', name: 'In Tmux by human', color: 'RED' },
|
|
66
|
+
{ id: 'e9f6a726', name: 'In Tmux by agent', color: 'YELLOW' },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const STORY_OPTIONS: ConsoleFixtureFieldOption[] = [
|
|
70
|
+
{ id: '1491051e', name: 'TDPM Console port', color: 'BLUE' },
|
|
71
|
+
{ id: '28415d6c', name: 'regular / workflow improvement', color: 'GRAY' },
|
|
72
|
+
{
|
|
73
|
+
id: 'f7cd5cbc',
|
|
74
|
+
name: 'Publish product documentation site',
|
|
75
|
+
color: 'GREEN',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const STORY_COLORS: Record<string, { color: string }> = {
|
|
80
|
+
'TDPM Console port': { color: 'BLUE' },
|
|
81
|
+
'regular / workflow improvement': { color: 'GRAY' },
|
|
82
|
+
'Publish product documentation site': { color: 'GREEN' },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const issueItem = (
|
|
86
|
+
number: number,
|
|
87
|
+
title: string,
|
|
88
|
+
projectItemSuffix: string,
|
|
89
|
+
story: string,
|
|
90
|
+
createdAt: string,
|
|
91
|
+
): ConsoleFixtureListItem => ({
|
|
92
|
+
number,
|
|
93
|
+
title,
|
|
94
|
+
url: `https://github.com/${REPO_NAME_WITH_OWNER}/issues/${number}`,
|
|
95
|
+
repo: REPO_NAME_WITH_OWNER,
|
|
96
|
+
nameWithOwner: REPO_NAME_WITH_OWNER,
|
|
97
|
+
projectItemId: `PVTI_lADOABCD1234zg${projectItemSuffix}`,
|
|
98
|
+
itemId: `PVTI_lADOABCD1234zg${projectItemSuffix}`,
|
|
99
|
+
isPr: false,
|
|
100
|
+
story,
|
|
101
|
+
labels: [],
|
|
102
|
+
createdAt,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const pullRequestItem = (
|
|
106
|
+
number: number,
|
|
107
|
+
title: string,
|
|
108
|
+
projectItemSuffix: string,
|
|
109
|
+
story: string,
|
|
110
|
+
createdAt: string,
|
|
111
|
+
): ConsoleFixtureListItem => ({
|
|
112
|
+
number,
|
|
113
|
+
title,
|
|
114
|
+
url: `https://github.com/${REPO_NAME_WITH_OWNER}/pull/${number}`,
|
|
115
|
+
repo: REPO_NAME_WITH_OWNER,
|
|
116
|
+
nameWithOwner: REPO_NAME_WITH_OWNER,
|
|
117
|
+
projectItemId: `PVTI_lADOABCD1234zg${projectItemSuffix}`,
|
|
118
|
+
itemId: `PVTI_lADOABCD1234zg${projectItemSuffix}`,
|
|
119
|
+
isPr: true,
|
|
120
|
+
story,
|
|
121
|
+
labels: ['claude'],
|
|
122
|
+
createdAt,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const buildSnapshot = (
|
|
126
|
+
items: ConsoleFixtureListItem[],
|
|
127
|
+
): ConsoleFixtureSnapshot => ({
|
|
128
|
+
pjcode: CONSOLE_E2E_PJCODE,
|
|
129
|
+
generatedAt: '2026-06-18T01:22:09.000Z',
|
|
130
|
+
statusOptions: STATUS_OPTIONS,
|
|
131
|
+
storyOptions: STORY_OPTIONS,
|
|
132
|
+
storyColors: STORY_COLORS,
|
|
133
|
+
items,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export const CONSOLE_E2E_TAB_ITEMS: Record<string, ConsoleFixtureListItem[]> = {
|
|
137
|
+
prs: [
|
|
138
|
+
pullRequestItem(
|
|
139
|
+
867,
|
|
140
|
+
'Serve the committed console UI bundle from serveConsole',
|
|
141
|
+
'PRS00867',
|
|
142
|
+
'TDPM Console port',
|
|
143
|
+
'2026-06-17T23:41:08.000Z',
|
|
144
|
+
),
|
|
145
|
+
],
|
|
146
|
+
triage: [
|
|
147
|
+
issueItem(
|
|
148
|
+
778,
|
|
149
|
+
'Add Sonnet to Opus weekly-limit fallback routing per token',
|
|
150
|
+
'TRI00778',
|
|
151
|
+
'regular / workflow improvement',
|
|
152
|
+
'2026-06-12T23:01:55.000Z',
|
|
153
|
+
),
|
|
154
|
+
issueItem(
|
|
155
|
+
692,
|
|
156
|
+
'Publish the generated documentation site to GitHub Pages',
|
|
157
|
+
'TRI00692',
|
|
158
|
+
'Publish product documentation site',
|
|
159
|
+
'2026-06-10T11:42:00.000Z',
|
|
160
|
+
),
|
|
161
|
+
],
|
|
162
|
+
unread: [
|
|
163
|
+
issueItem(
|
|
164
|
+
845,
|
|
165
|
+
'Scaffold React console UI under entry-points with build bundling',
|
|
166
|
+
'UNR00845',
|
|
167
|
+
'TDPM Console port',
|
|
168
|
+
'2026-06-16T22:01:55.000Z',
|
|
169
|
+
),
|
|
170
|
+
issueItem(
|
|
171
|
+
853,
|
|
172
|
+
'Add server-side console API handlers for read and operation endpoints',
|
|
173
|
+
'UNR00853',
|
|
174
|
+
'TDPM Console port',
|
|
175
|
+
'2026-06-17T05:48:09.000Z',
|
|
176
|
+
),
|
|
177
|
+
],
|
|
178
|
+
'failed-preparation': [],
|
|
179
|
+
'todo-by-human': [
|
|
180
|
+
issueItem(
|
|
181
|
+
869,
|
|
182
|
+
'Auto-advance to the next non-empty console tab when one empties',
|
|
183
|
+
'TODO00869',
|
|
184
|
+
'TDPM Console port',
|
|
185
|
+
'2026-06-18T00:14:51.000Z',
|
|
186
|
+
),
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const writeFixtureData = (consoleDataOutputDir: string): void => {
|
|
191
|
+
for (const [tab, items] of Object.entries(CONSOLE_E2E_TAB_ITEMS)) {
|
|
192
|
+
const tabDir = path.join(consoleDataOutputDir, CONSOLE_E2E_PJCODE, tab);
|
|
193
|
+
fs.mkdirSync(tabDir, { recursive: true });
|
|
194
|
+
fs.writeFileSync(
|
|
195
|
+
path.join(tabDir, 'list.json'),
|
|
196
|
+
JSON.stringify(buildSnapshot(items)),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const buildE2eProject = (): Project => ({
|
|
202
|
+
id: 'PVT_console_e2e',
|
|
203
|
+
url: `https://github.com/orgs/HiromiShikata/projects/1`,
|
|
204
|
+
databaseId: 1,
|
|
205
|
+
name: 'TDPM',
|
|
206
|
+
status: {
|
|
207
|
+
name: 'Status',
|
|
208
|
+
fieldId: 'PVTSSF_status',
|
|
209
|
+
statuses: STATUS_OPTIONS.map((option) => ({
|
|
210
|
+
id: option.id,
|
|
211
|
+
name: option.name,
|
|
212
|
+
color: option.color as Project['status']['statuses'][number]['color'],
|
|
213
|
+
description: '',
|
|
214
|
+
})),
|
|
215
|
+
},
|
|
216
|
+
nextActionDate: { name: 'Next Action Date', fieldId: 'PVTF_nad' },
|
|
217
|
+
nextActionHour: { name: 'Next Action Hour', fieldId: 'PVTF_nah' },
|
|
218
|
+
story: {
|
|
219
|
+
name: 'Story',
|
|
220
|
+
fieldId: 'PVTSSF_story',
|
|
221
|
+
databaseId: 2,
|
|
222
|
+
stories: STORY_OPTIONS.map((option) => ({
|
|
223
|
+
id: option.id,
|
|
224
|
+
name: option.name,
|
|
225
|
+
color: option.color as Project['status']['statuses'][number]['color'],
|
|
226
|
+
description: '',
|
|
227
|
+
})),
|
|
228
|
+
workflowManagementStory: { id: 'wms_1', name: 'regular / workflow' },
|
|
229
|
+
},
|
|
230
|
+
remainingEstimationMinutes: null,
|
|
231
|
+
dependedIssueUrlSeparatedByComma: null,
|
|
232
|
+
completionDate50PercentConfidence: null,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const buildIssueForUrl = (url: string): Issue => ({
|
|
236
|
+
nameWithOwner: REPO_NAME_WITH_OWNER,
|
|
237
|
+
number: 0,
|
|
238
|
+
title: 'Console E2E fixture issue',
|
|
239
|
+
state: 'OPEN',
|
|
240
|
+
status: null,
|
|
241
|
+
story: null,
|
|
242
|
+
nextActionDate: null,
|
|
243
|
+
nextActionHour: null,
|
|
244
|
+
estimationMinutes: null,
|
|
245
|
+
dependedIssueUrls: [],
|
|
246
|
+
completionDate50PercentConfidence: null,
|
|
247
|
+
url,
|
|
248
|
+
assignees: [],
|
|
249
|
+
labels: [],
|
|
250
|
+
org: 'HiromiShikata',
|
|
251
|
+
repo: 'npm-cli-github-issue-tower-defence-management',
|
|
252
|
+
body: 'Console E2E fixture issue body.',
|
|
253
|
+
itemId: '',
|
|
254
|
+
isPr: url.includes('/pull/'),
|
|
255
|
+
isInProgress: false,
|
|
256
|
+
isClosed: false,
|
|
257
|
+
createdAt: new Date('2026-06-18T00:00:00.000Z'),
|
|
258
|
+
author: 'HiromiShikata',
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const notImplemented = (method: string): never => {
|
|
262
|
+
throw new Error(`console E2E stub does not implement ${method}`);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const createStubIssueRepository = (): IssueRepository => ({
|
|
266
|
+
getAllIssues: () => notImplemented('getAllIssues'),
|
|
267
|
+
getIssueByUrl: async (url: string): Promise<Issue | null> =>
|
|
268
|
+
buildIssueForUrl(url),
|
|
269
|
+
createNewIssue: () => notImplemented('createNewIssue'),
|
|
270
|
+
searchIssue: () => notImplemented('searchIssue'),
|
|
271
|
+
updateIssue: () => notImplemented('updateIssue'),
|
|
272
|
+
updateNextActionDate: async (): Promise<void> => undefined,
|
|
273
|
+
updateNextActionHour: () => notImplemented('updateNextActionHour'),
|
|
274
|
+
updateProjectTextField: () => notImplemented('updateProjectTextField'),
|
|
275
|
+
updateStory: async (): Promise<void> => undefined,
|
|
276
|
+
updateStatus: async (): Promise<void> => undefined,
|
|
277
|
+
clearProjectField: () => notImplemented('clearProjectField'),
|
|
278
|
+
createComment: () => notImplemented('createComment'),
|
|
279
|
+
updateLabels: () => notImplemented('updateLabels'),
|
|
280
|
+
removeLabel: () => notImplemented('removeLabel'),
|
|
281
|
+
updateAssigneeList: () => notImplemented('updateAssigneeList'),
|
|
282
|
+
get: async (issueUrl: string): Promise<Issue | null> =>
|
|
283
|
+
buildIssueForUrl(issueUrl),
|
|
284
|
+
update: () => notImplemented('update'),
|
|
285
|
+
findRelatedOpenPRs: async (): Promise<RelatedPullRequest[]> => [],
|
|
286
|
+
getOpenPullRequest: async (): Promise<RelatedPullRequest | null> => null,
|
|
287
|
+
getPullRequestChangedFilePaths: async (): Promise<string[]> => [],
|
|
288
|
+
approvePullRequest: async (): Promise<void> => undefined,
|
|
289
|
+
requestChangesWithInlineComment: async (): Promise<void> => undefined,
|
|
290
|
+
closePullRequest: async (): Promise<void> => undefined,
|
|
291
|
+
closeIssueByUrl: async (): Promise<void> => undefined,
|
|
292
|
+
deletePullRequestBranch: () => notImplemented('deletePullRequestBranch'),
|
|
293
|
+
createCommentByUrl: async (): Promise<void> => undefined,
|
|
294
|
+
getAllOpened: () => notImplemented('getAllOpened'),
|
|
295
|
+
getStoryObjectMap: () => notImplemented('getStoryObjectMap'),
|
|
296
|
+
addIssueToProject: () => notImplemented('addIssueToProject'),
|
|
297
|
+
setDependedIssueUrl: () => notImplemented('setDependedIssueUrl'),
|
|
298
|
+
getIssueOrPullRequestBody: async (): Promise<string> =>
|
|
299
|
+
'## Console E2E fixture\n\nThis body is served by the isolated E2E stub.',
|
|
300
|
+
getIssueOrPullRequestComments: async (): Promise<IssueComment[]> => [],
|
|
301
|
+
getPullRequestDetail: async (): Promise<PullRequestDetail | null> => null,
|
|
302
|
+
getPullRequestCommits: async (): Promise<PullRequestCommit[]> => [],
|
|
303
|
+
getIssueOrPullRequestState: async (
|
|
304
|
+
url: string,
|
|
305
|
+
): Promise<{ state: string; merged: boolean; isPullRequest: boolean }> => ({
|
|
306
|
+
state: 'open',
|
|
307
|
+
merged: false,
|
|
308
|
+
isPullRequest: url.includes('/pull/'),
|
|
309
|
+
}),
|
|
310
|
+
getPullRequestSummary: async (): Promise<{
|
|
311
|
+
title: string;
|
|
312
|
+
body: string;
|
|
313
|
+
additions: number;
|
|
314
|
+
deletions: number;
|
|
315
|
+
changedFiles: number;
|
|
316
|
+
} | null> => null,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
export type ConsoleE2eHarness = {
|
|
320
|
+
baseUrl: string;
|
|
321
|
+
appUrl: string;
|
|
322
|
+
consoleDataOutputDir: string;
|
|
323
|
+
stop: () => Promise<void>;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
|
|
327
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'console-e2e-'));
|
|
328
|
+
const consoleDataOutputDir = path.join(tmpRoot, 'data');
|
|
329
|
+
writeFixtureData(consoleDataOutputDir);
|
|
330
|
+
|
|
331
|
+
const uiDistDir = path.resolve(__dirname, '..', '..', 'ui-dist');
|
|
332
|
+
|
|
333
|
+
const project = buildE2eProject();
|
|
334
|
+
const resolveProject = async (
|
|
335
|
+
pjcode: string,
|
|
336
|
+
): Promise<ConsoleProjectBinding | null> =>
|
|
337
|
+
pjcode === CONSOLE_E2E_PJCODE ? { pjcode, project } : null;
|
|
338
|
+
|
|
339
|
+
const server = await startConsoleServer({
|
|
340
|
+
accessToken: CONSOLE_E2E_TOKEN,
|
|
341
|
+
uiDistDir,
|
|
342
|
+
consoleDataOutputDir,
|
|
343
|
+
issueRepository: createStubIssueRepository(),
|
|
344
|
+
resolveProject,
|
|
345
|
+
issueTitleStateCache: new IssueTitleStateCache(),
|
|
346
|
+
inTmuxDataDir: null,
|
|
347
|
+
dashboardDir: null,
|
|
348
|
+
port: 0,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const address = server.address();
|
|
352
|
+
if (address === null || typeof address === 'string') {
|
|
353
|
+
await closeServer(server);
|
|
354
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
355
|
+
throw new Error('console E2E server is not listening on a TCP port');
|
|
356
|
+
}
|
|
357
|
+
const baseUrl = `http://127.0.0.1:${address.port}`;
|
|
358
|
+
const appUrl = `${baseUrl}/projects/${CONSOLE_E2E_PJCODE}?k=${CONSOLE_E2E_TOKEN}`;
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
baseUrl,
|
|
362
|
+
appUrl,
|
|
363
|
+
consoleDataOutputDir,
|
|
364
|
+
stop: async (): Promise<void> => {
|
|
365
|
+
await closeServer(server);
|
|
366
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const closeServer = (server: http.Server): Promise<void> =>
|
|
372
|
+
new Promise((resolve, reject) => {
|
|
373
|
+
server.close((error) => {
|
|
374
|
+
if (error) {
|
|
375
|
+
reject(error);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
resolve();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
testDir: __dirname,
|
|
6
|
+
testMatch: '**/*.spec.ts',
|
|
7
|
+
fullyParallel: false,
|
|
8
|
+
forbidOnly: !!process.env.CI,
|
|
9
|
+
retries: 0,
|
|
10
|
+
workers: 1,
|
|
11
|
+
reporter: [
|
|
12
|
+
['list'],
|
|
13
|
+
['html', { open: 'never', outputFolder: path.join(__dirname, 'report') }],
|
|
14
|
+
],
|
|
15
|
+
outputDir: path.join(__dirname, 'test-results'),
|
|
16
|
+
timeout: 60_000,
|
|
17
|
+
expect: { timeout: 3_000 },
|
|
18
|
+
use: {
|
|
19
|
+
actionTimeout: 3_000,
|
|
20
|
+
navigationTimeout: 3_000,
|
|
21
|
+
trace: 'on-first-retry',
|
|
22
|
+
screenshot: 'only-on-failure',
|
|
23
|
+
},
|
|
24
|
+
projects: [
|
|
25
|
+
{
|
|
26
|
+
name: 'chromium',
|
|
27
|
+
use: { ...devices['Desktop Chrome'] },
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "CommonJS",
|
|
6
|
+
"moduleResolution": "Node",
|
|
7
|
+
"ignoreDeprecations": "6.0",
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"noUnusedLocals": true,
|
|
14
|
+
"noUnusedParameters": true,
|
|
15
|
+
"types": ["node"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["."]
|
|
18
|
+
}
|
|
@@ -100,7 +100,7 @@ describe('ConsolePage', () => {
|
|
|
100
100
|
it('shows a cancellable toast and only drives the tab to zero after the five second window', async () => {
|
|
101
101
|
jest.useFakeTimers();
|
|
102
102
|
try {
|
|
103
|
-
const { getByText, findByText } = render(<ConsolePage />);
|
|
103
|
+
const { getByText, findByText, queryByText } = render(<ConsolePage />);
|
|
104
104
|
await waitFor(() => {
|
|
105
105
|
expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
|
|
106
106
|
});
|
|
@@ -127,9 +127,7 @@ describe('ConsolePage', () => {
|
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
await waitFor(() => {
|
|
130
|
-
expect(
|
|
131
|
-
getByText('Unread').closest('a')?.getAttribute('aria-current'),
|
|
132
|
-
).toBe('page');
|
|
130
|
+
expect(queryByText('Awaiting Quality Check')).toBeNull();
|
|
133
131
|
});
|
|
134
132
|
} finally {
|
|
135
133
|
jest.useRealTimers();
|
|
@@ -53,7 +53,8 @@ export const ConsolePage = () => {
|
|
|
53
53
|
const pjcode = useConsolePjcode();
|
|
54
54
|
const { snapshots, isLoading, error } = useConsoleTabData(pjcode);
|
|
55
55
|
const navigation = useConsoleNavigation(pjcode);
|
|
56
|
-
const { activeTab, selectedItemKey } =
|
|
56
|
+
const { activeTab, selectedItemKey, openItem, closeItem, selectTab } =
|
|
57
|
+
navigation;
|
|
57
58
|
|
|
58
59
|
const overlayState = useConsoleOverlay(pjcode ?? OVERLAY_NAMESPACE_FALLBACK);
|
|
59
60
|
const caches = useConsoleCaches();
|
|
@@ -124,11 +125,11 @@ export const ConsolePage = () => {
|
|
|
124
125
|
if (previous.count > 0 && activeCount === 0) {
|
|
125
126
|
const nextTab = findNextNonEmptyTabToRight(activeTab, counts);
|
|
126
127
|
if (nextTab !== null) {
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
selectTab(nextTab);
|
|
129
|
+
closeItem();
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
|
-
}, [activeTab, activeCount, counts,
|
|
132
|
+
}, [activeTab, activeCount, counts, selectTab, closeItem]);
|
|
132
133
|
|
|
133
134
|
const overlayStatusForSelected = ((): ConsoleOverlayStatus | null => {
|
|
134
135
|
if (selectedItem === null) {
|
|
@@ -143,8 +144,6 @@ export const ConsolePage = () => {
|
|
|
143
144
|
? resolveItemStory(selectedItem, overlayState.overlay)
|
|
144
145
|
: null;
|
|
145
146
|
|
|
146
|
-
const { openItem, closeItem } = navigation;
|
|
147
|
-
|
|
148
147
|
const advanceToNext = useCallback(
|
|
149
148
|
(actedKey: string): void => {
|
|
150
149
|
const nextKey = nextPendingKeyAfter(orderedPendingKeys, actedKey);
|