diff --git a/.agent/context/20260909T111228Z-gate-p5-frontend-acceptance.md b/.agent/context/20260909T111228Z-gate-p5-frontend-acceptance.md new file mode 100644 index 0000000..f332d4a --- /dev/null +++ b/.agent/context/20260909T111228Z-gate-p5-frontend-acceptance.md @@ -0,0 +1,61 @@ +# Session Context: Gate P5 Frontend Acceptance + +## Date/time + +- UTC: 2026-09-09T11:12:28Z + +## User goal + +Implement project Gate P5 according to `plan.md`, after ensuring the Recovery +Agent decision exists before frontend Graph MCP composition. + +## Key decisions + +- Compose A05/B05/C05 in the A-owned shell using the configured frozen API. +- Return the latest persisted Recovery Agent and deterministic-core result from + the recovery view; never hard-code an Agent decision. +- Keep Subgraph MCP and model output advisory with settlement permission + `NEVER`. +- Hide unsupported operator escalation in production instead of simulating a + successful external effect. +- Scope B05/C05 CSS at package boundaries to prevent cross-slice overrides. + +## Files/components touched + +- `apps/web`: composed shell, recovery API projection, Playwright acceptance, + responsive/token safety checks. +- `packages/contracts`: additive recovery decision and Graph observation view. +- `packages/reconciliation`: persist Agent boundary acceptance and explanation. +- `packages/storage-postgres`: project latest durable recovery command pack. +- `packages/recovery-ui`, `packages/settlement-ui`: scoped composition styles; + recovery escalation capability flag. +- CI, plan/status documentation, and Gate P5 evidence. + +## Commands/checks + +- Full workspace build/test: 58 files and 904 tests PASS. +- Lint, typecheck, generated contracts, fixtures: PASS. +- Web unit/component: 31 PASS. +- Reconciliation: 84 PASS. +- Storage unit: 8 PASS. +- Playwright Chromium: 4 PASS. +- Cloud Run `/health/ready`: HTTP 200, `{"status":"ok"}`. +- Docker is unavailable locally; PostgreSQL integration test is added for CI. + +## Git and PR state + +- Branch: `feat/gate-p5-frontend-acceptance` +- Base: `origin/develop` at `779c6cf` +- Commit: uncommitted +- PR: not created +- CI: not run + +## Review gates + +- Gate A: NOT RUN +- Gate B: NOT RUN + +## Handoff/next steps + +1. Run full validation and FreePi Gate A. +2. Push a draft PR, await exact-head CI, run Gate B, and hand to a human. diff --git a/.github/workflows/stack-lint.yml b/.github/workflows/stack-lint.yml index 7f22bac..da04536 100644 --- a/.github/workflows/stack-lint.yml +++ b/.github/workflows/stack-lint.yml @@ -107,6 +107,14 @@ jobs: if: steps.workspace.outputs.enabled == 'true' run: pnpm test + - name: Install Chromium + if: steps.workspace.outputs.enabled == 'true' + run: pnpm --filter @oneshot/web exec playwright install --with-deps chromium + + - name: Run Gate P5 browser acceptance + if: steps.workspace.outputs.enabled == 'true' + run: pnpm --filter @oneshot/web test:browser + - name: Run PostgreSQL integration tests if: steps.workspace.outputs.enabled == 'true' env: diff --git a/.gitignore b/.gitignore index bfb07c1..70f3c70 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,8 @@ dist/ site-dist/ build/ coverage/ +playwright-report/ +test-results/ node_modules/ .venv/ venv/ diff --git a/README.md b/README.md index 126bd61..b3776fa 100644 --- a/README.md +++ b/README.md @@ -202,9 +202,9 @@ Under active development. **Testnet only.** | --------------------------------------------- | ----------------------------------------------------------------------------------- | | Durable intent ledger, API, worker | Implemented | | Settlement adapters and error taxonomy | Implemented; simulator-tested and live-verified on Arc Testnet through Privy | -| Recovery evidence and safety core | Implemented against simulators | -| Subgraph MCP discovery and LLM recovery agent | Implemented boundary; live path not verified | -| Operator frontend | Intent/status UI and synthetic recovery viewer implemented; live API wiring pending | +| Recovery evidence and safety core | Live Graph/Vertex path implemented; deterministic core remains authoritative | +| Subgraph MCP discovery and LLM recovery agent | Live Subgraph MCP and Vertex AI path verified; deterministic core remains final | +| Operator frontend | Gate P5 candidate composes A05/B05/C05 against the frozen API with browser coverage | **One live testnet settlement has been executed.** A Privy-controlled execution wallet and scoped policy authorized one 1.00 USDC Arc Testnet transfer; live @@ -213,7 +213,7 @@ drill entered `UNKNOWN` and reconciled to that original settlement without a replacement payment. Privy and Arc are `QUALIFIED` for the documented testnet claim; see `docs/settlement/LIVE_EVIDENCE.md` and `packages/reconciliation/docs/c06/QUALIFICATION_REPORT.md`. The Graph live -Subgraph MCP and recovery-agent path remains `NOT VERIFIED`. +Subgraph MCP and recovery-agent path is also `QUALIFIED` by the latter report. Arc Mainnet is not configured. Its profile carries no chain ID, RPC, explorer, or token value by design, and enabling it requires published official values diff --git a/apps/web/README.md b/apps/web/README.md index 68a3ceb..c9ea0fc 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -1,6 +1,7 @@ # OneShot web -Minimal operator UI for creating or replaying a Business Intent and reading its authoritative status. +Gate P5 operator UI composing intent creation/status, Privy and Arc settlement +details, and Recovery Agent/Subgraph MCP evidence. The Cloudflare asset deployment serves this app at the domain root and the recovery fixture viewer from `@oneshot/recovery-ui` at `/recovery/`. Deploy it @@ -22,4 +23,13 @@ pnpm build:frontend This emits the main app to `apps/web/dist` and the recovery viewer to `apps/web/dist/recovery`, matching the Wrangler asset directory. -The client consumes generated `@oneshot/contracts` types from frozen OpenAPI v1. Tests use deterministic fetch responses matching that contract. +The clients consume generated `@oneshot/contracts` types from frozen OpenAPI v1. +The service token remains in React memory and is never written to browser +storage. Run the Chromium acceptance suite with: + +```powershell +pnpm --filter @oneshot/web test:browser +``` + +See [`../../docs/GATE_P5_CHECKLIST.md`](../../docs/GATE_P5_CHECKLIST.md) for the +covered states and safety boundary. diff --git a/apps/web/index.html b/apps/web/index.html index d855379..5fa209f 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -3,7 +3,7 @@ - OneShot — Intent & Authoritative Status + OneShot — Operator Control
diff --git a/apps/web/package.json b/apps/web/package.json index f8d4ab1..68b919e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -7,17 +7,21 @@ "build": "tsc -b && vite build", "clean": "tsc -b --clean", "dev": "vite", - "lint": "eslint src test", + "lint": "eslint src test playwright.config.ts", "preview": "vite preview", "test": "vitest run --config vitest.config.ts", + "test:browser": "playwright test", "typecheck": "tsc -b --pretty false" }, "dependencies": { "@oneshot/contracts": "workspace:*", + "@oneshot/recovery-ui": "workspace:*", + "@oneshot/settlement-ui": "workspace:*", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { + "@playwright/test": "1.63.0", "@testing-library/react": "16.3.3", "@testing-library/user-event": "14.6.7", "@types/node": "24.13.3", diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts new file mode 100644 index 0000000..f00cd22 --- /dev/null +++ b/apps/web/playwright.config.ts @@ -0,0 +1,18 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './test', + testMatch: '**/*.spec.ts', + fullyParallel: true, + reporter: 'line', + use: { + baseURL: 'http://127.0.0.1:4173', + trace: 'retain-on-failure', + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + webServer: { + command: 'pnpm dev --host 127.0.0.1 --port 4173', + url: 'http://127.0.0.1:4173', + reuseExistingServer: false, + }, +}); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index ce425ab..643bde9 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,30 +1,68 @@ import { useMemo, useState } from 'react'; +import { + createSettlementClient, + SettlementDetailsRoute, + type SettlementClient, +} from '@oneshot/settlement-ui'; +import { RecoveryRoute, type RecoveryClient } from '@oneshot/recovery-ui'; +import '@oneshot/recovery-ui/styles.css'; +import '@oneshot/settlement-ui/styles.css'; import { OneShotApiClient } from './api/client.js'; +import { createApiRecoveryClient } from './api/recovery-client.js'; import { ErrorBoundary } from './components/ErrorBoundary.js'; import { IntentForm } from './components/IntentForm.js'; import { IntentStatusView } from './components/IntentStatusView.js'; import { ReadinessBanner } from './components/ReadinessBanner.js'; import './styles.css'; -type Tab = 'create' | 'status'; +type Tab = 'create' | 'status' | 'settlement' | 'recovery'; -export function App() { +export interface AppProps { + readonly apiClient?: OneShotApiClient; + readonly settlementClient?: SettlementClient; + readonly recoveryClient?: RecoveryClient; +} + +const TABS: readonly { readonly id: Tab; readonly label: string }[] = [ + { id: 'create', label: 'Create or replay' }, + { id: 'status', label: 'Authoritative status' }, + { id: 'settlement', label: 'Settlement details' }, + { id: 'recovery', label: 'Recovery evidence' }, +]; + +export function App(props: AppProps = {}) { const [activeTab, setActiveTab] = useState('create'); const [selectedIntentId, setSelectedIntentId] = useState(''); const [authToken, setAuthToken] = useState(''); - const client = useMemo( + const apiBaseUrl = import.meta.env.VITE_ONESHOT_API_BASE_URL ?? ''; + const apiClient = useMemo( + () => + props.apiClient ?? + new OneShotApiClient({ baseUrl: apiBaseUrl, getAuthToken: () => authToken.trim() || null }), + [apiBaseUrl, authToken, props.apiClient], + ); + const settlementClient = useMemo( + () => + props.settlementClient ?? + createSettlementClient({ + baseUrl: apiBaseUrl, + getAuthToken: () => authToken.trim() || null, + }), + [apiBaseUrl, authToken, props.settlementClient], + ); + const recoveryClient = useMemo( () => - new OneShotApiClient({ - baseUrl: import.meta.env.VITE_ONESHOT_API_BASE_URL ?? '', + props.recoveryClient ?? + createApiRecoveryClient({ + baseUrl: apiBaseUrl, getAuthToken: () => authToken.trim() || null, }), - [authToken], + [apiBaseUrl, authToken, props.recoveryClient], ); - function showStatus(intentId: string): void { + function selectIntent(intentId: string): void { setSelectedIntentId(intentId); - setActiveTab('status'); } return ( @@ -34,7 +72,7 @@ export function App() {

ONESHOT / ARC TESTNET

One job. Many retries. One settlement.

Create a stable payment intent and follow its authoritative state.

- +
@@ -49,34 +87,52 @@ export function App() { Memory only. Sent as Bearer authorization.
-