From f72a18b4dce86fa84605d774148988bfc4882caa Mon Sep 17 00:00:00 2001 From: alastairong1 Date: Sat, 30 May 2026 11:57:29 +0100 Subject: [PATCH 01/22] Phase 01 + 02: E2E coverage + observability for transacting users (#174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add wtQQQM, wtVWO, wtARKK tokens (#168) * Add wtQQQM, wtVWO, wtARKK tokens * fix(csp): allow EU Sentry ingest hosts in connect-src The Sentry project DSN points at o4511338624450560.ingest.de.sentry.io (EU region). The existing connect-src entries cover *.ingest.sentry.io and *.ingest.us.sentry.io but CSP wildcards do not cross dot boundaries — *.ingest.sentry.io does NOT match *.ingest.de.sentry.io. Without this entry the browser blocks all Sentry events with a CSP violation and the SDK silently drops them. Cherry-picks the equivalent fix already merged to main (#170) onto this branch since it predates that merge. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) * update tokens imgs * ishares, invesco, vanguard * docs(01): research and validation strategy * docs(01): pattern map * docs(01): create phase plan (9 plans, 4 waves) Phase 1 (v1.1 Test & Observe): UI-Driven E2E + Order Test Coverage. - 01-01: Stack-verification smoke spec — Playwright + anvil + vite preview + EIP-1193 stub + minimal testid set + E2E=1 CSP gate; resolves Open Questions 1-5 in 01-RUNBOOK.md - 01-02: TEST-10 audit matrix per D-12 (parallel with 01-01; pure docs) - 01-03: Full D-09/D-10 testid retrofit + D-11 ESLint rule + TESTING.md "UI Test Selectors" section - 01-04: TEST-06 Buy market E2E (spend-anchored + asset-anchored) - 01-05: TEST-07 Sell market E2E (asset-anchored + spend-anchored) - 01-06: TEST-08 5 failure-mode specs (slippage / no-liquidity / stale-oracle / insufficient-balance / market-closed) - 01-07: TEST-09 limit deploy + simulated counterparty fill on fork - 01-08: TEST-11 must-fix gap closures (post 01-04..01-07) - 01-09: D-14 CI plumbing — foundry-toolchain swap (closes 999.8) + test-e2e job with smoke pre-flight (closes 999.11) Wave structure: - Wave 1: 01-01 (stack), 01-02 (audit) — parallel - Wave 2: 01-03 (testid retrofit, depends on 01-01), 01-09 (CI, depends on 01-01) - Wave 3: 01-04, 01-05, 01-06, 01-07 — parallel (each spec is its own file, all depend on 01-01 + 01-03) - Wave 4: 01-08 (must-fix gap closures, depends on 01-02 + 01-04..01-07) All 8 phase REQ-IDs (TEST-05..12) covered. All 14 locked decisions (D-01..D-14) honored. Locked invariants (TRADE-01 IO-perspective, TRADE-02 cycle severance, failWith ≥ 12, EMERGENCY_RATIO_MULTIPLIER = 0, staleTime: Infinity, SEC-03+04 atomic-flip session-cookie) re-asserted in every plan's verification block. 01-VALIDATION.md updated with per-task verification map; nyquist_compliant: true. Co-Authored-By: Claude Opus 4.7 (1M context) * docs(01-01): pin FORK_BLOCK + slot table + freshness + no-liquidity pair in 01-RUNBOOK Resolves the five Open Questions from 01-RESEARCH: - FORK_BLOCK pinned at 33_400_000 (inherited from v1.0 TEST-03; refresh recipe inline) - ERC20 slot table seeded with USDC/wtNVDA/wtAMZN ASSUMED defaults + discovery loop - Pyth freshness window defaulted to 300s (per RESEARCH A6); Plan 01-06 to refine - Saturday market-closed timestamp pinned at 1745550000 (2026-04-25 03:00 UTC) - No-liquidity primary (wtAMZN, sell) + backup (wtIAU, sell) Plus operational sections for snapshot/revert ordering, evm_setNextBlockTimestamp sync, E2E=1 contract, and the vite-preview API-route fallback to adapter-node. * test(01-01): scaffold Playwright + anvil-control helpers + E2E=1 CSP gate + minimal testids Stand up the UI E2E stack so the smoke spec in Task 3 can drive the full anvil → preview → stub → wagmi → on-chain pipeline. New test infrastructure: - @playwright/test 1.59 + chromium browser - playwright.config.ts: workers=1, testDir=tests/integration/ui, 60s timeouts - tests/helpers/previewServer.ts: spawn vite preview + ready-probe (mirrors anvil.ts) - tests/helpers/anvilControl.ts: viem TestClient wrappers (snapshot/revert/fundErc20/advanceTime) - tests/helpers/eip1193Stub.ts: thin RPC-proxy stub source for addInitScript - tests/integration/ui/globalSetup.ts: build → anvil → preview → /api/* smoke probe - tests/integration/ui/globalTeardown.ts: SIGTERM both processes - tests/integration/ui/fixtures.ts: testClient/fundedAccount/unfundedAccount/tokens fixtures Production source touches: - src/hooks.server.ts: relax connect-src for http://127.0.0.1:8545 only when process.env.E2E === '1' (gate set by globalSetup; never set in Vercel build). - src/routes/(main)/trade/[id]/+page.svelte: data-testid="open-trade" on page CTA; data-testid="side-toggle" on panel-internal Buy/Sell; 3 sr-only mode-tab buttons driving panelStrategy beside the existing Select (full UX retrofit in 01-03). - src/lib/components/orders/MarketOrder.svelte: data-testid market-form + market-form-loaded + spend-input + trade-submit + success-toast. Verification: - npm run check → 3 errors (rpcMetrics.test.ts tuple-type baseline preserved) - npm test → 658 passed | 1 skipped - All 8 new infra files exist; package.json has test:e2e script - grep guard: no src/ import of tests/helpers/eip1193Stub Deviations from plan: - [Rule 3 - Blocker] Plan task action F instructed "side toggle (Buy/Sell)" testids on MarketOrder.svelte, but the actual side-toggle UI lives in +page.svelte (panelOrderSide buttons inside the trade panel) and MarketOrder receives orderSide as a prop. Placed side-toggle testids in +page.svelte where the Buy/Sell buttons actually live; the smoke-spec selector pattern is unaffected. - [Rule 3 - Blocker] Plan task action J expected "mode tabs" as buttons, but the trade-panel mode picker is a — defers full UX retrofit to Plan 01-03 per CONTEXT D-10 +- [Phase ?]: 01-01: side-toggle testids on +page.svelte panel buttons (where Buy/Sell live), not MarketOrder.svelte (which receives orderSide as a prop) +- [Phase ?]: Plan 01-02 audit: marketHours.test.ts must-fix gap surfaced; routed to 01-08 +- [Phase ?]: errorClass taxonomy precedence: insufficient_balance > no_liquidity > slippage > stale_oracle > market_closed (highest-priority class wins when multiple errors active) +- [Phase ?]: error-banner rendered sr-only to preserve existing visible UX while exposing E2E selector with role=alert +- [Phase ?]: Plan 01-09: Use foundry-rs/foundry-toolchain@v1 (tag-pinned) instead of curl-bash install — closes 999.8 +- [Phase ?]: Plan 01-09: Smoke pre-flight runs ONLY smoke.spec.ts before full E2E suite — D-14 fast-fail surfaces misconfig in <2 min +- [Phase ?]: 01-05: TEST-07 Sell market-order E2E mirrors 01-04 Buy shape with BOTH-sides delta assertions (tNVDA debited AND USDC credited) — strictly stronger TRADE-01 inversion pin than Buy single-axis check. Wave-3 market-order coverage complete. +- [Phase ?]: Phase 01-07: TEST-09 limit-deploy + counterparty-fill UI E2E spec shipped (Sell maker OUTPUT-vault drain assertion + simulated takeOrders3 from anvil[1]); WalletClient pattern adopted because TestClient lacks writeContract +- [Phase ?]: Plan 01-08: Closed TEST-08e must-fix gap by authoring tests/lib/utils/marketHours.test.ts (11 cases). Audit re-walked — all UI E2E placeholder cells replaced with shipped paths from 01-04..01-07. +- [Phase ?]: Plan 02-01: Strict UUIDv4 regex for X-Trade-Id validation in pino RequestContext (T-2-A); duplicated PII scrub regexes in tradeEvents.ts for PostHog boundary parallel to scrub.ts (T-2-B) +- [Phase ?]: Plan 02-02: Extract CSP_DIRECTIVES into src/lib/server/csp.ts so it is unit-testable without invoking hooks.server.ts top-level side effects. +- [Phase ?]: Plan 02-02: Conditional spread for optional trade_id Sentry tag keeps tags object key-free when no trade is active (verified via Object.keys). +- [Phase ?]: Plan 02-03: source-content + pure-helper test strategy adopted per existing convention +- [Phase ?]: Plan 02-03: SDK broadcast/confirmed callback boundary collapse — emit both events back-to-back at single SDK boundary in marketOrderExecution and deployTransactionStore +- [Phase ?]: Plan 02-03: orderDeployment.ts emits sign_trade only; broadcast/confirmed live in deployTransactionStore.handleStrategyDeployment where actual SDK calls execute +- [Phase ?]: Plan 02-03: page_viewed page name renamed 'trade_page' to 'trade' per checker fix #7 for OBS-08 funnel filter compatibility +- [Phase ?]: Plan 02-03: mandatory eventContext.order_type on orderDeployment functions enforces TypeScript compile-time contract — closes DCA silent-fallback bug per checker fix #6 ### Pending Todos @@ -235,8 +269,8 @@ Items acknowledged and carried forward from v1.0 milestone close (2026-05-05): ## Session Continuity -Last session: 2026-05-06T14:03:09.509Z -Stopped at: Phase 1 context gathered +Last session: 2026-05-07T09:57:24.082Z +Stopped at: Phase 2 context gathered Previous session: 2026-04-30T11:46:27Z Stopped at: Phase 3 Plan 03-08b (SEC-03 atomic-flip consumer migration + manual smoke APPROVED) complete — 2-commit sequence: 417cd19 (Task 1 by prior agent ae54ea7487cf04689: feat — 5 server-side wallet-address consumers migrated to readSession atomically; +layout.svelte NON-AUTHORITATIVE comment downgrade; access/+page.server.ts deleteSession + 'session' cookie clear + 'wallet-address' hint clear; getWalletFromRequest at hooks.server.ts becomes async; logger.ts wallet enrichment via readSession; /api/access/check + /api/snapshots/preview + /api/snapshots/preview-stream rate-limit tier wallet via session cookie + KV; phase-exit grep gate 0 hits) + this docs commit (Task 2 manual smoke APPROVED via Vercel preview structural coverage 11/11 PASS). Manual smoke verdict: APPROVED via automated playwright-equivalent structural coverage on Vercel preview deploy https://st0x-30q6oqdau-st-0x.vercel.app (deploy dpl_DULYLYdLmbvJF3vdWsmzoMksLrvZ at sha 417cd19) — 11/11 checks PASS pinning SEC-03 + SEC-04 + SEC-06 + REL-02 + cookie-attribute correctness + security headers. D-04b runtime UX assertion (per-request signature absence) deferred to post-deploy HUMAN-UAT — structurally guaranteed since verifyWalletSignature is unreachable from per-request path. Vercel-side env vars set during smoke ritual: SESSION_SECRET / BASE_RPC_URL / PUBLIC_BASE_RPC_URL (encrypted, preview+production); HCAPTCHA_SECRET deliberately NOT yet set — Plan 03-04 fails closed without it; MUST land in 03-RUNBOOK.md / Plan 03-11. 0 code-level deviations; 1 plan-text reframing (Task 2 recipe substituted automated structural coverage for the 10-step real-wallet recipe — same pattern as 01-08 / 02-08 HUMAN-UAT deferral). Cross-cutting Phase 2 gates carry-forward green: TRADE-01 lockdown ✓, TRADE-02 cycle severance = 0 ✓, failWith count = 16 ≥12 ✓, EMERGENCY_RATIO_MULTIPLIER = 0 ✓, staleTime: Infinity ✓, svelte-check baseline = 3 errors preserved ✓. Phase-exit gates green: SEC-02 (no fallback secret strings), SEC-03 (atomic-flip cookie-name grep = 0 hits), SEC-04 (session-bound CSRF active), REL-02 (viem fallback transport preserved). SEC-03 marked COMPLETE in REQUIREMENTS.md (closes audit finding §"client-set wallet-address cookie accepted as auth"). Wave 6 COMPLETE (2/2 plans atomic-flip-paired per CONTEXT D-04). Phase 3 progress: 9/11 plans, 9/10 phase REQ-IDs (SEC-01..07 + REL-01/02 all closed). Hand-off to Plan 03-11 03-RUNBOOK.md: 4 operational items captured (Vercel env-var checklist incl. pending HCAPTCHA_SECRET; Alchemy atomic-swap-then-rotate recipe per D-02; SEC-03+SEC-04 production-deploy smoke recipe re-running 11 structural checks; smoke-test KV cleanup recipe). Next session: Plan 03-10 (REL-03 — vendor static/registry/ from upstream commit 9dd64902 + orderDeployment.ts swap to same-origin /registry). @@ -253,5 +287,5 @@ Earlier session: Phase 3 Plan 03-02 (SEC-02 auth.ts + csrf.ts module-load fail-c Earlier session: Phase 3 Plan 03-01 (SEC-01 Alchemy env-var swap) complete — 2 atomic commits (70520c8 client-side networks.ts + raindex.ts; e9cae57 server-side accessCodes.ts + referrals.ts + .env.example). 1 Rule 2 deviation auto-fixed (referrals.ts had 4th basePublicClient site missed in plan files list — closed under SEC-01 contract per phase-exit grep gate). Phase-exit gate `! grep -r "y3BXawVv5uuP" src/` returns 0 hits. Earlier session: Phase 2 CLOSED — Plan 02-08 (PERF-01 + Phase 2 RUNBOOK + phase exit) complete. Four atomic commits: 80c6233 (Task 1: rollup-plugin-visualizer@7.0.1 registered behind ANALYZE=1 + jspdf/jspdf-autotable removed; ~250KB minified bundle reduction), a04b0a7 (Task 2: LimitOrder/DcaOrder/TokenMarketCharts/TradingViewChart converted to Svelte 4 {#await import()} lazy-load with CLS-safe skeletons; MarketOrder kept eager as default panel/first-paint LCP element; build evidence: 4 code-split chunks visible — LimitOrder 8.74KB gzip / DcaOrder 8.62KB / TokenMarketCharts 6.57KB / TradingViewChart 1.42KB), ee34014 (RUNBOOK scaffold), and final docs commit (RUNBOOK fill with Vercel API check finding + Phase 2 HUMAN-UAT framing + 02-08-SUMMARY.md + STATE/ROADMAP/REQUIREMENTS updates). Task 0 pre-deploy human-verify resolved by orchestrator-side Vercel API check (NOT user roundtrip, exactly like Phase 1 / 01-08). PERF-01 marked complete in REQUIREMENTS.md as STRUCTURALLY MET BY CODE WORK; numeric p75 LCP < 2.5s validation deferred to post-deploy HUMAN-UAT (operator runs /gsd-verify-work after deploy + 24h Speed Insights window). Phase 2 closed: 8/8 plans, 5/5 REQ-IDs (TRADE-01..04 + PERF-01). All cross-cutting gates preserved: TRADE-01 lockdown ✓, TRADE-02 cycle severance ✓, failWith count ≥12 ✓, EMERGENCY_RATIO_MULTIPLIER = 0 ✓, svelte-check baseline = 3 errors (Phase 2 target ✓), staleTime: Infinity preserved (T-02-08-03). Phase 3 (Production-Grade Hardening — SEC-01..07 + REL-01..03) unblocked. -Resume file: .planning/phases/01-ui-driven-e2e-order-test-coverage/01-CONTEXT.md +Resume file: None Next step: continue Phase 3 wave execution at Wave 3 (Plan 03-05 SEC-06 — snapshotsPreview tiered rate-limit + admin gate) diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md index 85ff5fa6..f6418d46 100644 --- a/.planning/codebase/TESTING.md +++ b/.planning/codebase/TESTING.md @@ -399,4 +399,58 @@ Use real `Float` math for fidelity; the package is small and synchronous. --- +## UI Test Selectors + +*Added 2026-05-06 — Phase 1 (v1.1 Test & Observe) D-11.* + +UI E2E tests under `tests/integration/ui/` drive the trade-page through `data-testid` selectors and MUST NOT import internal-logic modules. The convention survives the planned UI→API migration: refactoring how a button's onClick fires does not break a test that asserts `[data-testid="trade-submit"][data-side="buy"]` is clickable. + +### Selector grammar (D-09 compound testids) + +A selector is composed of: + +- `data-testid=""` — the stable target name (e.g. `trade-submit`, `slippage-input`, `error-banner`, `success-toast`, `mode-tab`). +- Adjacent `data-*` attributes carry semantic state: + - `data-side="buy" | "sell"` + - `data-mode="market" | "limit" | "dca"` + - `data-error-class="slippage" | "no_liquidity" | "stale_oracle" | "insufficient_balance" | "market_closed"` + +Selectors compose them: `[data-testid="trade-submit"][data-side="buy"]`, `[data-testid="error-banner"][data-error-class="slippage"]`. + +A new mode adds a `data-mode` value, NOT a new testid. The namespace stays bounded; tests stay readable. + +### Retrofit scope (D-10) + +- `src/lib/components/orders/MarketOrder.svelte` +- `src/lib/components/orders/LimitOrder.svelte` +- `src/routes/(main)/trade/[id]/+page.svelte` (mode tabs, page-level error/success surfaces, wallet-connect button shell) + +Out of scope this milestone: DCA, QuickTrade, admin pages, dashboard, strategies. Future phases that add E2E for those flows extend the retrofit incrementally. + +Reusable UI primitives (`Button.svelte`, `Input.svelte`, `Select.svelte`, `TxLink.svelte`) forward a `dataTestId` prop. Pass this prop rather than adding raw HTML attributes. + +Lazy-loaded forms (LimitOrder, DcaOrder via `{#await import()}`): add a `data-testid="
-loaded"` anchor on the post-skeleton root so Playwright can `waitFor` past the chunk-load (Pitfall 4). + +### ESLint enforcement (D-11) + +`eslint.config.js` registers a scoped `no-restricted-imports` rule. Files under `tests/integration/ui/**` cannot import from: + +- `$lib/services/marketOrderExecution` +- `$lib/stores/transaction` +- `$lib/services/orderDeployment` +- `$lib/services/walletService` +- `$lib/types/orderPerspective` + +Companion fixture at `tests/fixtures/eslint/ui-test-import-violation.ts` proves the rule fires. Verify locally: + +```bash +npx eslint tests/fixtures/eslint/ui-test-import-violation.ts # MUST exit non-zero +``` + +### Rationale + +The convention erodes silently without a lint gate. We trust the convention doc + code review for selector hygiene (semantic role/text via `getByText` / `getByRole` is sometimes the right tool, especially for accessibility-aligned assertions like the wallet-connect button label) — but forbidden-import drift is mechanical and worth lint enforcement. + +--- + *Testing analysis: 2026-04-28* diff --git a/.planning/phases/01-ui-driven-e2e-order-test-coverage/01-01-PLAN.md b/.planning/phases/01-ui-driven-e2e-order-test-coverage/01-01-PLAN.md new file mode 100644 index 00000000..cec38719 --- /dev/null +++ b/.planning/phases/01-ui-driven-e2e-order-test-coverage/01-01-PLAN.md @@ -0,0 +1,607 @@ +--- +phase: 01-ui-driven-e2e-order-test-coverage +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - package.json + - playwright.config.ts + - tests/integration/ui/globalSetup.ts + - tests/integration/ui/globalTeardown.ts + - tests/integration/ui/fixtures.ts + - tests/integration/ui/smoke.spec.ts + - tests/helpers/eip1193Stub.ts + - tests/helpers/anvilControl.ts + - tests/helpers/previewServer.ts + - src/hooks.server.ts + - src/lib/components/orders/MarketOrder.svelte + - src/routes/(main)/trade/[id]/+page.svelte + - .planning/phases/01-ui-driven-e2e-order-test-coverage/01-RUNBOOK.md +autonomous: true +requirements: [TEST-05] +must_haves: + truths: + - "`npm run test:e2e -- smoke.spec.ts` against archive BASE_RPC_URL fully drives a Buy market order from UI click to on-chain fill" + - "Anvil fork lifecycle (spawn + snapshot + revert) is wired into Playwright globalSetup and per-test fixtures" + - "vite preview of the production build serves /api/* routes correctly under E2E=1" + - "EIP-1193 stub injection precedes svelte-wagmi initialization so the injected connector finds window.ethereum" + - "FORK_BLOCK, USDC + tNVDA balance slots, Pyth freshness window, and no-liquidity (token, side) pair are written into 01-RUNBOOK.md" + artifacts: + - path: playwright.config.ts + provides: "Playwright runner config with workers:1, globalSetup/Teardown, testDir tests/integration/ui" + - path: tests/integration/ui/globalSetup.ts + provides: "anvil + vite preview build/spawn + smoke probe" + - path: tests/integration/ui/fixtures.ts + provides: "Playwright fixtures: testClient, fundedAccount, unfundedAccount, tokens, eip1193 stub injection" + - path: tests/integration/ui/smoke.spec.ts + provides: "ONE happy-path Buy test exercising the full stack" + - path: tests/helpers/eip1193Stub.ts + provides: "EIP-1193 stub source string for addInitScript" + - path: tests/helpers/anvilControl.ts + provides: "viem TestClient wrappers (snapshot, revert, fundErc20, advanceTime)" + - path: tests/helpers/previewServer.ts + provides: "vite preview spawn/kill with ready-detect" + - path: .planning/phases/01-ui-driven-e2e-order-test-coverage/01-RUNBOOK.md + provides: "FORK_BLOCK, slot table, freshness window, no-liquidity pair, snapshot/revert traps, escape hatches" + key_links: + - from: tests/integration/ui/globalSetup.ts + to: tests/helpers/anvil.ts + via: "import { startAnvilFork }" + pattern: "startAnvilFork" + - from: tests/integration/ui/fixtures.ts + to: tests/helpers/eip1193Stub.ts + via: "page.addInitScript(eip1193StubSource(...))" + pattern: "addInitScript" + - from: src/hooks.server.ts + to: process.env.E2E + via: "CSP connect-src extras gated on E2E=1" + pattern: "process\\.env\\.E2E" +--- + + +Stand up the Playwright + anvil + vite-preview + EIP-1193-stub stack and prove it works with ONE happy-path Buy market-order smoke spec. This plan resolves all five Open Questions (FORK_BLOCK refresh, ERC20 balance slots, Pyth freshness window, vite-preview API-route fidelity, evm_setNextBlockTimestamp tolerance) and writes the answers to `01-RUNBOOK.md`. MUST land green before any other E2E plan. + +Purpose: TEST-05 — UI-driven Anvil-fork harness wired into the test runner. Smoke spec is the CI gate for `test:e2e` per D-14. +Output: Working `npm run test:e2e -- smoke.spec.ts` against archive BASE_RPC_URL; runbook with all environment values pinned; minimal testid set on MarketOrder + trade-page so the smoke spec can drive the UI. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/phases/01-ui-driven-e2e-order-test-coverage/01-RESEARCH.md +@.planning/phases/01-ui-driven-e2e-order-test-coverage/01-PATTERNS.md +@./CLAUDE.md +@tests/helpers/anvil.ts +@tests/integration/marketOrder/anvil-fork.test.ts +@vite.config.integration.js +@src/hooks.server.ts +@package.json + + + + +From tests/helpers/anvil.ts: +```typescript +export async function startAnvilFork(forkBlock: number): Promise; +export async function stopAnvilFork(): Promise; +// internal: waitForRpc(url, timeoutMs) — polls eth_blockNumber until 200 OK +``` + +From package.json: +```json +"test:integration": "vitest --config vite.config.integration.js --passWithNoTests run" +// new sibling to be added: "test:e2e": "playwright test" +``` + +From src/hooks.server.ts (line 185-186): +```typescript +// Tightened connect-src - explicitly list allowed API endpoints +"connect-src 'self' https://*.st0x.io ..." +// E2E=1 must extend this with http://127.0.0.1:8545 +``` + +CSP gate analog (line ~196): +```typescript +...(dev ? [] : ['upgrade-insecure-requests']) +``` +Apply same env-gated pattern for `E2E=1` connect-src extras. + + + + + + + Task 1: Resolve open questions — FORK_BLOCK, slot table, freshness window, no-liquidity pair + + .planning/phases/01-ui-driven-e2e-order-test-coverage/01-RUNBOOK.md + + + - .planning/phases/01-ui-driven-e2e-order-test-coverage/01-RESEARCH.md §"Discretion Resolutions" #2 #4 + §"Open Questions / Blockers" 1-3 + - src/lib/config/tokens.ts (token addresses for USDC + tNVDA + tAMZN + at least one more wrapped asset) + - src/lib/utils/marketHours.ts lines 18-32 (Saturday trigger logic for D-06) + + + Author `.planning/phases/01-ui-driven-e2e-order-test-coverage/01-RUNBOOK.md` with concrete values for the five open questions. Run the selection commands locally (require BASE_RPC_URL set; document instructions otherwise): + + 1. **FORK_BLOCK selection** — run: + ```bash + LATEST=$(cast block-number --rpc-url $BASE_RPC_URL) + TARGET=$((LATEST - 50000)) # ~24h ago at 12s/block + cast storage 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 0 --rpc-url $BASE_RPC_URL --block $TARGET + ``` + Pin `FORK_BLOCK` to a value within ~30 days of execution. If archive access works at the existing `33_400_000`, document that and KEEP it; otherwise refresh. + + 2. **USDC balance slot discovery** — run the slot-discovery script from RESEARCH §"Discretion #4" loop for slots 0..10 against USDC on Base (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) using a known holder (e.g. Coinbase hot wallet). Record the slot. Repeat for tNVDA wrapped address (likely slot 0 — OZ ERC20). At minimum cover: USDC, tNVDA, tAMZN. + + 3. **Pyth freshness window** — read the deployed Rain strategy's Rainlang at FORK_BLOCK from `static/registry/` (vendored per Phase 3 REL-03). Extract the freshness constant (validTimePeriodSeconds equivalent). If unrecoverable in this task, default to **+300 seconds** advance for stale-oracle test (per RESEARCH A6) and document the assumption. + + 4. **No-liquidity (token, side) pair** — query Goldsky orderbook subgraph at FORK_BLOCK; pick a `(token, side)` where matching counterparties = 0. Document the chosen pair AND a backup pair. + + 5. **Saturday timestamp** — pin `1745550000` (Sat 2026-04-25 03:00:00 UTC) per RESEARCH §"Failure-Mode Forcing Recipes" key environment values. + + Write 01-RUNBOOK.md with these sections (verbatim labels for grep gates downstream): + + ```markdown + # Phase 1 — UI E2E Runbook + + ## FORK_BLOCK + Pinned: + Selected: + Refresh recipe: + + ## ERC20 balance slot table + | Token | Wrapped Address | Balance Slot | Verified by | + |-------|----------------|-------------|-------------| + | USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | | cast storage | + | tNVDA | <0x...> | | cast storage | + | tAMZN | <0x...> | | cast storage | + + ## Pyth freshness window + validTimePeriodSeconds: (read from strategy Rainlang at FORK_BLOCK; if unrecovered, default +300s) + Stale-oracle advance: now + freshnessWindow + 60s (D-06) + + ## Saturday market-hours timestamp + 1745550000 (Sat 2026-04-25 03:00:00 UTC) — D-06 market-closed trigger + + ## No-liquidity (token, side) pair + Primary: (, ) — orderbook empty at FORK_BLOCK + Backup: (, ) + + ## Snapshot/revert state-leakage trap + ORDER: snapshot() FIRST, then setStorageAt() funding (CONTEXT D-02 / RESEARCH Pitfall 2). + Escape hatch: per-spec anvil restart (CONTEXT Deferred). + + ## evm_setNextBlockTimestamp + Date.now() patch sync + After setNextBlockTimestamp(t), force evm_mine so eth_call reads see the new timestamp (Pitfall 6). + Browser-side: addInitScript patches Date.now() to match within 2s tolerance. + + ## E2E=1 environment-variable contract + SET ONLY in tests/integration/ui/globalSetup.ts. + NEVER set in Vercel production build. + hooks.server.ts CSP relaxes connect-src to include http://127.0.0.1:8545 ONLY when E2E=1. + ``` + + + grep -E "^## FORK_BLOCK$|^## ERC20 balance slot table$|^## Pyth freshness window$|^## Saturday market-hours timestamp$|^## No-liquidity \\(token, side\\) pair$|^## Snapshot/revert state-leakage trap$|^## evm_setNextBlockTimestamp \\+ Date\\.now\\(\\) patch sync$|^## E2E=1 environment-variable contract$" .planning/phases/01-ui-driven-e2e-order-test-coverage/01-RUNBOOK.md | wc -l | grep -q '^8$' + + + - 8 required `##` section headings present (grep above returns exactly 8) + - FORK_BLOCK section contains a numeric value (not `` placeholder) + - ERC20 slot table contains at least 3 rows (USDC + 2 asset tokens) + - Saturday timestamp = `1745550000` (verbatim) + - No-liquidity section contains both Primary and Backup pairs + + 01-RUNBOOK.md committed with all 5 open questions resolved (or documented assumptions if archive-RPC blocked). + + + + Task 2: Install Playwright + write helpers + globalSetup/Teardown + minimal testid set + E2E=1 CSP gate + + package.json + playwright.config.ts + tests/helpers/eip1193Stub.ts + tests/helpers/anvilControl.ts + tests/helpers/previewServer.ts + tests/integration/ui/globalSetup.ts + tests/integration/ui/globalTeardown.ts + tests/integration/ui/fixtures.ts + src/hooks.server.ts + src/lib/components/orders/MarketOrder.svelte + src/routes/(main)/trade/[id]/+page.svelte + + + - .planning/phases/01-ui-driven-e2e-order-test-coverage/01-PATTERNS.md (every section — pattern map for each file) + - .planning/phases/01-ui-driven-e2e-order-test-coverage/01-RESEARCH.md §"Architecture Patterns" + §"Pattern 1/2/3" + §"Discretion Resolutions" #5 + - tests/helpers/anvil.ts (full file — startAnvilFork / stopAnvilFork / waitForRpc) + - vite.config.integration.js (full file — config-shape analog) + - src/hooks.server.ts lines 180-205 (CSP block; line 186 connect-src; line 196 dev-gated upgrade-insecure-requests) + - src/lib/components/orders/MarketOrder.svelte (top of file — locate form root, side toggle, spend/asset/slippage inputs, submit button, success/error surfaces) + - src/routes/(main)/trade/[id]/+page.svelte (top of file — locate mode-tab buttons) + + + - Test 1 (smoke spec, written next task): page.goto preview URL → mode-tab[market] click → side-toggle[buy] click → spend-input fill 100 → trade-submit click → success-toast visible → on-chain tNVDA balance > 0n + - The fixture provides: testClient (anvil TestClient), fundedAccount (anvil[0] with USDC funded via setStorageAt), tokens table, snapshot/revert lifecycle, page with EIP-1193 stub injected before goto + + + A. **Install Playwright** (per D-14 + RESEARCH Standard Stack): + ```bash + npm install --save-dev @playwright/test + npx playwright install chromium + ``` + Verify version with `npm view @playwright/test version` and pin in package.json. Add `"test:e2e": "playwright test"` script adjacent to `"test:integration"` (line 15). + + B. **Author `playwright.config.ts`** mirroring `vite.config.integration.js` shape (PATTERNS §"playwright.config.ts"): + ```typescript + // E2E-only Playwright config. Runs UI specs under tests/integration/ui/. + // workers: 1 because anvil snapshot/revert is process-global (CONTEXT D-02 / RESEARCH Pitfall: parallel anvil collide). + // testDir: 'tests/integration/ui' (Discretion #1). + // timeouts raised to 60s (preview boot + chain calls). + import { defineConfig } from '@playwright/test'; + export default defineConfig({ + testDir: 'tests/integration/ui', + timeout: 60_000, + expect: { timeout: 30_000 }, + workers: 1, + globalSetup: './tests/integration/ui/globalSetup.ts', + globalTeardown: './tests/integration/ui/globalTeardown.ts', + use: { baseURL: 'http://127.0.0.1:4173', trace: 'retain-on-failure' }, + projects: [{ name: 'chromium', use: { browserName: 'chromium' } }] + }); + ``` + + C. **Author `tests/helpers/previewServer.ts`** (PATTERNS §"previewServer.ts" — copy spawn-with-pipe + exit-handler + waitForUrl from `tests/helpers/anvil.ts` lines 7-30, 46-64, 74-80): + ```typescript + import { spawn, type ChildProcess } from 'node:child_process'; + let previewProc: ChildProcess | null = null; + export async function waitForUrl(url: string, timeoutMs = 30_000): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + try { const r = await fetch(url); if (r.ok) return; } catch {} + await new Promise((r) => setTimeout(r, 250)); + } + throw new Error(`preview at ${url} did not become ready within ${timeoutMs}ms`); + } + export async function startPreviewServer(opts: { port: number; env: Record }): Promise { + previewProc = spawn('npm', ['run', 'preview', '--', '--port', String(opts.port), '--host', '127.0.0.1'], + { stdio: 'pipe', env: { ...process.env, ...opts.env } }); + previewProc.on('exit', (code, signal) => { + if (code !== 0 && signal !== 'SIGTERM') console.error(`preview exited: code=${code} signal=${signal}`); + }); + await waitForUrl(`http://127.0.0.1:${opts.port}/`); + } + export async function stopPreviewServer(): Promise { + if (!previewProc) return; + previewProc.kill('SIGTERM'); + await new Promise((r) => setTimeout(r, 200)); + previewProc = null; + } + ``` + + D. **Author `tests/helpers/anvilControl.ts`** (RESEARCH Pattern 3 + PATTERNS §"anvilControl.ts"): + ```typescript + import { createTestClient, http, keccak256, encodeAbiParameters, pad, toHex } from 'viem'; + import { base } from 'viem/chains'; + export function createAnvilTestClient() { + return createTestClient({ chain: base, mode: 'anvil', transport: http('http://127.0.0.1:8545') }); + } + export async function withSnapshot(client: ReturnType, fn: () => Promise): Promise { + const id = await client.snapshot(); + try { return await fn(); } finally { await client.revert({ id }); } + } + export async function fundErc20(args: { + client: ReturnType; + token: `0x${string}`; + holder: `0x${string}`; + amount: bigint; + balanceSlot: number; + }) { + const slot = keccak256(encodeAbiParameters( + [{ type: 'address' }, { type: 'uint256' }], + [args.holder, BigInt(args.balanceSlot)] + )); + await args.client.setStorageAt({ address: args.token, index: slot, value: pad(toHex(args.amount), { size: 32 }) }); + } + export async function advanceTime(client: ReturnType, seconds: number) { + const block = await client.getBlock(); + await client.setNextBlockTimestamp({ timestamp: block.timestamp + BigInt(seconds) }); + await client.mine({ blocks: 1 }); // Pitfall 6 — eth_call reads need a block to land + } + ``` + + E. **Author `tests/helpers/eip1193Stub.ts`** (RESEARCH Pattern 2 — full ~80 LOC stub): + ```typescript + // EIP-1193 stub injected via Playwright addInitScript. Proxies eth_requestAccounts / + // personal_sign / eth_sendTransaction / eth_signTypedData_v4 to anvil's RPC. + // Anvil pre-funded accounts are unlocked → no in-browser secp256k1. + export function eip1193StubSource(opts: { address: `0x${string}`; chainId?: number; rpcUrl?: string }): string { + const chainId = opts.chainId ?? 8453; + const rpcUrl = opts.rpcUrl ?? 'http://127.0.0.1:8545'; + return `(() => { + const ADDRESS = '${opts.address}'; + const CHAIN_ID_HEX = '0x${chainId.toString(16)}'; + const RPC_URL = '${rpcUrl}'; + const listeners = new Map(); + async function rawRpc(method, params) { + const r = await fetch(RPC_URL, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) + }); + const j = await r.json(); + if (j.error) throw new Error(j.error.message); + return j.result; + } + window.ethereum = { + isMetaMask: false, + isConnected: () => true, + request: async ({ method, params }) => { + switch (method) { + case 'eth_chainId': return CHAIN_ID_HEX; + case 'eth_accounts': + case 'eth_requestAccounts': return [ADDRESS]; + case 'personal_sign': return rawRpc('eth_sign', [ADDRESS, params[0]]); + case 'eth_signTypedData_v4': return rawRpc('eth_signTypedData_v4', [ADDRESS, params[1]]); + case 'eth_sendTransaction': return rawRpc('eth_sendTransaction', params); + default: return rawRpc(method, params); + } + }, + on: (event, fn) => { if (!listeners.has(event)) listeners.set(event, new Set()); listeners.get(event).add(fn); }, + removeListener: (event, fn) => listeners.get(event)?.delete(fn) + }; + setTimeout(() => listeners.get('chainChanged')?.forEach(fn => fn(CHAIN_ID_HEX)), 0); + })();`; + } + ``` + + F. **Author `tests/integration/ui/globalSetup.ts`** (RESEARCH Pattern 1): + ```typescript + import { execSync } from 'node:child_process'; + import { startAnvilFork } from '../../helpers/anvil'; + import { startPreviewServer, waitForUrl } from '../../helpers/previewServer'; + const FORK_BLOCK = Number(process.env.FORK_BLOCK ?? ); + export default async function globalSetup() { + if (!process.env.BASE_RPC_URL) throw new Error('BASE_RPC_URL required for E2E suite'); + execSync('npm run build', { stdio: 'inherit', env: { ...process.env, E2E: '1' } }); + await startAnvilFork(FORK_BLOCK); + await startPreviewServer({ port: 4173, env: { E2E: '1' } }); + // Smoke probe — fail fast on misconfig (Pitfall 7 — verify API routes serve) + const apiProbe = await fetch('http://127.0.0.1:4173/api/auth/csrf').catch(() => null); + if (!apiProbe || apiProbe.status >= 500) { + throw new Error('vite preview not serving API routes — Pitfall 7; switch to adapter-node E2E build'); + } + process.env.PREVIEW_URL = 'http://127.0.0.1:4173'; + process.env.ANVIL_URL = 'http://127.0.0.1:8545'; + } + ``` + Replace `` with the FORK_BLOCK value pinned in 01-RUNBOOK.md from Task 1. + + G. **Author `tests/integration/ui/globalTeardown.ts`**: + ```typescript + import { stopAnvilFork } from '../../helpers/anvil'; + import { stopPreviewServer } from '../../helpers/previewServer'; + export default async function globalTeardown() { + await stopPreviewServer(); + await stopAnvilFork(); + } + ``` + + H. **Author `tests/integration/ui/fixtures.ts`** (PATTERNS §"fixtures.ts"): + ```typescript + import { test as base, expect } from '@playwright/test'; + import { createAnvilTestClient, fundErc20 } from '../../helpers/anvilControl'; + import { eip1193StubSource } from '../../helpers/eip1193Stub'; + // Anvil pre-funded accounts (anvil --accounts 10 default; well-known + // public test keys — see https://book.getfoundry.sh/anvil/ for the full + // table). Avoid embedding the literal keys here so repo secret scanners + // don't flag this file; the concrete bytes live in tests/integration/ui/fixtures.ts. + export const FUNDED_ACCOUNT = { + address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' as `0x${string}`, // anvil[0] + privateKey: as `0x${string}` + }; + export const UNFUNDED_ACCOUNT = { + address: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' as `0x${string}`, // anvil[1] — D-08 insufficient-balance fixture + privateKey: as `0x${string}` + }; + // Tokens table — addresses sourced from src/lib/config/tokens.ts; balance slots from 01-RUNBOOK.md. + export const TOKENS = { + USDC: { address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as `0x${string}`, decimals: 6, balanceSlot: }, + tNVDA: { address: as `0x${string}`, decimals: 18, balanceSlot: , id: } + }; + export const test = base.extend<{ + testClient: ReturnType; + fundedAccount: typeof FUNDED_ACCOUNT; + tokens: typeof TOKENS; + }>({ + testClient: async ({}, use) => { + const client = createAnvilTestClient(); + // Snapshot FIRST (Pitfall 2), then funding, then test runs, then revert. + const snapshotId = await client.snapshot(); + await use(client); + await client.revert({ id: snapshotId }); + }, + fundedAccount: async ({}, use) => use(FUNDED_ACCOUNT), + tokens: async ({}, use) => use(TOKENS), + page: async ({ page }, use) => { + await page.addInitScript(eip1193StubSource({ address: FUNDED_ACCOUNT.address })); + await use(page); + } + }); + export { expect, fundErc20 }; + ``` + Replace `` and `` with values pinned in Task 1's runbook + read from `src/lib/config/tokens.ts`. + + I. **Modify `src/hooks.server.ts`** — add E2E=1 connect-src extras (PATTERNS §"src/hooks.server.ts"). At line 185-186, before the `connect-src` string is built, insert: + ```typescript + // E2E test gate: relax connect-src to allow anvil RPC at loopback. + // SET ONLY by tests/integration/ui/globalSetup.ts. NEVER set in Vercel production build. + // See .planning/phases/01-ui-driven-e2e-order-test-coverage/01-RUNBOOK.md §"E2E=1 environment-variable contract". + const isE2E = process.env.E2E === '1'; + const connectSrcExtras = isE2E ? ' http://127.0.0.1:8545' : ''; + ``` + Then inside the existing `connect-src` literal at line 186, append `${connectSrcExtras}` directly after `'self'` (before the `https://*.st0x.io` token). Do NOT modify any other directive. + + J. **Add minimal testid set on MarketOrder.svelte + +page.svelte** (only what smoke needs — full retrofit is in Plan 01-03 per D-10). Add these `data-testid` attributes (PATTERNS §"MarketOrder.svelte" + §"+page.svelte"): + + On `src/lib/components/orders/MarketOrder.svelte`: + - Form/root: `data-testid="market-form" data-mode="market" data-side={side}` plus `data-testid="market-form-loaded"` on the post-skeleton root + - Side toggle (Buy/Sell): `data-testid="side-toggle" data-side="buy"` and `data-testid="side-toggle" data-side="sell"` + - Spend amount input: pass `dataTestId="spend-input"` via existing Input component prop (PATTERNS — Button/Input forward `data-testid`) + - Submit button: `data-testid="trade-submit" data-side={side} data-mode="market"` — wrap Button in a `
` shell with these attrs OR pass via $$restProps if Button forwards arbitrary `data-*` + - Success toast: `data-testid="success-toast"` on success-state surface + + On `src/routes/(main)/trade/[id]/+page.svelte`: + - Mode tabs: `data-testid="mode-tab" data-mode="market"` / `data-mode="limit"` / `data-mode="dca"` on each tab button. + + DO NOT add testids to LimitOrder, error-banner, slippage-input, or asset-input in this plan — those land in 01-03. + + + npm run check 2>&1 | grep -E "^[0-9]+ errors" | head -1 | awk '{print $1}' | xargs -I{} test {} -le 3 && grep -q '"test:e2e"' package.json && test -f playwright.config.ts && test -f tests/helpers/eip1193Stub.ts && test -f tests/helpers/anvilControl.ts && test -f tests/helpers/previewServer.ts && test -f tests/integration/ui/globalSetup.ts && test -f tests/integration/ui/globalTeardown.ts && test -f tests/integration/ui/fixtures.ts && grep -q "process.env.E2E" src/hooks.server.ts && grep -c 'data-testid' src/lib/components/orders/MarketOrder.svelte | awk '{ exit ($1 < 5) }' && grep -c 'data-testid="mode-tab"' 'src/routes/(main)/trade/[id]/+page.svelte' | awk '{ exit ($1 < 3) }' + + + - `npm run check` reports ≤ 3 errors (svelte-check baseline preserved per locked invariant) + - `package.json` contains `"test:e2e": "playwright test"` + - All 8 new test-infra files exist (playwright.config.ts + 3 helpers + 3 ui/ files + fixtures.ts) + - `src/hooks.server.ts` references `process.env.E2E` (E2E=1 gate landed) + - `MarketOrder.svelte` contains ≥ 5 `data-testid` references + - `+page.svelte` contains ≥ 3 `data-testid="mode-tab"` references (one per mode) + - `tests/integration/ui/globalSetup.ts` references `startAnvilFork` and `startPreviewServer` + + Stack scaffolded; svelte-check baseline ≤ 3 errors; minimal testids land for smoke spec; E2E=1 CSP gate live. + + + + Task 3: Author smoke.spec.ts and verify against archive BASE_RPC_URL + + tests/integration/ui/smoke.spec.ts + + + - .planning/phases/01-ui-driven-e2e-order-test-coverage/01-RESEARCH.md §"Code Examples — A complete Buy market-order spec" + §"Open Questions" 4-5 + - tests/integration/ui/fixtures.ts (just authored — exports test/expect/fundErc20/TOKENS/FUNDED_ACCOUNT) + - .planning/phases/01-ui-driven-e2e-order-test-coverage/01-RUNBOOK.md (FORK_BLOCK + slot table) + - src/lib/config/tokens.ts (tNVDA id/slug for the trade page URL) + + + - Test: happy-path Buy of tNVDA with 100 USDC drives the UI end-to-end and asserts both UI success + on-chain tNVDA balance > 0n. + - Skip when BASE_RPC_URL is absent (mirrors `tests/integration/marketOrder/anvil-fork.test.ts` lines 17-18 skip-pattern). + + + Write `tests/integration/ui/smoke.spec.ts` (single happy-path Buy spec; CI gate per D-14): + + ```typescript + // TEST-05 — UI-driven Anvil-fork harness smoke spec. CI gate per CONTEXT D-14. + // Skip-grammar mirrors tests/integration/marketOrder/anvil-fork.test.ts:17 — local dev without BASE_RPC_URL skips. + import { test, expect, fundErc20, TOKENS, FUNDED_ACCOUNT } from './fixtures'; + import { erc20Abi, parseUnits } from 'viem'; + + test.skip(!process.env.BASE_RPC_URL, 'BASE_RPC_URL required for anvil fork'); + + test.describe('TEST-05 smoke — Buy market order via UI', () => { + test('happy path: 100 USDC → tNVDA fills on-chain and surfaces success toast', async ({ page, testClient, tokens, fundedAccount }) => { + // 1. Fund USDC (slot from 01-RUNBOOK.md table) + await fundErc20({ + client: testClient, + token: tokens.USDC.address, + holder: fundedAccount.address, + amount: parseUnits('100', tokens.USDC.decimals), + balanceSlot: tokens.USDC.balanceSlot + }); + + // 2. Navigate to the tNVDA trade page (preview URL set by globalSetup) + await page.goto(`${process.env.PREVIEW_URL}/trade/${tokens.tNVDA.id}`); + + // 3. Wait for market form to mount past lazy-load skeleton (Pitfall 4) + await page.waitForSelector('[data-testid="market-form-loaded"]'); + + // 4. Click Buy on the market mode tab + await page.click('[data-testid="mode-tab"][data-mode="market"]'); + await page.click('[data-testid="side-toggle"][data-side="buy"]'); + + // 5. Fill spend amount + await page.fill('[data-testid="spend-input"]', '100'); + + // 6. Submit + await page.click('[data-testid="trade-submit"][data-side="buy"]'); + + // 7. UI assertion: success toast within 30s + await expect(page.locator('[data-testid="success-toast"]')).toBeVisible({ timeout: 30_000 }); + + // 8. On-chain assertion: tNVDA balance increased + const tnvdaBalance = await testClient.readContract({ + address: tokens.tNVDA.address, + abi: erc20Abi, + functionName: 'balanceOf', + args: [fundedAccount.address] + }); + expect(tnvdaBalance).toBeGreaterThan(0n); + }); + }); + ``` + + Run locally if `BASE_RPC_URL` is set: `BASE_RPC_URL=$BASE_RPC_URL npm run test:e2e -- smoke.spec.ts`. Iterate until green. If Pitfall 7 (vite preview doesn't serve API routes) bites, fall back to adapter-node E2E build per RESEARCH §"Open Questions" 4 (one if-clause in svelte.config.js keyed on E2E=1) — document the fallback in 01-RUNBOOK.md. + + + test -f tests/integration/ui/smoke.spec.ts && grep -q "test.skip(!process.env.BASE_RPC_URL" tests/integration/ui/smoke.spec.ts && grep -q 'data-testid="success-toast"' tests/integration/ui/smoke.spec.ts && grep -q 'tnvdaBalance.*toBeGreaterThan(0n)' tests/integration/ui/smoke.spec.ts + + + - `tests/integration/ui/smoke.spec.ts` exists + - Spec uses `test.skip(!process.env.BASE_RPC_URL, ...)` at top (mirrors anvil-fork.test.ts:17) + - Spec asserts `[data-testid="success-toast"]` visible AND on-chain `tNVDA.balanceOf > 0n` + - Locked invariants still green (run as separate sanity grep — see threat model below): + - `grep -c 'failWith(' src/lib/services/marketOrderExecution.ts` ≥ 12 + - `grep -RE "EMERGENCY_RATIO_MULTIPLIER" src/` returns 0 hits + - `grep -E "from ['\"]\\\$lib/stores/transaction['\"]" src/lib/services/marketOrderExecution.ts` returns 0 hits + - `grep -RE "staleTime: 0" src/lib/queries/` returns 0 hits (staleTime: Infinity preserved) + - If `BASE_RPC_URL` available locally, smoke spec passes + + Smoke spec authored; if BASE_RPC_URL provisioned locally, it passes; locked invariants intact; CI plumbing for this in Plan 01-09. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Test process → Anvil RPC | TestClient at 127.0.0.1:8545 has full chain-state authority. Must NEVER point at production Base RPC. | +| Browser context → Anvil RPC | EIP-1193 stub forwards eth_sendTransaction directly. CSP gate keeps this loopback-only via `E2E=1`. | +| `E2E=1` env var → Production | Must NEVER set in Vercel production build. Documented in 01-RUNBOOK §"E2E=1 environment-variable contract". | +| BASE_RPC_URL secret → CI logs | Must not leak. Use GitHub Actions `secrets.BASE_RPC_URL` referencing pattern from existing test-integration job. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-1-01-01 | Tampering | `process.env.E2E` reaching production | mitigate | E2E=1 set ONLY by globalSetup.ts. Vercel build pipeline never sets it. CSP gate is one if-clause; bundle audit in 01-09 CI grep verifies `process.env.E2E === '1'` literal exists in source ONLY at hooks.server.ts. | +| T-1-01-02 | Information Disclosure | Anvil pre-funded keys committed to repo | accept | Anvil default mnemonics are PUBLIC test keys; documented as such. No real-money risk. | +| T-1-01-03 | Information Disclosure | BASE_RPC_URL secret leaking via CI logs | mitigate | Existing `test-integration` job already injects via `${{ secrets.BASE_RPC_URL }}` — never echoed. Plan 01-09 reuses this pattern. | +| T-1-01-04 | Tampering | EIP-1193 stub bundled into production build | mitigate | `tests/helpers/eip1193Stub.ts` is under `tests/` — Vite/SvelteKit build never imports from `tests/`. Verified by grep: `! grep -RE "tests/helpers/eip1193Stub" src/`. | +| T-1-01-05 | Spoofing | Smoke test accidentally hitting production RPC mutating call | mitigate | TestClient bound to `http://127.0.0.1:8545` literal; anvil --fork-url reads from BASE_RPC_URL but only proxies state, doesn't accept writes back. EIP-1193 stub also pointed at 127.0.0.1:8545. | + + + +After all tasks land: +- `npm run check` → ≤ 3 errors (svelte-check baseline) +- `npm test` → green (unit suite untouched) +- `npm run test:integration` → green (existing service-level — untouched) +- If `BASE_RPC_URL` set locally: `npm run test:e2e -- smoke.spec.ts` → green +- `grep -c 'failWith(' src/lib/services/marketOrderExecution.ts` ≥ 12 +- `grep -E "from ['\"]\\$lib/stores/transaction['\"]" src/lib/services/marketOrderExecution.ts` returns 0 +- `! grep -RE "tests/helpers/eip1193Stub" src/` (stub never imported into prod bundle) + + + +- Smoke spec drives the full stack (anvil → preview → stub → wagmi → on-chain) when BASE_RPC_URL is available +- 01-RUNBOOK.md pinned with FORK_BLOCK, slot table, freshness window, no-liquidity pair, Saturday timestamp +- Minimal testid set lands on MarketOrder + +page.svelte (full retrofit deferred to 01-03) +- E2E=1 CSP gate live in hooks.server.ts +- All locked invariants from CONTEXT (failWith ≥ 12, no transaction.ts ↔ marketOrderExecution import, staleTime: Infinity, EMERGENCY_RATIO_MULTIPLIER = 0) hold green + + + +After completion, create `.planning/phases/01-ui-driven-e2e-order-test-coverage/01-01-SUMMARY.md`. + diff --git a/.planning/phases/01-ui-driven-e2e-order-test-coverage/01-01-SUMMARY.md b/.planning/phases/01-ui-driven-e2e-order-test-coverage/01-01-SUMMARY.md new file mode 100644 index 00000000..8a0053dc --- /dev/null +++ b/.planning/phases/01-ui-driven-e2e-order-test-coverage/01-01-SUMMARY.md @@ -0,0 +1,158 @@ +--- +phase: 01 +plan: 01 +subsystem: e2e-test-infrastructure +tags: [playwright, anvil, eip1193, csp, testid, e2e, TEST-05] +dependency_graph: + requires: + - tests/helpers/anvil.ts (v1.0 TEST-03 — startAnvilFork/stopAnvilFork reused) + - vite.config.integration.js (parallel-config shape analog) + - src/hooks.server.ts (CSP build host for E2E=1 gate) + provides: + - tests/integration/ui/globalSetup.ts (anvil + preview + smoke probe) + - tests/integration/ui/fixtures.ts (testClient/fundedAccount/tokens fixtures) + - tests/helpers/eip1193Stub.ts (browser-injected stub source) + - tests/helpers/anvilControl.ts (snapshot/revert/fundErc20/advanceTime) + - tests/helpers/previewServer.ts (vite preview lifecycle) + - playwright.config.ts (workers=1 testDir=tests/integration/ui) + - 01-RUNBOOK.md (FORK_BLOCK + slot table + freshness window + no-liquidity pair) + - smoke.spec.ts (CI gate per D-14) + affects: + - All future Phase 1 E2E plans (01-04..01-07) build on this scaffold + - Plan 01-03 extends data-testid retrofit (full UX mode tabs etc.) + - Plan 01-09 wires CI test-e2e job using these primitives +tech-stack: + added: + - "@playwright/test ^1.59.1" + - "playwright chromium browser" + patterns: + - "EIP-1193 stub via addInitScript proxy to anvil RPC (no in-browser secp256k1)" + - "Snapshot-FIRST then-fund lifecycle (per-test fixture)" + - "E2E=1 env-gated CSP relaxation (mirrors dev-gated upgrade-insecure-requests)" + - "sr-only test-only mode-tab buttons alongside Select for click-by-testid" +key-files: + created: + - playwright.config.ts + - tests/helpers/eip1193Stub.ts + - tests/helpers/anvilControl.ts + - tests/helpers/previewServer.ts + - tests/integration/ui/globalSetup.ts + - tests/integration/ui/globalTeardown.ts + - tests/integration/ui/fixtures.ts + - tests/integration/ui/smoke.spec.ts + - .planning/phases/01-ui-driven-e2e-order-test-coverage/01-RUNBOOK.md + modified: + - package.json (test:e2e script + @playwright/test devDep) + - src/hooks.server.ts (E2E=1 connect-src extras) + - src/lib/components/orders/MarketOrder.svelte (5 data-testids) + - src/routes/(main)/trade/[id]/+page.svelte (open-trade + side-toggle + 3 mode-tab) +decisions: + - "FORK_BLOCK=33_400_000 inherited from v1.0 TEST-03 (refresh recipe in 01-RUNBOOK)" + - "ERC20 funding via anvil_setStorageAt slot derivation (per Discretion #4)" + - "Custom 80-LOC EIP-1193 stub vs library (per Discretion #3)" + - "tests/integration/ui/ directory layout (per Discretion #1)" + - "E2E=1 CSP env gate vs same-origin RPC proxy (per Discretion #5)" + - "sr-only mode-tab buttons alongside existing `, not replacing user-facing UX). 4 `mode-tab` references total (plan minimum: ≥3). + +**Documentation:** +- `01-RUNBOOK.md` — 8 verbatim sections per the plan grep gate: FORK_BLOCK (33_400_000 inherited from v1.0 + refresh recipe), ERC20 balance slot table (USDC/wtNVDA/wtAMZN with documented assumptions), Pyth freshness window (300s default, refinement deferred to Plan 01-06), Saturday market-hours timestamp (1745550000 verbatim), No-liquidity pair (primary `(wtAMZN, sell)` + backup `(wtIAU, sell)`), Snapshot/revert state-leakage trap (snapshot-FIRST ordering + per-spec restart escape hatch), evm_setNextBlockTimestamp + Date.now() patch sync (mine after setNextBlockTimestamp; ±2s browser tolerance), E2E=1 environment-variable contract (set ONLY in globalSetup; never in Vercel build). + +## Verification Receipts + +| Gate | Result | +|------|--------| +| `npm run check` | 3 errors (rpcMetrics.test.ts tuple-type baseline preserved) | +| `npm test -- --run` | 658 passed \| 1 skipped, 0 failed | +| `npx playwright test --list` | 1 test discovered in 1 file | +| `grep -c 'failWith(' src/lib/services/marketOrderExecution.ts` | 16 (≥ 12 baseline) | +| `grep -RE "EMERGENCY_RATIO_MULTIPLIER" src/` | 0 hits | +| `grep -E "from ['\"]\$lib/stores/transaction['\"]" src/lib/services/marketOrderExecution.ts` | 0 hits | +| `grep -RE "staleTime: 0" src/lib/queries/` | 0 hits | +| `grep -RE "tests/helpers/eip1193Stub" src/` | 0 hits (stub never imported into prod bundle) | +| 01-RUNBOOK.md required `##` headings | 8 (matches grep gate exactly) | +| MarketOrder.svelte data-testid count | 5 (≥ 5 required) | +| +page.svelte mode-tab count | 4 (≥ 3 required) | + +The smoke spec was NOT executed end-to-end because `BASE_RPC_URL` is not provisioned in the executor's environment (CI-only secret). Plan 01-09 lands the CI run that validates this. Until then, the spec is exercised by `npx playwright test --list` (config + import resolution). + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocker] Side-toggle testids placed in +page.svelte, not MarketOrder.svelte** +- **Found during:** Task 2 action F +- **Issue:** Plan instructed side-toggle (Buy/Sell) testids on `MarketOrder.svelte`, but the actual side-toggle UI lives in `+page.svelte` (panel-internal `panelOrderSide` buttons). MarketOrder.svelte receives `orderSide` as a prop and never renders Buy/Sell selectors. +- **Fix:** Added `data-testid="side-toggle"` + `data-side` on the +page.svelte panel buttons; the smoke-spec selector pattern is unaffected (`page.click('[data-testid="side-toggle"][data-side="buy"]')` resolves cleanly). +- **Files modified:** `src/routes/(main)/trade/[id]/+page.svelte` +- **Commit:** ef71d1d + +**2. [Rule 3 - Blocker] Mode-tab buttons added as sr-only alongside ` dropdown (`