diff --git a/.github/workflows/bitcode-gate-quality.yml b/.github/workflows/bitcode-gate-quality.yml index 95f1019e9..e38d64bb5 100644 --- a/.github/workflows/bitcode-gate-quality.yml +++ b/.github/workflows/bitcode-gate-quality.yml @@ -70,6 +70,7 @@ jobs: node scripts/check-v29-gate6-settlement-reconciliation-repair.mjs --skip-branch-check node scripts/check-v29-gate7-organization-permission-authority.mjs --skip-branch-check node scripts/check-v29-gate8-demonstration-origin-formalization.mjs --skip-branch-check + node scripts/check-v29-gate9-terminal-ux-browser-proof.mjs --skip-branch-check - name: Validate source casing and imports run: | @@ -116,9 +117,16 @@ jobs: tests/protocolCommercialBoundary.test.ts \ tests/terminalTransactionDetailCards.test.tsx \ tests/terminalTransactionDetailSnapshot.test.ts \ + tests/terminalUxBrowserProof.test.tsx \ tests/pipelineExecutionLogHeader.test.tsx \ --runInBand + - name: Install Terminal browser proof browser + run: pnpm --dir uapi exec playwright install chromium --with-deps + + - name: Browser proof Terminal cockpit + run: pnpm --dir uapi run test:e2e:terminal-ux + - name: Test pipeline readback verifier run: pnpm test:qa:v28:pipeline-readback diff --git a/BITCODE_SPEC_V29.md b/BITCODE_SPEC_V29.md index 8103ff6ff..c78e27b66 100644 --- a/BITCODE_SPEC_V29.md +++ b/BITCODE_SPEC_V29.md @@ -223,6 +223,29 @@ The Terminal transaction read model must contain: The model is deterministic from execution history, detail readback, route query state, and data mode. It must tolerate partial live readback by preserving a fallback selected-run projection while naming empty or blocked sections. +## V29 Terminal UX quality and browser-proof canon + +The Terminal transaction cockpit is a protocol-facing operator interface. +It must be readable by default, keyboard reachable, responsive, and browser-proven. + +The cockpit must expose: + +- one named `main` landmark for the Terminal route; +- a keyboard-reachable skip link to the selected transaction workspace; +- a named transaction workspace region for activity selection, filtering, and selected-result digest; +- a named selected activity detail region for the low-detail detail hero, route-owned section controls, and source-safe detail surfaces; +- explicit loading, empty, failed, blocked, and source-safe preview states with `status` or `alert` semantics where appropriate; +- contained table overflow so phone and tablet viewports do not acquire document-level horizontal overflow; +- route-owned detail controls whose current, available, empty, and blocked states are understandable before raw payload expansion. + +The browser-proof contract is package-local to Terminal and exported by `uapi/app/terminal/terminal-ux-browser-proof.ts`. +It identifies the required landmarks, viewports, state semantics, route checks, and evidence files. +The focused Jest test proves the contract and state semantics. +The focused Playwright spec proves the cockpit in a real browser in deterministic mock mode across key route and responsive states. + +Gate 9 acceptance does not change Reading law, source disclosure law, settlement law, or organization authority law. +It makes the current V29 Terminal cockpit commercially operable enough that later local/staging promotion readiness can debug failures from the UI rather than from browser network logs alone. + ## V29 Wallet/BTC operation canon Wallet and BTC fee state is an ordinary Terminal transaction surface, not an opaque settlement footnote. diff --git a/BITCODE_SPEC_V29_DELTA.md b/BITCODE_SPEC_V29_DELTA.md index fe5199c21..de2d678e8 100644 --- a/BITCODE_SPEC_V29_DELTA.md +++ b/BITCODE_SPEC_V29_DELTA.md @@ -183,6 +183,16 @@ Closure acceptance: Gate 9 owns accessibility, responsive layout, copy/prose clarity, empty/loading/blocked/failed states, Playwright coverage, and visual/browser verification for the complete Terminal transaction cockpit. +Closure acceptance: + +- Terminal exposes one named cockpit `main` landmark, a keyboard-reachable skip link to the selected transaction workspace, named regions for the transaction workspace and selected activity detail, and accessible controls for route-owned detail sections. +- The default operator path remains low-detail: activity summary, selected result digest, section availability, blockers, and source-safe preview metadata are readable before raw payload expansion. +- Loading, empty, failed, blocked, and source-safe preview states are represented by typed UI states with `status` or `alert` semantics where appropriate, not only by browser-network inspection. +- The complete cockpit is responsive across phone, tablet, laptop, and widescreen viewports without document-level horizontal overflow; table overflow remains contained inside the table scroller. +- Browser proof is executable through a focused Playwright spec that opens Terminal in deterministic mock mode, verifies landmarks, skip navigation, blocked Console posture, source-safe detail, route-owned selection, and responsive readability. +- Focused Jest coverage proves the UI state contract and the exported browser-proof contract that CI and documentation name. +- `check:v29-gate9` fails closed unless V29 specification, Terminal README, Terminal UX contract, Jest tests, Playwright spec, package scripts, and gate-quality workflow all name the Gate 9 browser-proof surface. + ### Gate 10: Local And Staging Promotion Readiness Gate 10 owns non-mocked local validation, staging-testnet readback, generated proof artifacts, V29 promotion workflow support, promotion dry-run, and final version branch readiness. diff --git a/BITCODE_SPEC_V29_NOTES.md b/BITCODE_SPEC_V29_NOTES.md index eb25f8062..2b355e509 100644 --- a/BITCODE_SPEC_V29_NOTES.md +++ b/BITCODE_SPEC_V29_NOTES.md @@ -76,6 +76,7 @@ For a simplified reading of V29: 9. **Gate 9: Terminal UX Quality And Browser Proof** - Close accessibility, responsive, copy, loading/empty/error/blocked states, browser checks, and Playwright coverage for the complete Terminal cockpit. + - Acceptance detail: the Terminal page exposes a named cockpit `main`, skip navigation into the selected transaction workspace, named workspace/detail regions, source-safe low-detail defaults, explicit state semantics, and deterministic Playwright coverage for landmarks, blocked Console posture, route-owned selection, and phone/tablet/laptop/widescreen readability. 10. **Gate 10: Local And Staging Promotion Readiness** - Run non-mocked local validation, staging-testnet readback, generated proof artifacts, promotion dry-run, and `version/v29` promotion readiness. @@ -209,6 +210,34 @@ Gate 8 completion condition: - Protocol package tests pass. - The spec family, package README, root scripts, and CI all name and enforce the package-native boundary. +## Gate 9 working notes + +Gate 9 closes the Terminal cockpit quality slice. +It treats UX quality as protocol evidence: operators must be able to read the transaction state, section availability, source-safe preview posture, blocked actions, and activity stream without relying on browser network logs. + +Accepted surfaces: + +- `uapi/app/terminal/terminal-ux-browser-proof.ts` records the browser-proof contract: required landmarks, state semantics, viewports, route checks, and evidence files. +- `TerminalPageClient.tsx` owns the named cockpit `main` landmark and skip link. +- `TerminalTransactionWorkspace.tsx` owns the named transaction workspace region, loading/empty/error semantics, and selected result status. +- `TerminalTransactionDetailSurface.tsx`, `TerminalTransactionDetailHero.tsx`, and `TerminalTransactionDetailActionBar.tsx` own the selected activity detail region, low-detail source-safe header, section controls, blocked Console posture, and action errors. +- `BitcodeTransactionsTable.tsx` and `BitcodeTransactionsDataTable.tsx` keep transaction table overflow contained while exposing loading, empty, error, and selectable-row states to tests and assistive technology. +- `uapi/tests/terminalUxBrowserProof.test.tsx` proves the typed state contract. +- `uapi/tests/e2e/commercial-mvp.terminal-ux.spec.ts` proves the cockpit in a real browser in deterministic mock mode. + +Gate 9 accepted boundaries: + +- Gate 9 does not redesign the five-step Reading product flow; it makes the current transaction cockpit accessible and provable. +- Gate 9 does not reveal protected AssetPack source before settlement. +- Gate 9 does not add versioned routes, gate-named runtime routes, or work-in-progress source identifiers. +- Gate 9 does not require live staging credentials; Gate 10 owns non-mocked local/staging promotion readiness. + +Gate 9 completion condition: + +- `pnpm run check:v29-gate9` passes. +- Focused UAPI Jest coverage and Terminal Playwright browser proof pass locally. +- Gate-quality CI invokes the Gate 9 checker, focused Jest test, and focused Playwright browser proof. + ## Later-version boundaries - V30 remains reserved for Protocol/BTD hardening that is not necessary to close Terminal transaction depth. diff --git a/BITCODE_SPEC_V29_PARITY_MATRIX.md b/BITCODE_SPEC_V29_PARITY_MATRIX.md index 768f58a64..f1a4b9c53 100644 --- a/BITCODE_SPEC_V29_PARITY_MATRIX.md +++ b/BITCODE_SPEC_V29_PARITY_MATRIX.md @@ -56,7 +56,7 @@ No `_legacy/` source is active source truth. | Settlement reconciliation repair | Gate 6 | BTD journal/reconciliation, Supabase readback, sandbox harness settlement evidence, Terminal repair UI, Gate 6 checker | drafted | Ledger, database, and metaphysical state drift is classified, proof-rooted, repair-actioned, and visible. | | Organization permission authority | Gate 7 | `packages/btd/src/authority.ts`, BTD/API/MCP/ChatGPT App tests, sandbox harness authority evidence, Terminal Authority section, Gate 7 checker | drafted | Registry-derived roles, holdings, read-license authority, settlement, confirmation, and interface admission govern actions. | | Commercial formalization | Gate 8 | `packages/protocol/src/index.js`, `packages/protocol/src/canon-posture.js`, root scripts, protocol package tests, import scans, docs, Gate 8 checker | drafted | Demonstration-origin commercial internals are package-native and no direct demonstration-source imports remain in commercial/runtime sources. | -| Terminal UX quality | Gate 9 | Playwright/Jest/a11y/responsive/browser QA | pending | Complete transaction cockpit is usable by default and inspectable in detail. | +| Terminal UX quality | Gate 9 | Terminal UX contract, Playwright/Jest/a11y/responsive/browser QA | drafted | Complete transaction cockpit is usable by default and inspectable in detail. | | Promotion readiness | Gate 10 | promotion workflow, `.bitcode/v29-*`, `BITCODE_SPEC_V29_PROVEN.md` | pending | `version/v29` can promote to `main` only after all V29 checks pass. | ## V29 implementation checklist @@ -73,7 +73,7 @@ No `_legacy/` source is active source truth. | Gate 5 AssetPack disclosure | `pnpm run check:v29-gate5` proves source-safe disclosure review, leakage detection, Terminal preview rendering, and PR title enforcement | drafted | | Gate 7 organization authority | `pnpm run check:v29-gate7` proves shared org/interface authority across BTD, API, MCP, ChatGPT App, harness, and Terminal | drafted | | Gate 8 commercial formalization | `pnpm run check:v29-gate8` proves package-native protocol exports, V28/V29 posture, script import cleanup, docs, tests, and CI wiring | drafted | -| Product implementation gates | Gate 9 closes remaining Terminal transaction UX quality with browser proof | pending | +| Gate 9 Terminal UX browser proof | `pnpm run check:v29-gate9` proves named landmarks, explicit state semantics, responsive browser coverage, focused Jest coverage, and CI wiring | drafted | | Promotion gate | Gate 10 closes generated proof and promotion automation | pending | ## Gate 1 Parity @@ -272,3 +272,31 @@ Gate 8 completion condition: - Focused protocol package typecheck/test and the UAPI commercial protocol boundary test pass. - V29 SPEC, DELTA, NOTES, PARITY, protocol package README, package scripts, and gate-quality workflow name the formalized package-native boundary. - Root scripts use protocol package exports for canonical helpers and do not directly import `protocol-demonstration/src/*`. + +## Gate 9 Parity + +Gate 9 closes the Terminal UX quality and browser-proof slice by making the transaction cockpit readable, keyboard reachable, responsive, and test-proven. + +Accepted surfaces: + +- `uapi/app/terminal/terminal-ux-browser-proof.ts` exports the typed contract for Gate 9 landmarks, viewports, states, route checks, and evidence files. +- `TerminalPageClient.tsx` exposes a named `main` cockpit region and skip link to the selected transaction workspace. +- `TerminalTransactionWorkspace.tsx` exposes a named transaction workspace region, selected activity status, and explicit loading, empty, and error states. +- `TerminalTransactionDetailSurface.tsx`, `TerminalTransactionDetailHero.tsx`, and `TerminalTransactionDetailActionBar.tsx` expose a named selected activity detail region, source-safe low-detail detail hero, route-owned section controls, blocked Console posture, and action error alerts. +- `BitcodeTransactionsTable.tsx` and `BitcodeTransactionsDataTable.tsx` expose table state semantics while containing table overflow inside the table scroller. +- `uapi/tests/terminalUxBrowserProof.test.tsx` proves the contract and UI state semantics. +- `uapi/tests/e2e/commercial-mvp.terminal-ux.spec.ts` proves the cockpit through a real browser in deterministic mock mode. +- `.github/workflows/bitcode-gate-quality.yml` runs the Gate 9 checker, focused Jest test, Playwright browser install, and focused Terminal browser proof. + +Accepted boundaries: + +- Gate 9 does not redesign Reading into a different flow. +- Gate 9 does not reveal protected AssetPack source before settlement. +- Gate 9 does not add versioned API routes or gate/work-in-progress runtime identifiers. +- Gate 9 does not perform non-mocked staging promotion readiness; Gate 10 owns that slice. + +Gate 9 completion condition: + +- `pnpm run check:v29-gate9` passes. +- Focused UAPI Jest coverage and Playwright Terminal browser proof pass. +- V29 SPEC, DELTA, NOTES, PARITY, Terminal README, package scripts, and gate-quality workflow name the browser-proof contract. diff --git a/package.json b/package.json index 8828780e9..3e734144b 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "check:v29-gate6": "node scripts/check-v29-gate6-settlement-reconciliation-repair.mjs", "check:v29-gate7": "node scripts/check-v29-gate7-organization-permission-authority.mjs", "check:v29-gate8": "node scripts/check-v29-gate8-demonstration-origin-formalization.mjs", + "check:v29-gate9": "node scripts/check-v29-gate9-terminal-ux-browser-proof.mjs", "check:spec-quality": "node scripts/run-bitcode-spec-quality.mjs --mode basic", "check:spec-quality:title": "node scripts/run-bitcode-spec-quality.mjs --mode strict-from-title", "check:spec-quality:v24": "node scripts/run-bitcode-spec-quality.mjs --mode strict-version --version V24", diff --git a/scripts/check-v29-gate9-terminal-ux-browser-proof.mjs b/scripts/check-v29-gate9-terminal-ux-browser-proof.mjs new file mode 100644 index 000000000..205947d83 --- /dev/null +++ b/scripts/check-v29-gate9-terminal-ux-browser-proof.mjs @@ -0,0 +1,250 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); + +function read(root, relativePath) { + return readFileSync(path.join(root, relativePath), 'utf8'); +} + +function fileExists(root, relativePath) { + return existsSync(path.join(root, relativePath)); +} + +function git(root, args) { + return execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim(); +} + +function assertCheck(failures, condition, message) { + if (!condition) failures.push(message); +} + +function parseArgs(argv) { + const args = { + skipBranchCheck: false, + repoRoot, + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--skip-branch-check') args.skipBranchCheck = true; + else if (arg === '--repo-root') args.repoRoot = path.resolve(argv[++index]); + else if (arg === '--help' || arg === '-h') args.help = true; + else throw new Error(`Unknown argument ${arg}`); + } + + return args; +} + +function printHelp() { + process.stdout.write( + [ + 'Usage: node scripts/check-v29-gate9-terminal-ux-browser-proof.mjs [--skip-branch-check] [--repo-root ]', + '', + 'Checks V29 Gate 9 Terminal UX quality and browser-proof closure.', + ].join('\n'), + ); + process.stdout.write('\n'); +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + if (args.help) { + printHelp(); + return; + } + + const root = args.repoRoot; + const failures = []; + const pointer = read(root, 'BITCODE_SPEC.txt').trim(); + + assertCheck( + failures, + pointer === 'V28', + `BITCODE_SPEC.txt must remain V28 during V29 Gate 9 work. Observed ${pointer || 'empty'}.`, + ); + + if (!args.skipBranchCheck) { + const branch = git(root, ['branch', '--show-current']); + assertCheck( + failures, + branch === 'version/v29' || /^v29\/gate-9-[a-z0-9][a-z0-9-]*$/u.test(branch), + `V29 Gate 9 work must occur on version/v29 or v29/gate-9-* branches. Observed ${branch || 'detached HEAD'}.`, + ); + } + + const requiredFiles = [ + 'BITCODE_SPEC_V29.md', + 'BITCODE_SPEC_V29_DELTA.md', + 'BITCODE_SPEC_V29_NOTES.md', + 'BITCODE_SPEC_V29_PARITY_MATRIX.md', + 'scripts/check-v29-gate9-terminal-ux-browser-proof.mjs', + 'uapi/app/terminal/terminal-ux-browser-proof.ts', + 'uapi/app/terminal/TerminalPageClient.tsx', + 'uapi/app/terminal/TerminalTransactionWorkspace.tsx', + 'uapi/app/terminal/TerminalTransactionDetailSurface.tsx', + 'uapi/app/terminal/TerminalTransactionDetailHero.tsx', + 'uapi/app/terminal/TerminalTransactionDetailActionBar.tsx', + 'uapi/app/terminal/TerminalTransactionActivitySurface.tsx', + 'uapi/components/base/bitcode/execution/BitcodeTransactionsTable.tsx', + 'uapi/components/base/bitcode/execution/BitcodeTransactionsDataTable.tsx', + 'uapi/tests/terminalUxBrowserProof.test.tsx', + 'uapi/tests/e2e/commercial-mvp.terminal-ux.spec.ts', + 'uapi/playwright.config.ts', + 'uapi/app/terminal/README.md', + 'uapi/package.json', + 'uapi/jest.config.cjs', + '.github/workflows/bitcode-gate-quality.yml', + 'package.json', + ]; + + for (const relativePath of requiredFiles) { + assertCheck(failures, fileExists(root, relativePath), `Missing Gate 9 file: ${relativePath}`); + } + + const spec = read(root, 'BITCODE_SPEC_V29.md'); + const delta = read(root, 'BITCODE_SPEC_V29_DELTA.md'); + const notes = read(root, 'BITCODE_SPEC_V29_NOTES.md'); + const parity = read(root, 'BITCODE_SPEC_V29_PARITY_MATRIX.md'); + const contract = read(root, 'uapi/app/terminal/terminal-ux-browser-proof.ts'); + const pageClient = read(root, 'uapi/app/terminal/TerminalPageClient.tsx'); + const workspace = read(root, 'uapi/app/terminal/TerminalTransactionWorkspace.tsx'); + const detailSurface = read(root, 'uapi/app/terminal/TerminalTransactionDetailSurface.tsx'); + const detailHero = read(root, 'uapi/app/terminal/TerminalTransactionDetailHero.tsx'); + const actionBar = read(root, 'uapi/app/terminal/TerminalTransactionDetailActionBar.tsx'); + const activitySurface = read(root, 'uapi/app/terminal/TerminalTransactionActivitySurface.tsx'); + const table = read(root, 'uapi/components/base/bitcode/execution/BitcodeTransactionsTable.tsx'); + const dataTable = read(root, 'uapi/components/base/bitcode/execution/BitcodeTransactionsDataTable.tsx'); + const jestTest = read(root, 'uapi/tests/terminalUxBrowserProof.test.tsx'); + const e2eTest = read(root, 'uapi/tests/e2e/commercial-mvp.terminal-ux.spec.ts'); + const playwrightConfig = read(root, 'uapi/playwright.config.ts'); + const terminalReadme = read(root, 'uapi/app/terminal/README.md'); + const uapiPackage = read(root, 'uapi/package.json'); + const jestConfig = read(root, 'uapi/jest.config.cjs'); + const packageJson = read(root, 'package.json'); + const gateWorkflow = read(root, '.github/workflows/bitcode-gate-quality.yml'); + + assertCheck(failures, spec.includes('V29 Terminal UX quality and browser-proof canon'), 'V29 SPEC must define the Terminal UX browser-proof canon.'); + assertCheck(failures, delta.includes('Gate 9: Terminal UX Quality And Browser Proof') && delta.includes('focused Playwright spec'), 'V29 DELTA must define Gate 9 closure acceptance.'); + assertCheck(failures, notes.includes('Gate 9 working notes') && notes.includes('terminal-ux-browser-proof.ts'), 'V29 NOTES must carry Gate 9 working notes.'); + assertCheck(failures, parity.includes('## Gate 9 Parity') && parity.includes('Gate 9 completion condition'), 'V29 PARITY must include Gate 9 parity and completion condition.'); + assertCheck(failures, terminalReadme.includes('V29 Terminal UX browser proof checkpoint') && terminalReadme.includes('terminal-ux-browser-proof.ts'), 'Terminal README must document the Gate 9 browser-proof checkpoint.'); + + for (const token of [ + 'TERMINAL_UX_BROWSER_PROOF_CONTRACT', + 'TERMINAL_UX_BROWSER_PROOF_LANDMARKS', + 'TERMINAL_UX_BROWSER_PROOF_VIEWPORTS', + 'TERMINAL_UX_BROWSER_PROOF_STATES', + 'TERMINAL_UX_BROWSER_PROOF_ROUTE_CHECKS', + 'terminalMain', + 'terminalTransactionWorkspace', + 'terminalSelectedActivityDetail', + 'phone', + 'tablet', + 'laptop', + 'widescreen', + ]) { + assertCheck(failures, contract.includes(token), `Terminal UX browser-proof contract is missing ${token}.`); + } + + assertCheck( + failures, + pageClient.includes('id="terminalMain"') && + pageClient.includes('data-testid="terminal-cockpit-root"') && + pageClient.includes('Skip to selected Terminal activity') && + pageClient.includes('aria-labelledby="terminalPageTitle"'), + 'Terminal page must expose named main landmark and skip link.', + ); + assertCheck( + failures, + workspace.includes('data-testid="terminal-transaction-workspace"') && + workspace.includes('aria-labelledby="terminalTransactionWorkspaceTitle"') && + workspace.includes('terminal-workspace-loading-state') && + workspace.includes('terminal-workspace-empty-state') && + workspace.includes('terminal-workspace-error-state') && + workspace.includes('role="alert"') && + workspace.includes('role="status"'), + 'Terminal workspace must expose named region and loading/empty/error semantics.', + ); + assertCheck( + failures, + detailSurface.includes('data-testid="terminal-selected-activity-detail"') && + detailSurface.includes('terminalTransactionDetailTitle') && + detailSurface.includes('terminal-detail-loading-state') && + detailSurface.includes('terminal-detail-empty-state') && + detailSurface.includes('terminal-detail-action-error') && + detailHero.includes('data-testid="terminal-selected-activity-hero"') && + detailHero.includes('titleId') && + actionBar.includes('data-testid="terminal-detail-section-controls"') && + actionBar.includes('aria-pressed') && + actionBar.includes('Selected activity detail sections') && + activitySurface.includes('terminal-activity-stream-surface'), + 'Selected activity detail surfaces must expose source-safe hero, detail controls, blocked/error states, and activity stream proof hooks.', + ); + assertCheck( + failures, + table.includes('data-testid="bitcode-transactions-table-shell"') && + dataTable.includes('data-testid="bitcode-transactions-loading-state"') && + dataTable.includes('data-testid="bitcode-transactions-empty-state"') && + dataTable.includes('data-testid="bitcode-transactions-error-state"') && + dataTable.includes('aria-label="Recent Bitcode transactions"') && + dataTable.includes('overflow-x-auto'), + 'Transaction table must expose state semantics and contained overflow.', + ); + assertCheck( + failures, + jestTest.includes('TERMINAL_UX_BROWSER_PROOF_CONTRACT') && + jestTest.includes('terminal-workspace-loading-state') && + jestTest.includes('bitcode-transactions-loading-state') && + jestTest.includes('Selected activity detail sections') && + jestConfig.includes('terminalUxBrowserProof.test.tsx'), + 'Focused Jest coverage must prove the Gate 9 contract and UI state semantics.', + ); + assertCheck( + failures, + e2eTest.includes('Terminal UX browser proof') && + e2eTest.includes('terminal-cockpit-root') && + e2eTest.includes('Skip to selected Terminal activity') && + e2eTest.includes('terminal-selected-activity-hero') && + e2eTest.includes('documentOverflow') && + e2eTest.includes('phone') && + e2eTest.includes('widescreen'), + 'Focused Playwright coverage must prove landmarks, skip navigation, blocked detail posture, and responsive readability.', + ); + assertCheck( + failures, + playwrightConfig.includes('PLAYWRIGHT_READY_URL') && + playwrightConfig.includes('/terminal') && + playwrightConfig.includes('url: appReadyUrl'), + 'Playwright web server readiness must wait on the Terminal route, not only an open port.', + ); + assertCheck( + failures, + packageJson.includes('"check:v29-gate9"') && + uapiPackage.includes('"test:e2e:terminal-ux"') && + uapiPackage.includes('NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321') && + uapiPackage.includes('NEXT_PUBLIC_SUPABASE_ANON_KEY=mock-anon-key') && + uapiPackage.includes('NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=mock-publishable-key') && + gateWorkflow.includes('check-v29-gate9-terminal-ux-browser-proof.mjs') && + gateWorkflow.includes('terminalUxBrowserProof.test.tsx') && + gateWorkflow.includes('test:e2e:terminal-ux') && + gateWorkflow.includes('playwright install chromium --with-deps'), + 'Package scripts and gate-quality workflow must invoke self-contained Gate 9 checker, focused Jest, and Terminal browser proof.', + ); + + if (failures.length) { + process.stderr.write('V29 Gate 9 check failed:\n'); + for (const failure of failures) process.stderr.write(`- ${failure}\n`); + process.exit(1); + } + + process.stdout.write('V29 Gate 9 Terminal UX quality and browser proof check passed.\n'); +} + +main(); diff --git a/uapi/app/terminal/README.md b/uapi/app/terminal/README.md index 24ee6df08..59fe795af 100644 --- a/uapi/app/terminal/README.md +++ b/uapi/app/terminal/README.md @@ -149,6 +149,23 @@ authority payload inspection. Terminal may show source-safe previews without a paid unlock, but protected-source unlock and delivery remain blocked until the same `organizationAuthority` evidence admits the action. +## V29 Terminal UX browser proof checkpoint + +The Terminal cockpit is now browser-proofed as a transaction operation surface, +not only a collection of panels. The route must expose a named `main` landmark, +a keyboard-reachable skip link into `#terminalTransactionWorkspace`, named +workspace/detail regions, explicit loading/empty/error/blocked state semantics, +and contained table overflow for phone through widescreen viewports. + +The browser-proof contract lives in `terminal-ux-browser-proof.ts`. It names the +required landmarks, state semantics, viewport set, route checks, and evidence +files used by Gate 9. The focused Jest test proves the contract and UI state +semantics; the focused Playwright spec proves the cockpit through a real browser +in deterministic mock mode. Playwright readiness waits on the Terminal route +itself through `PLAYWRIGHT_READY_URL` so an open dev-server port is not mistaken +for a rendered cockpit. Gate 9 does not reveal protected AssetPack source, does +not redesign Reading, and does not add versioned routes. + ## Related shared systems - [../../components/base/bitcode/execution/README.md](../../components/base/bitcode/execution/README.md) diff --git a/uapi/app/terminal/TerminalPageClient.tsx b/uapi/app/terminal/TerminalPageClient.tsx index 15d522c02..5e7a974aa 100644 --- a/uapi/app/terminal/TerminalPageClient.tsx +++ b/uapi/app/terminal/TerminalPageClient.tsx @@ -472,12 +472,26 @@ export default function TerminalPageClient() { ) : null}
-
-
+ + Skip to selected Terminal activity + +
+

Bitcode Terminal

-

+

Deposit, Read, and read recent Bitcode activity

@@ -655,7 +669,7 @@ export default function TerminalPageClient() {

-
+
{FEATURE_FLAGS.TERMINAL_DEBUG_WIDGET ? ( +
No activity stream is available for this selected Bitcode activity yet.
); } return ( -
+

Bitcode activity

-

Execution activity and work updates

+

Execution activity and work updates

Read the selected Bitcode activity's execution stream, state, and iteration updates directly from the Bitcode Terminal. Current convergence keeps execution primitives explicit here while the broader Bitcode activity family also admits diff --git a/uapi/app/terminal/TerminalTransactionDetailActionBar.tsx b/uapi/app/terminal/TerminalTransactionDetailActionBar.tsx index cba0f9d14..945de6a6f 100644 --- a/uapi/app/terminal/TerminalTransactionDetailActionBar.tsx +++ b/uapi/app/terminal/TerminalTransactionDetailActionBar.tsx @@ -1,5 +1,7 @@ 'use client'; +import React from 'react'; + import { DisabledTooltipWrapper } from '@/components/base/bitcode/overlays/disabled-tooltip-wrapper'; import type { TerminalTransactionDetailSection } from './terminal-transaction-query'; @@ -86,13 +88,17 @@ export default function TerminalTransactionDetailActionBar({ ); return ( -

+

{isExchangeSurface ? 'Exchange detail interaction' : 'Activity interaction'}

-

+

{isExchangeSurface ? 'Route-owned Exchange detail focus' : 'Route-owned result focus and closure actions'}

@@ -118,13 +124,19 @@ export default function TerminalTransactionDetailActionBar({ ) : null}

-
+
{visibleActions.map((action) => { const sectionButton = (
) : null} -
+
); } diff --git a/uapi/app/terminal/TerminalTransactionWorkspace.tsx b/uapi/app/terminal/TerminalTransactionWorkspace.tsx index da6f4dccc..6605c9463 100644 --- a/uapi/app/terminal/TerminalTransactionWorkspace.tsx +++ b/uapi/app/terminal/TerminalTransactionWorkspace.tsx @@ -1,5 +1,6 @@ 'use client'; +import React from 'react'; import { useEffect, useMemo, useState } from 'react'; import { @@ -371,16 +372,19 @@ export default function TerminalTransactionWorkspace({ return (

{surfaceKicker}

-

+

{surfaceTitle}

-

+

{surfaceSummary}

@@ -400,19 +404,46 @@ export default function TerminalTransactionWorkspace({
) : null}
+

+ {isLoadingRuns + ? loadingLabel + : runsError + ? `Terminal activity failed: ${runsError}` + : selectedRun + ? `Selected activity ${selectedRun.id} is active in ${getTransactionDataModeLabel(transactionDataMode)}.` + : 'No Terminal activity is selected.'} +

{isLoadingRuns ? ( -
+
{loadingLabel}
) : runsError ? ( -
+
{runsError}
) : !selectedRun ? ( -
+
{TERMINAL_SURFACE_COPY.detail.emptySelection}
) : ( diff --git a/uapi/app/terminal/terminal-ux-browser-proof.ts b/uapi/app/terminal/terminal-ux-browser-proof.ts new file mode 100644 index 000000000..c3065de2f --- /dev/null +++ b/uapi/app/terminal/terminal-ux-browser-proof.ts @@ -0,0 +1,100 @@ +export const TERMINAL_UX_BROWSER_PROOF_LANDMARKS = [ + { + id: 'terminalMain', + label: 'Terminal cockpit main landmark', + selector: '#terminalMain', + semantic: 'main', + }, + { + id: 'terminalTransactionWorkspace', + label: 'Selected transaction workspace region', + selector: '#terminalTransactionWorkspace', + semantic: 'region', + }, + { + id: 'terminalSelectedActivityDetail', + label: 'Selected activity detail region', + selector: '[data-testid="terminal-selected-activity-detail"]', + semantic: 'region', + }, +] as const; + +export const TERMINAL_UX_BROWSER_PROOF_VIEWPORTS = [ + { id: 'phone', width: 390, height: 844 }, + { id: 'tablet', width: 768, height: 1024 }, + { id: 'laptop', width: 1280, height: 900 }, + { id: 'widescreen', width: 1440, height: 900 }, +] as const; + +export const TERMINAL_UX_BROWSER_PROOF_STATES = [ + { + id: 'loading', + testId: 'terminal-workspace-loading-state', + semantic: 'status', + }, + { + id: 'empty', + testId: 'terminal-workspace-empty-state', + semantic: 'status', + }, + { + id: 'failed', + testId: 'terminal-workspace-error-state', + semantic: 'alert', + }, + { + id: 'blocked', + testId: 'terminal-detail-section-controls', + semantic: 'disabled-action', + }, + { + id: 'source-safe-preview', + testId: 'terminal-selected-activity-hero', + semantic: 'source-safe-low-detail', + }, +] as const; + +export const TERMINAL_UX_BROWSER_PROOF_ROUTE_CHECKS = [ + { + id: 'default-terminal', + path: '/terminal', + expectation: 'named cockpit, selected transaction workspace, selected result digest', + }, + { + id: 'activity-detail', + path: '/terminal?transactionId=mock-run-branch-remediation&transactionDetail=activity', + expectation: 'source-safe selected detail and execution stream', + }, + { + id: 'blocked-console', + path: '/terminal?transactionId=mock-run-branch-remediation&transactionDetail=console', + expectation: 'Console detail stays blocked in mock review posture', + }, +] as const; + +export const TERMINAL_UX_BROWSER_PROOF_EVIDENCE_FILES = [ + 'uapi/tests/terminalUxBrowserProof.test.tsx', + 'uapi/tests/e2e/commercial-mvp.terminal-ux.spec.ts', + 'scripts/check-v29-gate9-terminal-ux-browser-proof.mjs', + '.github/workflows/bitcode-gate-quality.yml', +] as const; + +export const TERMINAL_UX_BROWSER_PROOF_CONTRACT = { + gate: 'V29 Gate 9: Terminal UX Quality And Browser Proof', + landmarks: TERMINAL_UX_BROWSER_PROOF_LANDMARKS, + viewports: TERMINAL_UX_BROWSER_PROOF_VIEWPORTS, + states: TERMINAL_UX_BROWSER_PROOF_STATES, + routeChecks: TERMINAL_UX_BROWSER_PROOF_ROUTE_CHECKS, + evidenceFiles: TERMINAL_UX_BROWSER_PROOF_EVIDENCE_FILES, +} as const; + +export function summarizeTerminalUxBrowserProofContract() { + return { + gate: TERMINAL_UX_BROWSER_PROOF_CONTRACT.gate, + landmarkCount: TERMINAL_UX_BROWSER_PROOF_LANDMARKS.length, + viewportCount: TERMINAL_UX_BROWSER_PROOF_VIEWPORTS.length, + stateCount: TERMINAL_UX_BROWSER_PROOF_STATES.length, + routeCheckCount: TERMINAL_UX_BROWSER_PROOF_ROUTE_CHECKS.length, + evidenceFileCount: TERMINAL_UX_BROWSER_PROOF_EVIDENCE_FILES.length, + }; +} diff --git a/uapi/components/base/bitcode/execution/BitcodeTransactionsDataTable.tsx b/uapi/components/base/bitcode/execution/BitcodeTransactionsDataTable.tsx index 9bd8dfcdc..93bef27bf 100644 --- a/uapi/components/base/bitcode/execution/BitcodeTransactionsDataTable.tsx +++ b/uapi/components/base/bitcode/execution/BitcodeTransactionsDataTable.tsx @@ -47,16 +47,26 @@ export default function BitcodeTransactionsDataTable({ error, }: BitcodeTransactionsDataTableProps) { return ( -
+
{isLoading ? ( -
Loading Bitcode transactions…
+
+ Loading Bitcode transactions… +
) : error ? ( -
{error}
+
{error}
) : records.length === 0 ? ( -
No Bitcode transactions match the current filters.
+
No Bitcode transactions match the current filters.
) : (
- +
diff --git a/uapi/components/base/bitcode/execution/BitcodeTransactionsTable.tsx b/uapi/components/base/bitcode/execution/BitcodeTransactionsTable.tsx index 5b97dbead..d6fb3525f 100644 --- a/uapi/components/base/bitcode/execution/BitcodeTransactionsTable.tsx +++ b/uapi/components/base/bitcode/execution/BitcodeTransactionsTable.tsx @@ -66,11 +66,15 @@ export default function BitcodeTransactionsTable({ : 'Terminal uses this shared activity table as a focused result surface for recent Deposit, Read, proof, and closure work. Select a row to read its AssetPack evidence, proof posture, history, and execution updates.'; return ( -
+

{tableKicker}

-

{tableTitle}

+

{tableTitle}

{tableSummary}

diff --git a/uapi/jest.config.cjs b/uapi/jest.config.cjs index 8d2524531..e1a7f09f9 100644 --- a/uapi/jest.config.cjs +++ b/uapi/jest.config.cjs @@ -149,6 +149,7 @@ module.exports = { '/tests/terminalTransactionDetailSnapshot.test.ts', '/tests/terminalTransactionQuery.test.ts', '/tests/terminalTransactionReadModel.test.ts', + '/tests/terminalUxBrowserProof.test.tsx', '/tests/terminalWalletBtcOperation.test.ts', '/tests/terminalCoreNativeSections.test.tsx', '/tests/terminalDepositComposer.test.ts', diff --git a/uapi/package.json b/uapi/package.json index d7d47e002..429e837b6 100644 --- a/uapi/package.json +++ b/uapi/package.json @@ -23,6 +23,7 @@ "test:e2e:api": "playwright test tests/e2e/api.e2e.test.ts", "test:e2e:ui": "playwright test", "test:e2e:commercial-mvp": "START_STORYBOOK=false NEXT_PUBLIC_MASTER_MOCK_MODE=true NEXT_PUBLIC_ENABLE_MOCKS=true NEXT_PUBLIC_MOCK_USER_AUXILLARIES=true NEXT_PUBLIC_MOCK_USER_AUXILLARIES_SCENARIO=demo NEXT_PUBLIC_MOCK_SCENARIO=demo NEXT_PUBLIC_MOCK_GITHUB_ACCOUNTS=true NEXT_PUBLIC_MOCK_GITHUB_REPOS=true NEXT_PUBLIC_MOCK_GITHUB_BRANCHES=true NEXT_PUBLIC_MOCK_GITHUB_COMMITS=true NEXT_PUBLIC_MOCK_CHAT_STREAM=true NEXT_PUBLIC_MOCK_CHAT_SCENARIO=demo playwright test tests/e2e/commercial-mvp*.spec.ts --project=laptop --workers=1", + "test:e2e:terminal-ux": "START_STORYBOOK=false NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 NEXT_PUBLIC_SUPABASE_ANON_KEY=mock-anon-key NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=mock-publishable-key NEXT_PUBLIC_MASTER_MOCK_MODE=true NEXT_PUBLIC_ENABLE_MOCKS=true NEXT_PUBLIC_MOCK_USER_AUXILLARIES=true NEXT_PUBLIC_MOCK_USER_AUXILLARIES_SCENARIO=demo NEXT_PUBLIC_MOCK_SCENARIO=demo NEXT_PUBLIC_MOCK_GITHUB_ACCOUNTS=true NEXT_PUBLIC_MOCK_GITHUB_REPOS=true NEXT_PUBLIC_MOCK_GITHUB_BRANCHES=true NEXT_PUBLIC_MOCK_GITHUB_COMMITS=true NEXT_PUBLIC_MOCK_CHAT_STREAM=true NEXT_PUBLIC_MOCK_CHAT_SCENARIO=demo playwright test tests/e2e/commercial-mvp.terminal-ux.spec.ts --project=laptop --workers=1", "test:e2e:visual": "playwright test tests/e2e/**/*.visual.spec.ts", "test:e2e:onboarding": "START_STORYBOOK=false USE_API_MOCK=true playwright test tests/e2e/onboarding.visual.spec.ts tests/e2e/account.onboarding.visual.spec.ts --timeout=120000", "test:e2e:onboarding-first-step": "START_STORYBOOK=false USE_API_MOCK=true playwright test tests/e2e/onboarding-first-step.spec.ts --timeout=120000", diff --git a/uapi/playwright.config.ts b/uapi/playwright.config.ts index 57fbb7233..df07a998f 100644 --- a/uapi/playwright.config.ts +++ b/uapi/playwright.config.ts @@ -6,6 +6,7 @@ import { defineConfig, devices } from '@playwright/test'; // it here would accidentally launch / hit the admin interface instead of // the main product and make the tests fail with `ERR_CONNECTION_REFUSED`. const devPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; +const appReadyUrl = process.env.PLAYWRIGHT_READY_URL || `http://127.0.0.1:${devPort}/terminal`; // Port for Storybook server during Playwright tests; override via STORYBOOK_PORT env var or default to 6006 const storybookPort = process.env.STORYBOOK_PORT ? parseInt(process.env.STORYBOOK_PORT, 10) : 6006; @@ -24,7 +25,7 @@ export default defineConfig({ // triggers unrelated prebuilds and local-service startup before the // product route under test can even load. command: `pnpm dev:remote -- -p ${devPort}`, - port: devPort, + url: appReadyUrl, reuseExistingServer: true, }, // Optionally start Storybook for component visual tests diff --git a/uapi/tests/e2e/commercial-mvp.terminal-ux.spec.ts b/uapi/tests/e2e/commercial-mvp.terminal-ux.spec.ts new file mode 100644 index 000000000..838441935 --- /dev/null +++ b/uapi/tests/e2e/commercial-mvp.terminal-ux.spec.ts @@ -0,0 +1,104 @@ +import { expect, test } from '@playwright/test'; + +import { + expectNoFrameworkOverlay, + expectReadableViewport, + installCommercialBrowserErrorTrap, + installCommercialMvpApiMocks, + openCommercialRoute, +} from './commercial-mvp.helpers'; + +const TERMINAL_BROWSER_PROOF_VIEWPORTS = [ + { id: 'phone', width: 390, height: 844 }, + { id: 'tablet', width: 768, height: 1024 }, + { id: 'laptop', width: 1280, height: 900 }, + { id: 'widescreen', width: 1440, height: 900 }, +] as const; + +test.describe('commercial MVP Terminal UX browser proof', () => { + test.setTimeout(120_000); + + test.beforeEach(async ({ page }) => { + await installCommercialMvpApiMocks(page); + }); + + test('Terminal cockpit exposes landmarks, skip navigation, and selected activity detail', async ({ + page, + }, testInfo) => { + const trap = installCommercialBrowserErrorTrap(page, testInfo); + + await page.goto('/terminal'); + await expect(page.getByTestId('terminal-cockpit-root')).toBeVisible({ timeout: 90_000 }); + await expect(page.getByText(/The Bitcode Terminal is where operators prepare Deposit and Read work/i).first()).toBeVisible(); + await expectNoFrameworkOverlay(page); + await expectReadableViewport(page); + + await expect(page.getByRole('main', { name: /Deposit, Read, and read recent Bitcode activity/i })).toBeVisible(); + await expect(page.getByTestId('terminal-cockpit-root')).toBeVisible(); + await expect(page.getByRole('region', { name: /Recent Terminal activity and selected result/i })).toBeVisible(); + await expect(page.getByTestId('terminal-selected-activity-detail')).toBeVisible(); + await expect(page.getByRole('group', { name: /Selected activity detail sections/i })).toBeVisible(); + + const skipLink = page.getByRole('link', { name: /Skip to selected Terminal activity/i }); + await skipLink.evaluate((element) => (element as HTMLElement).focus()); + await expect(skipLink).toBeFocused(); + await page.keyboard.press('Enter'); + await expect(page.locator('#terminalTransactionWorkspace')).toBeInViewport(); + + await page.goto('/terminal?transactionId=mock-run-branch-remediation&transactionDetail=activity'); + await expect(page.getByTestId('terminal-activity-stream-surface')).toBeVisible({ timeout: 20_000 }); + + await trap.assertClean(); + }); + + test('Terminal blocked detail posture is readable without raw network inspection', async ({ + page, + }, testInfo) => { + const trap = installCommercialBrowserErrorTrap(page, testInfo); + + await openCommercialRoute( + page, + '/terminal?transactionId=mock-run-branch-remediation&transactionDetail=console', + /Prepared the active branch artifact pack/i, + ); + + await expect(page.getByTestId('terminal-selected-activity-hero')).toContainText(/Console \/ blocked/i); + await expect(page.getByRole('button', { name: /^Console$/ })).toBeDisabled(); + await expect(page.getByText(/Console detail is available only for live execution-history rows/i)).toBeVisible(); + + await trap.assertClean(); + }); + + test('Terminal cockpit remains readable from phone through widescreen without document overflow', async ({ + page, + }, testInfo) => { + const trap = installCommercialBrowserErrorTrap(page, testInfo); + + for (const viewport of TERMINAL_BROWSER_PROOF_VIEWPORTS) { + await page.setViewportSize({ width: viewport.width, height: viewport.height }); + await openCommercialRoute( + page, + '/terminal', + /The Bitcode Terminal is where operators prepare Deposit and Read work/i, + ); + await expect(page.getByTestId('terminal-cockpit-root')).toBeVisible(); + await expect(page.locator('#terminalTransactionWorkspace')).toBeVisible(); + + const metrics = await page.evaluate(() => { + const workspace = document.querySelector('#terminalTransactionWorkspace'); + const workspaceRect = workspace?.getBoundingClientRect(); + return { + documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth, + workspaceWidth: Math.round(workspaceRect?.width || 0), + viewportWidth: window.innerWidth, + }; + }); + + expect(metrics.workspaceWidth).toBeGreaterThan(0); + expect(metrics.workspaceWidth).toBeLessThanOrEqual(metrics.viewportWidth); + expect(metrics.documentOverflow, `${viewport.id} document overflow`).toBeLessThanOrEqual(48); + } + + await trap.assertClean(); + }); +}); diff --git a/uapi/tests/terminalUxBrowserProof.test.tsx b/uapi/tests/terminalUxBrowserProof.test.tsx new file mode 100644 index 000000000..c9ded7c3d --- /dev/null +++ b/uapi/tests/terminalUxBrowserProof.test.tsx @@ -0,0 +1,234 @@ +import React from 'react'; +import { fireEvent, render, screen, within } from '@testing-library/react'; + +jest.mock('@/app/terminal/TerminalTransactionDetailSurface', () => ({ + __esModule: true, + default: () => null, +})); + +import TerminalTransactionDetailActionBar from '@/app/terminal/TerminalTransactionDetailActionBar'; +import TerminalTransactionWorkspace from '@/app/terminal/TerminalTransactionWorkspace'; +import { + summarizeTerminalUxBrowserProofContract, + TERMINAL_UX_BROWSER_PROOF_CONTRACT, +} from '@/app/terminal/terminal-ux-browser-proof'; +import { buildTerminalTransactionFilters } from '@/app/terminal/terminal-transactions'; +import BitcodeTransactionsDataTable from '@/components/base/bitcode/execution/BitcodeTransactionsDataTable'; +import { DEFAULT_TRANSACTION_PAGINATION } from '@/components/base/bitcode/execution/bitcode-transaction-types'; +import type { TransactionRecord } from '@/components/base/bitcode/execution/bitcode-transaction-types'; + +const noop = jest.fn(); + +function renderWorkspaceState({ + isLoadingRuns = false, + runsError = null, +}: { + isLoadingRuns?: boolean; + runsError?: string | null; +}) { + return render( + , + ); +} + +const transactionRecord: TransactionRecord = { + id: 'tx-browser-proof-1', + summary: 'Browser proof transaction row.', + type: 'agentic-execution:read-measurement', + typeLabel: 'Read measurement execution', + status: 'completed', + participant: 'engineeredsoftware', + repository: 'engineeredsoftware/ENGI', + branch: 'main', + proofStatus: 'proof witness ready', + closureFocus: 'source-safe preview', + createdAt: '2026-05-21T12:00:00.000Z', + tokenTotal: 1200, + isOwnTransaction: true, + transactionLens: 'read', +}; + +describe('Terminal UX browser proof contract', () => { + it('exports the Gate 9 landmarks, states, viewports, routes, and evidence files', () => { + expect(TERMINAL_UX_BROWSER_PROOF_CONTRACT.gate).toBe( + 'V29 Gate 9: Terminal UX Quality And Browser Proof', + ); + expect(summarizeTerminalUxBrowserProofContract()).toEqual({ + gate: 'V29 Gate 9: Terminal UX Quality And Browser Proof', + landmarkCount: 3, + viewportCount: 4, + stateCount: 5, + routeCheckCount: 3, + evidenceFileCount: 4, + }); + expect(TERMINAL_UX_BROWSER_PROOF_CONTRACT.landmarks.map((landmark) => landmark.id)).toEqual( + expect.arrayContaining([ + 'terminalMain', + 'terminalTransactionWorkspace', + 'terminalSelectedActivityDetail', + ]), + ); + expect(TERMINAL_UX_BROWSER_PROOF_CONTRACT.states.map((state) => state.id)).toEqual( + expect.arrayContaining(['loading', 'empty', 'failed', 'blocked', 'source-safe-preview']), + ); + expect(TERMINAL_UX_BROWSER_PROOF_CONTRACT.viewports.map((viewport) => viewport.id)).toEqual([ + 'phone', + 'tablet', + 'laptop', + 'widescreen', + ]); + expect(TERMINAL_UX_BROWSER_PROOF_CONTRACT.evidenceFiles).toContain( + 'uapi/tests/e2e/commercial-mvp.terminal-ux.spec.ts', + ); + }); + + it('renders Terminal workspace loading, failed, and empty states with explicit semantics', () => { + const { rerender } = renderWorkspaceState({ isLoadingRuns: true }); + const loadingState = screen.getByTestId('terminal-workspace-loading-state'); + expect(loadingState.getAttribute('role')).toBe('status'); + expect(loadingState.textContent).toContain('Loading Bitcode Terminal'); + + rerender( + , + ); + const errorState = screen.getByTestId('terminal-workspace-error-state'); + expect(errorState.getAttribute('role')).toBe('alert'); + expect(errorState.textContent).toContain('Pipeline history read failed.'); + + rerender( + , + ); + const emptyState = screen.getByTestId('terminal-workspace-empty-state'); + expect(emptyState.getAttribute('role')).toBe('status'); + expect(emptyState.textContent).toContain('Select recent Terminal activity'); + }); + + it('keeps transaction-table state semantics and selectable row access stable', () => { + const onSelectTransaction = jest.fn(); + const { rerender } = render( + , + ); + + expect(screen.getByTestId('bitcode-transactions-loading-state').getAttribute('role')).toBe('status'); + + rerender( + , + ); + expect(screen.getByTestId('bitcode-transactions-error-state').getAttribute('role')).toBe('alert'); + + rerender( + , + ); + expect(screen.getByTestId('bitcode-transactions-empty-state').getAttribute('role')).toBe('status'); + + rerender( + , + ); + expect(screen.getByRole('table', { name: /Recent Bitcode transactions/i })).toBeTruthy(); + fireEvent.click(screen.getByRole('button', { name: /tx-browser-proof-1/i })); + expect(onSelectTransaction).toHaveBeenCalledWith('tx-browser-proof-1'); + }); + + it('marks selected detail sections as route-owned controls with blocked actions disabled', () => { + const onChangeSection = jest.fn(); + render( + , + ); + + const controlGroup = screen.getByRole('group', { name: /Selected activity detail sections/i }); + const activityButton = within(controlGroup).getByRole('button', { name: 'Execution stream' }); + const consoleButton = within(controlGroup).getByRole('button', { name: 'Console' }) as HTMLButtonElement; + + expect(activityButton.getAttribute('aria-pressed')).toBe('true'); + expect(consoleButton.disabled).toBe(true); + fireEvent.click(activityButton); + expect(onChangeSection).toHaveBeenCalledWith('activity'); + }); +});