github-issue-tower-defence-management 1.95.0 → 1.96.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 (42) hide show
  1. package/.github/workflows/console-ui.yml +17 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +11 -0
  4. package/bin/adapter/entry-points/cli/index.js +21 -0
  5. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  6. package/bin/adapter/entry-points/console/ui-dist/assets/{index-BeJzGnfH.js → index-B3F4E8LD.js} +33 -33
  7. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  8. package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js +77 -0
  9. package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js.map +1 -0
  10. package/bin/adapter/repositories/ProcClaudeLiveSessionRepository.js +139 -0
  11. package/bin/adapter/repositories/ProcClaudeLiveSessionRepository.js.map +1 -0
  12. package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js +59 -0
  13. package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js.map +1 -0
  14. package/bin/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.js +3 -0
  15. package/bin/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.js.map +1 -0
  16. package/package.json +4 -1
  17. package/src/adapter/entry-points/cli/index.ts +38 -0
  18. package/src/adapter/entry-points/console/ui/biome.json +7 -1
  19. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +93 -0
  20. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +380 -0
  21. package/src/adapter/entry-points/console/ui/e2e/playwright.config.ts +30 -0
  22. package/src/adapter/entry-points/console/ui/e2e/tsconfig.json +18 -0
  23. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +2 -4
  24. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +5 -6
  25. package/src/adapter/entry-points/console/ui-dist/assets/{index-BeJzGnfH.js → index-B3F4E8LD.js} +33 -33
  26. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  27. package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.test.ts +268 -0
  28. package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.ts +116 -0
  29. package/src/adapter/repositories/ProcClaudeLiveSessionRepository.test.ts +146 -0
  30. package/src/adapter/repositories/ProcClaudeLiveSessionRepository.ts +119 -0
  31. package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.test.ts +147 -0
  32. package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.ts +100 -0
  33. package/src/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.ts +8 -0
  34. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  35. package/types/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.d.ts +20 -0
  36. package/types/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.d.ts.map +1 -0
  37. package/types/adapter/repositories/ProcClaudeLiveSessionRepository.d.ts +11 -0
  38. package/types/adapter/repositories/ProcClaudeLiveSessionRepository.d.ts.map +1 -0
  39. package/types/domain/usecases/LiveSessionOauthTokenSelectUseCase.d.ts +23 -0
  40. package/types/domain/usecases/LiveSessionOauthTokenSelectUseCase.d.ts.map +1 -0
  41. package/types/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.d.ts +8 -0
  42. package/types/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.d.ts.map +1 -0
@@ -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-BeJzGnfH.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-B3F4E8LD.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-CO3Vvzqr.css">
9
9
  </head>
10
10
  <body>
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LiveSessionOauthTokenSelectHandler = void 0;
4
+ const LiveSessionOauthTokenSelectUseCase_1 = require("../../../domain/usecases/LiveSessionOauthTokenSelectUseCase");
5
+ const ProcClaudeLiveSessionRepository_1 = require("../../repositories/ProcClaudeLiveSessionRepository");
6
+ const RateLimitCache_1 = require("../../proxy/RateLimitCache");
7
+ const TokenListLoader_1 = require("../../proxy/TokenListLoader");
8
+ const OauthTokenSelectHandler_1 = require("./OauthTokenSelectHandler");
9
+ class LiveSessionOauthTokenSelectHandler {
10
+ constructor(useCase = new LiveSessionOauthTokenSelectUseCase_1.LiveSessionOauthTokenSelectUseCase(), liveSessionRepository = new ProcClaudeLiveSessionRepository_1.ProcClaudeLiveSessionRepository()) {
11
+ this.useCase = useCase;
12
+ this.liveSessionRepository = liveSessionRepository;
13
+ this.handle = (input) => {
14
+ const tokenListJsonPath = (0, OauthTokenSelectHandler_1.resolveTokenListJsonPath)(input.tokenListJsonPath);
15
+ if (tokenListJsonPath === null) {
16
+ return {
17
+ selectedToken: null,
18
+ selectedName: null,
19
+ diagnostics: [
20
+ 'No token list path provided. Pass --tokenListJsonPath or set CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH.',
21
+ ],
22
+ };
23
+ }
24
+ const entries = (0, TokenListLoader_1.loadTokenEntries)(tokenListJsonPath);
25
+ if (entries === null) {
26
+ return {
27
+ selectedToken: null,
28
+ selectedName: null,
29
+ diagnostics: [
30
+ `No usable token entries loaded from ${tokenListJsonPath}.`,
31
+ ],
32
+ };
33
+ }
34
+ const cacheDirectory = (0, OauthTokenSelectHandler_1.resolveCacheDirectory)(input.cacheDirectory);
35
+ const candidates = entries.map(({ name, token }) => {
36
+ const snapshot = (0, RateLimitCache_1.readRateLimit)(token, cacheDirectory);
37
+ return {
38
+ name,
39
+ token,
40
+ snapshot: snapshot === null
41
+ ? null
42
+ : {
43
+ fiveHourUtilization: snapshot.fiveHourUtilization,
44
+ fiveHourReset: snapshot.fiveHourReset,
45
+ sevenDayUtilization: snapshot.sevenDayUtilization,
46
+ sevenDayReset: snapshot.sevenDayReset,
47
+ },
48
+ };
49
+ });
50
+ const liveSessions = this.liveSessionRepository.listLiveSessions();
51
+ const result = this.useCase.run(candidates, liveSessions, input.nowEpochSeconds);
52
+ return {
53
+ selectedToken: result.selected?.token ?? null,
54
+ selectedName: result.selected?.name ?? null,
55
+ diagnostics: this.formatDiagnostics(result, input.nowEpochSeconds),
56
+ };
57
+ };
58
+ this.formatDiagnostics = (result, nowEpochSeconds) => {
59
+ const lines = result.metrics.map((metric) => {
60
+ const secondsUntilSevenDayEnd = Math.round(metric.sevenDayEndEpoch - nowEpochSeconds);
61
+ const status = metric.eligible
62
+ ? 'eligible'
63
+ : `excluded (${metric.exclusionReason})`;
64
+ return `${metric.name}: ${metric.liveSessionCount} live session(s), 5h ${Math.round(metric.fiveHourFreeRatio * 100)}% free, 7d ${Math.round(metric.sevenDayFreeRatio * 100)}% free, 7d-end in ${secondsUntilSevenDayEnd}s -> ${status}`;
65
+ });
66
+ if (result.selected === null) {
67
+ lines.push('No eligible token passed the rate-limit filter.');
68
+ }
69
+ else {
70
+ lines.push(`Selected ${result.selected.name} (fewest live sessions, then soonest 7d reset among eligible tokens).`);
71
+ }
72
+ return lines;
73
+ };
74
+ }
75
+ }
76
+ exports.LiveSessionOauthTokenSelectHandler = LiveSessionOauthTokenSelectHandler;
77
+ //# sourceMappingURL=LiveSessionOauthTokenSelectHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LiveSessionOauthTokenSelectHandler.js","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ProcClaudeLiveSessionRepository = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const DEFAULT_PROC_DIRECTORY = '/proc';
40
+ const OAUTH_TOKEN_ENVIRON_KEY = 'CLAUDE_CODE_OAUTH_TOKEN';
41
+ const SESSION_ID_ENVIRON_KEY = 'CLAUDE_CODE_SESSION_ID';
42
+ const isClaudeProcessCommand = (command) => {
43
+ if (command.length === 0) {
44
+ return false;
45
+ }
46
+ if (command.includes('.local/share/claude')) {
47
+ return true;
48
+ }
49
+ const executableName = path.basename(command.split('\0')[0] ?? '');
50
+ return executableName === 'claude';
51
+ };
52
+ class ProcClaudeLiveSessionRepository {
53
+ constructor(procDirectory = DEFAULT_PROC_DIRECTORY) {
54
+ this.procDirectory = procDirectory;
55
+ this.listLiveSessions = () => {
56
+ const liveSessions = [];
57
+ for (const pidDirectory of this.listProcessIdDirectories()) {
58
+ const liveSession = this.readLiveSession(pidDirectory);
59
+ if (liveSession !== null) {
60
+ liveSessions.push(liveSession);
61
+ }
62
+ }
63
+ return liveSessions;
64
+ };
65
+ this.listProcessIdDirectories = () => {
66
+ let entries;
67
+ try {
68
+ entries = fs.readdirSync(this.procDirectory);
69
+ }
70
+ catch {
71
+ return [];
72
+ }
73
+ return entries.filter((entry) => /^\d+$/.test(entry));
74
+ };
75
+ this.readLiveSession = (processIdDirectory) => {
76
+ const environ = this.readEnviron(processIdDirectory);
77
+ if (environ === null) {
78
+ return null;
79
+ }
80
+ const token = environ.get(OAUTH_TOKEN_ENVIRON_KEY);
81
+ const sessionId = environ.get(SESSION_ID_ENVIRON_KEY);
82
+ if (token === undefined ||
83
+ token.length === 0 ||
84
+ sessionId === undefined ||
85
+ sessionId.length === 0) {
86
+ return null;
87
+ }
88
+ if (!this.isClaudeProcess(processIdDirectory)) {
89
+ return null;
90
+ }
91
+ return { token, sessionId };
92
+ };
93
+ this.isClaudeProcess = (processIdDirectory) => {
94
+ const basePath = path.join(this.procDirectory, processIdDirectory);
95
+ try {
96
+ const cmdline = fs.readFileSync(path.join(basePath, 'cmdline'), 'utf8');
97
+ if (isClaudeProcessCommand(cmdline)) {
98
+ return true;
99
+ }
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ try {
105
+ const exe = fs.readlinkSync(path.join(basePath, 'exe'));
106
+ return isClaudeProcessCommand(exe);
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ };
112
+ this.readEnviron = (processIdDirectory) => {
113
+ const environPath = path.join(this.procDirectory, processIdDirectory, 'environ');
114
+ let raw;
115
+ try {
116
+ raw = fs.readFileSync(environPath, 'utf8');
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ const environ = new Map();
122
+ for (const entry of raw.split('\0')) {
123
+ if (entry.length === 0) {
124
+ continue;
125
+ }
126
+ const separatorIndex = entry.indexOf('=');
127
+ if (separatorIndex <= 0) {
128
+ continue;
129
+ }
130
+ const key = entry.slice(0, separatorIndex);
131
+ const value = entry.slice(separatorIndex + 1);
132
+ environ.set(key, value);
133
+ }
134
+ return environ;
135
+ };
136
+ }
137
+ }
138
+ exports.ProcClaudeLiveSessionRepository = ProcClaudeLiveSessionRepository;
139
+ //# sourceMappingURL=ProcClaudeLiveSessionRepository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProcClaudeLiveSessionRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/ProcClaudeLiveSessionRepository.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LiveSessionOauthTokenSelectUseCase = void 0;
4
+ const OauthTokenSelectUseCase_1 = require("./OauthTokenSelectUseCase");
5
+ class LiveSessionOauthTokenSelectUseCase {
6
+ constructor(rateLimitSelectUseCase = new OauthTokenSelectUseCase_1.OauthTokenSelectUseCase()) {
7
+ this.rateLimitSelectUseCase = rateLimitSelectUseCase;
8
+ this.run = (candidates, liveSessions, nowEpochSeconds) => {
9
+ const rateLimitResult = this.rateLimitSelectUseCase.run(candidates, nowEpochSeconds);
10
+ const liveSessionCountByToken = this.liveSessionCountByToken(liveSessions);
11
+ const evaluated = candidates.map((candidate, index) => {
12
+ const rateLimitMetric = rateLimitResult.metrics[index];
13
+ const liveSessionCount = liveSessionCountByToken.get(candidate.token) ?? 0;
14
+ return {
15
+ candidate,
16
+ metric: {
17
+ name: rateLimitMetric.name,
18
+ fiveHourFreeRatio: rateLimitMetric.fiveHourFreeRatio,
19
+ sevenDayFreeRatio: rateLimitMetric.sevenDayFreeRatio,
20
+ sevenDayEndEpoch: rateLimitMetric.sevenDayEndEpoch,
21
+ liveSessionCount,
22
+ eligible: rateLimitMetric.eligible,
23
+ exclusionReason: rateLimitMetric.exclusionReason,
24
+ },
25
+ };
26
+ });
27
+ const metrics = evaluated.map((entry) => entry.metric);
28
+ const eligible = evaluated.filter((entry) => entry.metric.eligible);
29
+ if (eligible.length === 0) {
30
+ return { selected: null, metrics };
31
+ }
32
+ const best = eligible.reduce((bestEntry, currentEntry) => this.preferred(currentEntry.metric, bestEntry.metric)
33
+ ? currentEntry
34
+ : bestEntry);
35
+ return { selected: best.candidate, metrics };
36
+ };
37
+ this.preferred = (candidateMetric, incumbentMetric) => {
38
+ if (candidateMetric.liveSessionCount !== incumbentMetric.liveSessionCount) {
39
+ return (candidateMetric.liveSessionCount < incumbentMetric.liveSessionCount);
40
+ }
41
+ return candidateMetric.sevenDayEndEpoch < incumbentMetric.sevenDayEndEpoch;
42
+ };
43
+ this.liveSessionCountByToken = (liveSessions) => {
44
+ const sessionIdsByToken = new Map();
45
+ for (const liveSession of liveSessions) {
46
+ const sessionIds = sessionIdsByToken.get(liveSession.token) ?? new Set();
47
+ sessionIds.add(liveSession.sessionId);
48
+ sessionIdsByToken.set(liveSession.token, sessionIds);
49
+ }
50
+ const countByToken = new Map();
51
+ for (const [token, sessionIds] of sessionIdsByToken.entries()) {
52
+ countByToken.set(token, sessionIds.size);
53
+ }
54
+ return countByToken;
55
+ };
56
+ }
57
+ }
58
+ exports.LiveSessionOauthTokenSelectUseCase = LiveSessionOauthTokenSelectUseCase;
59
+ //# sourceMappingURL=LiveSessionOauthTokenSelectUseCase.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LiveSessionOauthTokenSelectUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/LiveSessionOauthTokenSelectUseCase.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=ClaudeLiveSessionRepository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClaudeLiveSessionRepository.js","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.95.0",
3
+ "version": "1.96.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -15,6 +15,8 @@
15
15
  "build": "tsc -p ./tsconfig.build.json",
16
16
  "postbuild": "node scripts/copyConsoleUiDist.mjs",
17
17
  "console-ui:lint": "biome check src/adapter/entry-points/console/ui",
18
+ "console-ui:e2e:typecheck": "tsc --noEmit -p src/adapter/entry-points/console/ui/e2e/tsconfig.json",
19
+ "console-ui:e2e": "npm run build:console-ui && playwright test --config src/adapter/entry-points/console/ui/e2e/playwright.config.ts",
18
20
  "console-ui:storybook": "storybook dev -p 6006 -c src/adapter/entry-points/console/ui/.storybook",
19
21
  "console-ui:build-storybook": "storybook build -c src/adapter/entry-points/console/ui/.storybook -o src/adapter/entry-points/console/ui/storybook-static",
20
22
  "test": "jest",
@@ -54,6 +56,7 @@
54
56
  "homepage": "https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management#readme",
55
57
  "devDependencies": {
56
58
  "@biomejs/biome": "^2.2.0",
59
+ "@playwright/test": "1.60.0",
57
60
  "@radix-ui/react-slot": "^1.2.0",
58
61
  "@semantic-release/changelog": "^6.0.3",
59
62
  "@semantic-release/commit-analyzer": "^13.0.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);
@@ -8,7 +8,13 @@
8
8
  },
9
9
  "files": {
10
10
  "ignoreUnknown": false,
11
- "includes": ["src/**", ".storybook/**"]
11
+ "includes": [
12
+ "src/**",
13
+ ".storybook/**",
14
+ "e2e/**",
15
+ "!e2e/report/**",
16
+ "!e2e/test-results/**"
17
+ ]
12
18
  },
13
19
  "formatter": {
14
20
  "enabled": true,
@@ -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
+ });