Release: develop -> main - #67
Merged
Merged
Conversation
* Add viewport toggle for handbook screens with multiple baselines Merge desktop/tablet/mobile variant cards into a single card per screen with a Mobile/Desktop[/Tablet] toggle. Mobile is the default, since zkCoins is a mobile-first PWA. Affects Welcome, Settings, Funded Balance, Send-Confirm Dialog and Receive Default. The underlying E2E tests and baseline images are unchanged — only the handbook presentation collapses redundant variants. * Validate handbook ↔ e2e baseline coverage in sync script The handbook references screenshots under /handbook/screenshots/<name>.png that are hardlinked at build time from e2e/<spec>.spec.ts-snapshots/. Until now, drift between the two sides (a removed e2e test still documented, or a new baseline never linked from any card) only surfaced as a runtime 404 on the live page. Extend scripts/sync-handbook-baselines.mjs to scan both handbook HTMLs after the sync runs and exit non-zero on either direction: - handbook references a name with no matching baseline → fail - baseline exists but no handbook card references it → fail This runs automatically via `predev` and `prebuild`, so CI's Lint & Build step now catches the drift before it ships. Also fix the existing drift the validator surfaced: the `unlock-unlocking` card was still in both handbooks even though that test was removed from 04-unlock-password.spec.ts. Drop the card and bring the per-spec / top-level test+baseline counters in line (71/68 → 70/67, spec 04 5/5 → 4/4).
…ers (#66) PR #64 widened the settings-network-badge timeout from 10 s to 30 s to mask a race between WalletScreen's `useEffect(api.info, …)` and any subsequent navigation that gates UI on `networkName !== ''`. Symptom fix, not root cause: the test still races, just with more headroom. Root cause: the helper that brings a test into the wallet does not wait for the api.info roundtrip to settle. Any test that navigates to /settings (or another networkName-gated route) immediately afterwards sees an empty store. Fix: add `waitForNetworkInfo` to e2e/_helpers/wallet.ts and call it at the end of `aliceLogin`/`bobLogin`. It polls the zustand store directly via the window-exposed `__useNetworkStore` (see src/stores/network.ts) — already the pattern the 09-network-badge- loading test uses to guard its own intercept. Once the helper returns, every navigation to a networkName-gated UI is deterministic. Restore the 05-disconnect goToSettings timeout to the default 10 s; the wider value is no longer doing any work.
…helpers (#70) Same pattern as the api.info root-cause fix (#66): the login helpers now block on WalletScreen's first /api/balance tick before returning, so any subsequent assertion is race-free. Shared helper waitForBalanceLoaded in e2e/_helpers/wallet.ts polls the data-loading attribute on balance-amount-usd. The attribute is true while balance === null (post-mount loading) and absent once the first tick lands — regardless of value. wallet-empty-banner visibility is not a reliable loaded-signal because the banner only renders for balance === 0; for a funded wallet it never appears. aliceLogin / bobLogin in fixtures.ts now call both waitForNetworkInfo and waitForBalanceLoaded at the end. Inline waitForAliceBalanceLoaded in 07-send.spec.ts is removed — its work is now done in the helper. The 02-create-seed wallet-after-create timeout is restored from 60s (PR #64 widening) to the default 30s: with the deterministic balance wait wired into the upstream helper this margin is no longer needed.
…nistically (#73) Two follow-up fixes after PR #70 merged and revealed flake under concurrent CI load (multiple branches running E2E against the same dev.zkcoins.app at once): 1. wait helpers exit as soon as the underlying signal lands, so a larger upper bound is purely a safety margin and not wasted time: waitForNetworkInfo 15s -> 30s waitForBalanceLoaded 30s -> 60s These cover api.info and the first /api/balance tick when the shared DEV API is under load. 2. wallet-after-create in 02-create-seed.spec.ts does not go through aliceLogin (creates a fresh wallet inline), so the helper-level pre-warm never ran here. Call waitForBalanceLoaded directly before the wallet-empty-banner assertion, and drop the banner timeout to 5s (the wait above is the real gate).
#72) App-side half of #71. The full intended scope was App-side testid + audit- script update + spec locator swap, but the spec swap cannot ship in this PR: the E2E suite runs against the deployed DEV bundle (https://dev.zkcoins.app), and the new testid is not present there until this PR merges and DEV redeploys. The spec swap is tracked in #74 as a post-deploy follow-up. Changes in this PR: - Add `data-testid="onboarding-step-back-btn"` to the StepHeader Back-Btn so the real MVP button is classified individually instead of waved through by a file-level exemption. - Drop `Onboarding.tsx` from `MVP_EXEMPT_FILES`; add the two `FEATURES.PASSKEY`-gated buttons (PasskeyFlow register, PasskeyRestoreFlow authenticate) to `MVP_EXEMPT_BUTTON_SNIPPETS` — both are dead-stripped from the PRD bundle. The authenticate button pins on the unique label `Authenticate with passkey` rather than `onClick={restore}` (the latter also appears on the SeedImport submit button, which has its own testid). - Add `onboarding-step-back-btn` to `MVP_EXEMPT_TESTIDS` with a TODO(#74) comment. The exemption disappears once #74 lands and the specs reference the testid. Refs #71
…estid (#75) Post-deploy follow-up to #72. The App-side testid landed in #72 and is now live on https://dev.zkcoins.app, so the spec swap and the audit's temporary exemption can ship. - e2e/02-create-seed.spec.ts `back-from-reveal`: both clicks switch from `page.locator('button').first()` to `page.getByTestId('onboarding-step-back-btn')`. Click count and the DEV-vs-PRD branching logic stay as they are. - e2e/03-restore-seed.spec.ts `back-from-input`: same swap on the single click. - e2e/_audit/coverage.mjs: drop the `onboarding-step-back-btn` TODO(#74) entry from `MVP_EXEMPT_TESTIDS` — the testid is now covered by an e2e reference, so Section A picks it up automatically. Closes #71
…69) * test(e2e): add axe-core a11y regression spec (issue #68 W2) Adds e2e/12-a11y.spec.ts: runs @axe-core/playwright with wcag2a/wcag2aa tags against the six MVP routes (welcome, mid-onboarding seed-reveal, wallet home, /send, /receive, /settings) and fails on any new serious or critical violation. Allowlist mechanism (KNOWN_VIOLATIONS) is empty by default — first PR's job is to surface existing violations, not silence them. No screenshots, so the spec is intentionally excluded from the visual-baseline regen workflow. e2e/README.md gets a new \xa78.13 section describing the route matrix and allowlist convention, and totals are bumped to 79/70. * feat(api): validate HTTP responses with Zod at the boundary (issue #68 W3) Every api.* call in src/lib/api/client.ts now parses its response through a schema in src/lib/api/schemas.ts before returning. A renamed or missing server field throws a ZodError at the boundary instead of leaking undefined deep into a render. Two layers of contract checking: - Compile-time: existing client*.test.ts mocks are typed via z.infer<typeof Schema>, mirroring the WASM-mock pattern. Drift between a mock and its schema becomes a TS error. - Runtime, opt-in: src/__tests__/lib/api/contract.live.test.ts is skipped unless RUN_API_CONTRACT=true. It performs a real /api/info plus a mint -> balance -> sendSigned round-trip against E2E_API_URL (default dev-api.zkcoins.app) and feeds the responses through the schemas. The manual api-contract.yml workflow runs it weekly and on workflow_dispatch. README gains an 'API Client' section describing both layers. * test(components): add SetPassword/SendForm/UnlockWallet tests (issue #68 W1) Adds three component tests under @testing-library/react that catch regressions in form validation without going through the 10-minute e2e pipeline: - SetPassword.test.tsx: drives SeedFlow to the password stage and asserts on the Create-button disabled state, the password-mismatch branch, the min-length branch, and that the inline error clears once a valid pair is accepted. - SendForm.test.tsx: drives /send with a funded Alice fixture in the store and covers empty/invalid/exceeds-balance/set-max branches plus the success-cleared-error transition. - UnlockWallet.test.tsx: exercises the unlock screen's password flow (empty disables submit, typed enables it, wrong password surfaces 'Incorrect password'). To make the unlock screen testable, UnlockScreen moves from src/app/page.tsx (which Next.js does not allow extra exports from) to src/components/onboarding/UnlockScreen.tsx. The prop bag is unchanged, so the call site in Home is a one-line import swap. Plumbing changes: - @testing-library/jest-dom matchers wired into the global vitest setup, alongside a cleanup() afterEach so getByTestId does not hit leftover trees from a previous test. - @vitejs/plugin-react added so vitest can parse JSX in *.test.tsx. - vitest.config.ts test.include extended with src/**/*.test.tsx. - coverage.include is intentionally unchanged: tsx coverage stays out of the gate (regression safety, not coverage chasing). * test(e2e): add axe-core a11y regression spec (issue #68 W2) Adds e2e/12-a11y.spec.ts: runs @axe-core/playwright with wcag2a/wcag2aa tags against the six MVP routes (welcome, mid-onboarding seed-reveal, wallet home, /send, /receive, /settings) and fails on any new serious or critical violation. Allowlist mechanism (KNOWN_VIOLATIONS) is empty by default — first PR's job is to surface existing violations, not silence them. No screenshots, so the spec is intentionally excluded from the visual-baseline regen workflow. e2e/README.md gets a new \xa78.13 section describing the route matrix and allowlist convention, and totals are bumped to 79/70. * feat(api): validate HTTP responses with Zod at the boundary (issue #68 W3) Every api.* call in src/lib/api/client.ts now parses its response through a schema in src/lib/api/schemas.ts before returning. A renamed or missing server field throws a ZodError at the boundary instead of leaking undefined deep into a render. Two layers of contract checking: - Compile-time: existing client*.test.ts mocks are typed via z.infer<typeof Schema>, mirroring the WASM-mock pattern. Drift between a mock and its schema becomes a TS error. - Runtime, opt-in: src/__tests__/lib/api/contract.live.test.ts is skipped unless RUN_API_CONTRACT=true. It performs a real /api/info plus a mint -> balance -> sendSigned round-trip against E2E_API_URL (default dev-api.zkcoins.app) and feeds the responses through the schemas. The manual api-contract.yml workflow runs it weekly and on workflow_dispatch. README gains an 'API Client' section describing both layers. * test(components): add SetPassword/SendForm/UnlockWallet tests (issue #68 W1) Adds three component tests under @testing-library/react that catch regressions in form validation without going through the 10-minute e2e pipeline: - SetPassword.test.tsx: drives SeedFlow to the password stage and asserts on the Create-button disabled state, the password-mismatch branch, the min-length branch, and that the inline error clears once a valid pair is accepted. - SendForm.test.tsx: drives /send with a funded Alice fixture in the store and covers empty/invalid/exceeds-balance/set-max branches plus the success-cleared-error transition. - UnlockWallet.test.tsx: exercises the unlock screen's password flow (empty disables submit, typed enables it, wrong password surfaces 'Incorrect password'). To make the unlock screen testable, UnlockScreen moves from src/app/page.tsx (which Next.js does not allow extra exports from) to src/components/onboarding/UnlockScreen.tsx. The prop bag is unchanged, so the call site in Home is a one-line import swap. Plumbing changes: - @testing-library/jest-dom matchers wired into the global vitest setup, alongside a cleanup() afterEach so getByTestId does not hit leftover trees from a previous test. - @vitejs/plugin-react added so vitest can parse JSX in *.test.tsx. - vitest.config.ts test.include extended with src/**/*.test.tsx. - coverage.include is intentionally unchanged: tsx coverage stays out of the gate (regression safety, not coverage chasing).
…obes (#76) vitest's 5 s default times out the /api/mint and /api/balance probes before the server's real ZK-proof generation completes (typical 10–20 s on DEV). The pre-merge version had a single 120 s timeout on the combined round-trip test; when the test was split into three endpoint-specific cases, only /api/info inherited the default — the other two needed the same explicit override. Confirmed by manual workflow_dispatch on develop (run 26004360092): /api/info green, /api/mint + /api/balance timed out at 5 s.
Adds src/__tests__/app/send-pipeline.test.tsx (14 tests) for the SendPage paths the existing SendForm.test.tsx and e2e/07-send.spec.ts do not reach: - Phase-1 /api/send + Phase-2 /api/commit round-trip, success screen, balance refresh, transaction row, request body shape. - 3-attempt commit retry loop: success on attempt 2, exhaustion after attempts 1+2+3 with the user-visible "delivery failed" error and preserved inflight payload. Back-off setTimeout calls of 2 s / 4 s are intercepted to a microtask so the retry budget collapses; every other timer (incl. the 120 s AbortController guard in request()) uses the real clock so the rest of the page lifecycle is untouched. - In-flight commit crash recovery on mount: payload cleared on success, preserved on failure, malformed JSON tolerated. - Username resolution branches gated by FEATURES.USERNAMES: @suffix, $prefix, hex fast-path, resolve failure surfacing the API error. - Defensive branches: account.xpriv empty throws "No private key"; no-account redirect honours the 100 ms grace window; redirect suppressed when the account lands inside that window. Coverage thresholds unchanged — the surface remains src/lib/** and src/stores/**, both still 100 % on every axis. SendPage itself stays outside coverage scope (component, not lib/store), so the tests are additive: 175 → 189 unit tests, no new dead code paths.
Adds src/__tests__/packages/zkcoins-wasm/fallback.test.ts (12 tests). The setup-file mock for `@zkcoins/wasm` is bypassed via `vi.importActual`, so the real `initWasm()` runs. wasm-bindgen's client loads `client_bg.wasm` through `fetch(new URL(..., import.meta.url))`, which cannot resolve under happy-dom (no `file://` fetch) — the import rejects and `initWasm()` substitutes `createJsFallback()`. Every fallback method is asserted to throw the WASM_REQUIRED message so SendPage / Onboarding / the wallet store surface the "reload the page or use a modern browser" prompt instead of silently returning `undefined` and crashing further downstream. Also covers the `wasmModule` singleton: repeated `initWasm()` calls within one module evaluation return the same instance; resetting the module cache produces a fresh fallback.
Adds src/__tests__/app/send-edge-cases.test.tsx (9 tests). Complements `SendForm.test.tsx` (amount-field validation) and `send-pipeline.test.tsx` (Phase-1 + Phase-2 round-trip) by targeting conditional renders and state-preservation branches neither file exercises: - balance===null pre-tick: "— BTC" placeholder, data-loading attribute, Set max disabled, no-funds banner hidden. - balance===0: no-funds banner visible, Set max still disabled. - balance>0: formatted "1.00000000 BTC", Set max enabled. - Recovering banner stays hidden without an inflight payload. - Confirm card Cancel: recipient + amount inputs preserved, confirm card unmounts, submit button re-enabled. - Confirm card content: typed amount rendered in compact BTC. - handleConfirm guard: "Balance not loaded yet" surfaced when balance is null at submit time. - Floating-point safety: 0.3 BTC → exactly 30_000_000 sats (asserts Math.round vs floor; without it, 0.3 * 1e8 in IEEE-754 would silently underflow to 29_999_999). - 0.00000001 BTC → exactly 1 sat in the confirm card.
Adds src/__tests__/components/WalletScreen.polling.test.tsx (10 tests). The 5 s `setInterval(api.balance, 5000)` in WalletScreen is the only background work in the wallet UI and was previously asserted only by e2e screenshots — which capture the post-poll DOM, not the timing semantics, the cleanup behaviour, or the account-swap path. Covers: - mount tick fires immediately and writes balance to store - interval fires every 5 s; balance overwrites each time - unmount clears the interval (no ticks after a 60 s wall-time wait) - empty account → no fetch - account swap → new interval keyed to the new address - thrown balance error is swallowed; store balance unchanged - FEATURES.USERNAMES off → setUsername never called even if server returns one - FEATURES.USERNAMES on + no existing username → username assigned on first tick - FEATURES.USERNAMES on + existing username → second tick does not overwrite - api.info on mount writes networkName to the network store
Adds src/__tests__/components/Onboarding-restore.test.tsx (13 tests). Onboarding.tsx is 847 LOC and the only existing unit test (SetPassword.test.tsx) covers the create-wallet password stage. The restore path has its own BIP-39 validation, word-count guard, and store-write semantics that fan out across WASM + auth store + wallet store + api.balance. Covers: - entry from Welcome via onboarding-restore-btn - StepHeader Back returns to Welcome - input validation: Continue disabled while empty; rejects non-12-word phrase; rejects 12-word phrase that fails BIP-39 list check (validateMnemonic mocked false for one call); accepts valid phrase; retyping clears the inline error eagerly via onChange - password stage: Restore disabled while either field empty; min-8-char enforced; mismatch enforced - restore side effects: account written, authMethod='seed' set, balance fetched - balance fetch failure: non-fatal, restore still completes, balance stays null (WalletScreen poll picks it up later) - createAccountFromMnemonic throws: stage rolls back to password, inline error shows the WASM error, no store writes happen
) * fix(a11y): bump ink3 contrast, hide seed-grid indexes, title QR svg Closes #77, #78, #79, #80, #81. Resolves the six axe-core violations `12-a11y.spec.ts` pre-populated into `KNOWN_VIOLATIONS` on its first CI run against DEV. Three independent fixes, one shared root cause analysis (see `fix/probe-a11y` run output captured below): 1. `tailwind.config.ts` — bump `ink3` from #71717a to #909099. The old token rendered at 4.04:1 on `bg-surface` (#0c0c0c) and failed WCAG AA color-contrast on 13 nodes across every MVP route: back links, section headers, descriptive body text, the `BTC` input suffix, the address chip on wallet home, etc. The new value clears 4.5:1 with margin (~6.2:1) and keeps a visible step between ink2 (8.4:1) and ink3. 2. `Onboarding.tsx` — `aria-hidden="true"` on the seed-grid index numbers ("1", "2", …, "12"). Those labels are purely decorative: a screen reader user hears each mnemonic word in order, no need to also announce its position. axe flagged them at 2.01:1 (ink4 on pure black), but the correct fix is to skip them from the accessibility tree, not to bump ink4 globally and visually un-mute the disabled-state palette. 3. `receive/page.tsx` — pass `title="Receive address QR code"` to `QRCodeSVG`. qrcode.react's `title` prop is the documented accessibility hook and emits a `<title>` child of the SVG, which axe accepts as the accessible name for `svg-img-alt`. Cleanup: - `12-a11y.spec.ts` `KNOWN_VIOLATIONS` array now empty (all six entries removed) and the file header reflects the new state. - `e2e/README.md` § 8.13 updated for the empty allowlist. CI note: every screen that uses `text-ink3` will pixel-diff against the existing visual baselines. The `E2E Tests` job will go red until `regenerate-visual-baselines.yml` is dispatched on this branch and its auto-PR is admin-merged into here. * test(e2e): regenerate visual baselines (#91) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(a11y): keep KNOWN_VIOLATIONS populated until DEV picks up the fix The accessibility spec runs against `dev.zkcoins.app`. DEV deploys from `develop`, so a fix lives in two stages: code change merged → deploy-dev workflow rolls → DEV serves the new bundle. Between the merge and the deploy the spec would otherwise fire on "violations the codebase no longer has" — green-vs-red flips on the deploy, not on the merge. Re-populate the allowlist with the six entries the spec surfaced on the first run, but spell out in each reason that the code fix already landed and the entry comes out in the follow-up PR after the DEV deploy. Same wording in `e2e/README.md § 8.13`. The next PR on this branch (once #83 has merged and the DEV deploy is green) empties the array again. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…gic (#87) * test(shell): cover AppShell + BottomNav prop matrix and active-tab logic Adds src/__tests__/components/AppShell.test.tsx (11 tests). AppShell wraps every wallet route and BottomNav dead-strips its Apps tab via FEATURES.APPS_DIRECTORY; neither had a unit test, and e2e only captured the styled default, not the prop matrix or the active-tab logic across paths with sub-routes. Covers: - AppShell: renders children, shows nav + footer by default, omits each when showNav=false / showFooterLinks=false. - BottomNav PRD bundle (APPS_DIRECTORY off): - "/" exact match highlights Wallet - non-root path does not highlight Wallet (no startsWith trap) - /settings exact + /settings/* sub-route both highlight Settings - nav-apps absent - BottomNav DEV bundle (APPS_DIRECTORY on, via vi.resetModules): - nav-apps present with href="/apps" - /apps/* sub-route highlights Apps * test(shell): use text marker instead of test-only testid e2e/_audit/coverage.mjs walks the entire src/ tree, including src/__tests__/, so a `data-testid="shell-child"` fixture in this unit test was flagged as an uncovered MVP testid. Switch to a text marker (`screen.getByText`) which avoids polluting the src/ testid inventory.
On pass, the sticky PR comment now renders a single-line "all clear" summary instead of dumping per-section lists of exempt entries and template-literal resolutions. Reviewers reading the PR only need to know whether the audit found something actionable; the implementation detail of what counts as exempt belongs in the script, not in every comment. On fail, the comment now lists only the actionable findings (non-exempt A/C plus ghost B entries), so the punch list maps directly to what needs fixing. --json output is unchanged and still emits all three sections for tooling.
Adds src/__tests__/components/PwaPrompt.test.tsx (13 tests). The component has four user-visible branches (native install, iOS Safari, manual address-bar hint, hidden) plus a handful of lifecycle invariants that e2e/10-pwa.spec.ts cannot verify: listener cleanup on unmount, localStorage write/clear paths, catch behaviour on thrown prompt(). Covers: - visibility gates: dismissed flag in localStorage, standalone display-mode, iOS legacy `navigator.standalone`. - mode detection: iOS Safari → ios card; Android UA without BIP → manual Android body; no UA hint → desktop manual body; BIP event switches manual → native card. - native install flow: prompt + userChoice called; user rejecting flips the dismissed flag and unmounts the card; a thrown prompt() is swallowed and the button re-enables. - dismiss + appinstalled: X-button persists the flag and unmounts; appinstalled clears the legacy hint flag; beforeinstallprompt listener is removed on unmount.
Closes the last gap from the test-coverage audit. Before this PR
the coverage gate only watched src/lib/** + src/stores/**, so any
new uncovered code in a page.tsx or component.tsx could ship
without triggering the threshold. The audit identified this as
the structural risk that lets app/components rot.
Changes:
- include now also covers src/app/** + src/components/**
- exclude expanded for non-MVP / decorative files:
- layout.tsx (Next.js root, no logic)
- app/apps, app/simulate, app/reset (FEATURES-gated → notFound)
- app/network + components/NetworkActivity (network-activity
chart, triage: keep — already excluded on the lib side)
- components/PixelIcon, components/icons/** (decorative)
- thresholds split into two tiers:
- src/lib/** + src/stores/**: still strict 100 % per-glob
aggregate, matching the prior per-file invariant
- Global aggregate: 75 % statements / functions / lines, 60 %
branches — set just below the post-PR-coverage numbers so a
regression on the UI surface fails CI
Depends on PRs #82, #84, #85, #86, #87, #89, #90 — without those
test additions the global aggregate is ~59 % and CI fails.
Merge those first, then this.
The first cleanup attempt (#93 v1) emptied `KNOWN_VIOLATIONS` straight away, but the CI run against the freshly-deployed DEV surfaced three residual color-contrast nodes that #83 didn't cover: the seed-grid index labels ("1", "2", …, "12") inside `SeedFlow`. `aria-hidden` is the correct hook for screen-readers, but axe's `color-contrast` rule exempts only the *descendants of an ancestor with aria-hidden*, not the element itself, so the labels still showed up at ink4@2.01:1. This PR: - bumps the labels' Tailwind class from `text-ink4` to `text-ink3` (now ~6.2:1 on the dark surface, well over WCAG AA), keeping the `aria-hidden="true"` so screen readers continue to skip them; - trims `KNOWN_VIOLATIONS` to a single residual seed-reveal entry that disappears the moment the DEV deploy following this merge rolls out; - documents the post-deploy state in the spec header. The trailing follow-up empties the array once DEV is live.
… 200 (#94) The server-side fix (zk-coins/node#21) landed in develop, so the /api/balance endpoint now returns 200 with `{balance: 0}` for any well-formed but unobserved address. Drop the client-side try/catch that mapped 404 to balance:0 — the contract is now uniform across the client's `request()` helper. - src/lib/api/client.ts: collapse `balance` to the same single-line shape as `info` and `resolveUsername` - src/__tests__/lib/api/client.test.ts: replace the 404→0 mapping test with a 200→0 test that pins the new contract; keep the non-2xx-error case - src/__tests__/lib/api/contract.live.test.ts: drop the stale 404 reference from the comment
* fix(ui): submit forms on Enter key in send/onboarding/username-claim * fix(ui): guard async handlers against concurrent triggers Enter-key submissions could re-fire handlers while a previous call was still in flight (claim, unlock, send), because button-disabled state is not enforced by the keyboard path. Add explicit loading-state guards in each async handler so click and keyboard paths behave identically. Also realign the username-claim audit exemption snippet, which moved out of the button window when the inline onClick was refactored to a shared callback (so the keyDown path can reuse it). * refactor(ui): migrate Enter-key handling to native form submission Replace onKeyDown listeners and bare onClick submits with native HTML <form onSubmit> semantics across send, onboarding (create + import + unlock), and the username-claim mini-form on the wallet screen. The browser now handles implicit submission (Enter on any single-line input when the submit button is enabled), which gives us: - Free race-condition protection (browsers do not implicitly submit while the type=submit button is disabled), so click and keyboard paths follow the same disabled gate. - Better password-manager integration (form element is the contract managers look for to autofill + submit). - Improved a11y (assistive tech recognises form regions). Non-submit buttons (Set max, Cancel, Confirm Send) get an explicit type=button so they no longer behave as default submitters inside the new form, which would have re-triggered the Send flow. The WalletScreen claim button gets its own data-testid now that the exemption-by-snippet is no longer needed; the entry moves from MVP_EXEMPT_BUTTON_SNIPPETS to MVP_EXEMPT_TESTIDS so future refactors do not silently lose the exemption. * cleanup(ui): drop race guards now redundant under form-submit semantics After moving every keyboard submission path through native <form> onSubmit, the browser already refuses to dispatch implicit submission while the submit button is disabled, and disabled buttons swallow clicks. The if (sending || ...) / if (unlocking) / if (claiming) early-returns I added to defend the previous onKeyDown handlers no longer fire and now just inflate useCallback dep arrays. Revert them so each handler is back to its pre-PR shape (validation-only). Also drop type="button" from the standalone passkey unlock button: it has no enclosing form, so the attribute is inert today and is not the style used by other standalone buttons in this codebase.
* Adapt to server 4xx/5xx + structured error contract
Add a typed `ApiError` class so non-2xx responses surface
`{status, serverError, rawBody}` instead of a stringly-typed
`Error("API error 4xx: ...")`. Centralise the server-error-string
→ German user-message mapping in `errorMessages.ts`, mirroring the
server's `map_send_coins_error` and `handler_error_response` tables.
Send-page and faucet call-sites now branch on `ApiError` to render
the user-facing message via `userMessageFor`. The send path also
normalises pre-PR-#31 servers (200 + `{success: false}`) into an
`ApiError` so the failure handling is uniform across both contracts.
Tests cover the round-trip for every known server error string and
assert each has a non-fallback mapping — fails loudly if the server
adds a new error string without an app-side mapping update.
Closes #99
* Exempt wallet-mint-error testid from button audit
The faucet error toast only renders on api.mint ApiError responses;
the happy-path E2E flow never triggers it. Coverage is provided by
the unit-level error-mapping tests.
The seed-grid index-label fix from #93 is now live on DEV (deploy run 26024966020, 2026-05-18T09:26:11Z). Probed the seed-reveal route directly — zero blocking axe violations across the six MVP routes. Drop the last allowlist entry and rewrite the spec header to reflect the post-fix steady state. No code or DOM change here; pure CI-gate tightening.
…102) Replace the build-time NEXT_PUBLIC_ENABLE_FAUCET / NEXT_PUBLIC_ENABLE_USERNAMES flags with a runtime fetch of /api/info.capabilities. The server reports its Cargo feature set (faucet, usernames, address_list, lnurl) and the app gates the corresponding UI via useFeatures() instead of mirroring the env at build time. The same shipped bundle now works against any DEV/PRD server without a rebuild. - new useCapabilities zustand store with a fail-closed default so an unreachable server or a pre-#29 server hides gated UI rather than crashing - InfoResponseSchema gains an optional CapabilitiesSchema for backward compat - FEATURES retains the six build-time client flags (PASSKEY, APPS_DIRECTORY, AUTO_LOCK, ADDRESS_ROTATION, TOR_ROUTING, DEV_ROUTES); FAUCET / USERNAMES only live on useFeatures() and are not reachable via the static export - mount the capabilities fetch from the root page useEffect - migrate send/page.tsx and WalletScreen.tsx consumers to useFeatures() - tests: new capabilities store coverage, features test split for the hybrid surface, existing FEATURES_STATE-mock pattern adapted to also back useFeatures(), api.info() schema test for both legacy and new response shapes - README + Dockerfile + deploy-dev.yaml comments reflect the new model
…#104) * fix(e2e): 07-send 'send-success' — bump timeouts + race against error banner The send-success test failed twice in a row on the develop CI run following PR #100's merge: `toBeVisible('send-success-heading')` hit its 90 s gate before the 2-phase Send pipeline finished. The PR #100 merge fired three deploys in 90 s and DEV was mid-roll during the test window — proof generation slowed enough that a single commit retry pushed the wallet UI past the 90 s gate. Two small changes: 1. `test.setTimeout(180_000)` and the locator timeout to 150 s — DEV's worst observed wall-clock for this flow is ~100 s (proof + commit + one retry); 150 s gives 50 s headroom and 180 s on the test as a whole still surfaces a genuine deadlock. 2. Race the success heading against `send-error`: either the happy path resolves or the inline error banner shows the real server-side failure. A future flake fails fast with the server's error text instead of "element never appeared after 150 s." * test(home): cover Home routing decision tree Adds src/__tests__/app/home.test.tsx (5 tests). Home decides which of the three top-level screens render: Onboarding (no stored wallet) → UnlockScreen (stored + locked) → WalletScreen (account in memory). The branch priority is load-bearing — a regression that swapped the order would silently leave users on the wrong screen. Covers: - onboarding branch: both hydration calls find nothing → welcome - unlock branch: encrypted blob in IndexedDB → unlock-heading - wallet branch: account in store + unlocked → BottomNav + WalletScreen - priority: account in memory overrides stored-blob-also-exists state - priority: stored blob without account → unlock (not onboarding)
PR #105 was merged before its force-push update reached develop, so the squash-merge picked up the pre-fix version of `vi.mock('@/lib/features', ...)` that only exposed `FEATURES`. After #102 (capabilities) landed, WalletScreen reads runtime FAUCET/USERNAMES through `useFeatures()` — Home renders WalletScreen on the unlocked branch, so this missing export crashes 2 home tests + breaks the global Unit Tests job on every other open PR that rebases onto develop. Add the missing `useFeatures: () => FEATURES_STATE` proxy.
This was referenced May 19, 2026
## Problem
When pushing multiple commits back-to-back to a PR, every commit
triggered a fresh CI run and the older runs kept running instead of
being cancelled. New commits queued behind doomed in-flight runs.
## Root cause
Both `ci.yaml` and `audit-button-coverage.yml` grouped concurrency by
commit SHA:
group: ci-${{ github.event.pull_request.head.sha || github.sha }}
Every commit gets its own group, so `cancel-in-progress: true` never
triggered. The block's stated intent ("a new push cancels the in-flight
run") was the opposite of what actually happened.
## Fix
Group by PR number (with `github.ref` fallback for push/dispatch):
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
A new push to the same PR now lands in the same group as the previous
run, so the old run gets cancelled and the new run starts immediately
instead of queueing.
`audit-button-coverage.yml` uses the same shape with its own workflow
prefix and a short pointer comment back to `ci.yaml` for the full
rationale.
## Stale trigger-block comment
The `on:` block in `ci.yaml` previously justified omitting `main` from
`pull_request.branches` with "the concurrency block would cancel one of
them and surface a red 'cancelled' check on the release PR". Under
PR-number grouping the PR-side run (keyed by PR number) and the push
run (keyed by `refs/heads/develop`) land in DIFFERENT groups, so the
block does NOT deduplicate them. The omission is still correct — it
avoids double-loading the same SHA — but the stale reasoning is
replaced so future readers do not reintroduce `main` by deleting the
comment.
## Concurrency-block gaps on deploy / regen workflows
This change also closes adjacent concurrency-block gaps on the deploy
and regen workflows. They are independent improvements bundled here
because they touch the same area:
- `deploy-dev.yaml`: `deploy-dev-${{ github.ref }}` with
`cancel-in-progress: true`. Back-to-back develop pushes used to fire
parallel deploys that raced on `docker compose recreate`; the newest
commit's deploy now wins and the older one is cancelled. A
`workflow_dispatch` from a feature branch lands in its own ref-keyed
group and does not interact with the develop deploy.
- `deploy-prd.yaml`: `deploy-prd-${{ github.ref }}` with
`cancel-in-progress: false`. Production deploys must NEVER be killed
mid-flight — cancelling halfway through `docker compose recreate`
produces half-renamed Created-state containers; subsequent deploys
queue instead of preempting.
- `regenerate-visual-baselines.yml`: keyed by `inputs.branch` with
`cancel-in-progress: false`. Two concurrent runs against the same
branch would race on the auto-commit of the new baselines and clobber
each other.
## Behaviour
| Event | Group | Effect |
|-----------------------------------|--------------------------------------------------------------------|-------------------------------------------------------------------|
| 2nd push to PR #42 | `ci-CI-42` | cancels in-flight run for #42, starts fresh run |
| Post-merge `push: develop` | `ci-CI-refs/heads/develop` | own group, doesn't touch open PRs |
| `workflow_dispatch` on a feature | `ci-CI-refs/heads/<feature>` | own group per ref |
| 2nd push to develop (DEV deploy) | `deploy-dev-refs/heads/develop` | newest commit's deploy wins; older one cancelled |
| 2nd push to main (PRD deploy) | `deploy-prd-refs/heads/main` | queued; never preempt mid-flight |
| Regen run for branch `feat/x` | `regenerate-visual-baselines-feat/x` | queued; independent branches run in parallel |
## Test plan
- [ ] Push a second commit to this PR; first CI run gets cancelled, second runs without queueing.
- [ ] After merge: the `push: develop` CI run fires independently of any open PR and gets its own group.
- [ ] Two back-to-back merges to develop: only the newest commit's DEV deploy runs to completion.
Aligns the Docker Hub namespace with the rest of the zkCoins project
identity (GitHub org `zk-coins`, domain `zkcoins.app`, sibling repo
`zk-coins/node` already on `zkcoins/node`). The singular `zkcoin/*`
namespace was a holdover from the early days.
Touched:
- .github/workflows/deploy-dev.yaml — DOCKER_TAGS: zkcoin/app:beta → zkcoins/app:beta
- .github/workflows/deploy-prd.yaml — DOCKER_TAGS: zkcoin/app:latest → zkcoins/app:latest
- README.md, CONTRIBUTING.md — image refs in tables + docker build examples
Repo secrets DOCKER_USERNAME / DOCKER_PASSWORD already swapped to the
new `zkcoins` Docker Hub account (same PAT used by zk-coins/node).
Coordinated DFXServer/server compose update follows immediately after
merge so dfxdev / dfxprd pull `zkcoins/app:{beta,latest}`.
- Docker version + pulls badges at the top - Explicit hub.docker.com/r/zkcoins/app link in lead paragraph - Image-tag cells in the Live table now link directly to the corresponding Docker Hub tag listing
* docs: update zk-coins/server links to zk-coins/node
Post-rename audit found 3 markdown refs in README.md + 1 in
CONTRIBUTING.md still pointing at the legacy zk-coins/server GitHub
URL. GitHub auto-redirects so functionality is unaffected, but the
cosmetic drift was flagged by both internal- and external-consistency
audits. Replaces the literal repo string everywhere; preserves the
historical issue numbers (those redirect cleanly).
* fix: residual server → node refs in code comments + e2e doc
Second-pass audit found cross-repo references the first PR missed:
- e2e/README.md: cross-repo doc pointer "zk-coins/server CONTRIBUTING.md
§ DEV state recovery" → zk-coins/node
- src/stores/capabilities.ts, src/lib/api/{errorMessages,schemas}.ts,
src/lib/features.ts, src/__tests__/lib/api/error-mapping.test.ts:
source comments referencing the old Rust module locations
(`zk-coins/server::server.rs::Capabilities`, ...::map_send_coins_error)
switched to `zk-coins/node::router.rs::...`
Comment-only changes; no runtime / test behavior touched.
) Document the rule that surfaced from the May 2026 `07-send-success` incident: the App is a thin client. The private key + UI rendering live in the App; everything else (balance, num_sends, transaction history, server capabilities, account proofs, commitment lookups) lives on `zk-coins/node` and the App must fetch the authoritative value before every operation that depends on it. The local-store `numPubkeys` counter that produced the `prove_account_update_with_in_and_out_coins_and_sources failed` class of bugs is called out explicitly so the next maintainer doesn't repeat the assumption that App-side derivation is "free". Update the State Management subsection to point at the new rule and trim the list of authoritative store fields to those that genuinely belong in the App (xpriv, address, UI flags) instead of the previously-listed `transactions` (server-owned).
The wallet tracks the BIP-32 child-index counter (`numPubkeys`) purely in local state. After a seed restore the counter is reset to 0 — but the server may still hold `account.proof = Some(...)` from a previous session for the same address. The next /send then either omits `prev_commitment_pubkey` (server 400 `"prev_commitment_pubkey required for account update"`, mapped client-side to `Interner Fehler: Vorheriger Public Key fehlt.`) or reuses pubkey[0] and collides on the same SMT slot at commit time. The E2E `07-send.spec.ts::send-success` test exposed this on every retry after the first global-setup minted balance was spent. This is the app side of zk-coins/node PR #129, which adds the authoritative `Account.num_sends` counter and emits it on `/api/balance`. The app: * extends `BalanceResponseSchema` with `num_sends: z.number().default(0)`; * adds `syncNumPubkeys(n)` to the wallet store — a no-op fast-path when the counter is already correct, replace semantics otherwise; * drives a fresh `api.balance` BEFORE every send (SendPage) so the counter is hydrated against the server's source of truth even if the WalletScreen 5-s poll hasn't ticked yet; * hydrates `numPubkeys` on every seed/passkey restore flow; * re-syncs on every WalletScreen balance tick + after the post-send /api/balance refresh. Tests: * `wallet.test.ts` — `syncNumPubkeys` replace semantics + no-op shortcut + multi-account safety. * `send-pipeline.test.tsx` — pre-send balance hydration is mocked in every send-confirm path; the existing `prev_commitment_pubkey` + `numPubkeys` assertions still hold. App-PR depends on node-PR being merged + DEV-deployed first — otherwise the wallet reads `undefined` for `num_sends` against the old server. The schema's `.default(0)` keeps the fallback safe (local counter behaviour preserved).
…es + fresh Goldens (#129) * ci(regen): add spec_glob + tolerate_failures inputs to baseline regen Currently the workflow runs every spec under e2e/0*.spec.ts e2e/1*.spec.ts unconditionally and the whole job aborts (set -e) the moment any spec fails. When the only thing that needs regenerating is the wallet header — and an unrelated spec (e.g. 07-send) is flaky on DEV — the regen can't even reach its commit step. The fix that needs the baselines gets stuck behind a problem it has nothing to do with. Two new workflow_dispatch inputs: - `spec_glob` (default unchanged) — narrows the run to specific files. e.g. `e2e/02-create-seed.spec.ts e2e/03-restore-seed.spec.ts e2e/04-unlock-password.spec.ts` only refreshes the wallet headers. - `tolerate_failures` (default false, opt-in) — appends `|| true` to the playwright invocation. `--update-snapshots=all` still writes new PNGs for the specs that ran; the commit step inspects `git status` and commits only files that actually changed. Partial regens are safe. Defaults preserve current behaviour — full suite, fail-fast — so existing routine calls keep working. * fix(errorMessages): family pattern matching for diagnostic node error strings The node API emits diagnostic, field-naming error strings (e.g. "account_address is not valid hex", "recipient must be 32 bytes (64 hex chars)", "Failed to broadcast commitment inscription on-chain") that did not match the exact-string lookup table on the app side, so users saw the raw "Serverfehler <status>: ..." fallback instead of the translated message. Adds a second lookup layer SERVER_ERROR_PATTERNS (regex) that runs only when the existing exact-match pass misses. KNOWN_SERVER_ERRORS stays unchanged so the lockstep test with api_remote.rs::error_strings_match_known_app_mapping keeps holding. Patterns are ordered specific-before-generic and intentionally do not overlap with any KNOWN_SERVER_ERRORS string — exact-match always wins. Three new test cases in error-mapping.test.ts cover one variant per pattern, including an address-length variant with a different field name to prove the family (not a specific field) is matched. * fix(wallet): case-insensitive network check for faucet gating The node reports `network` as a display string ("Mainnet", "Mutinynet", …) — see `node::router::info_handler`. The existing check used a case-sensitive comparison, so `"Mainnet" !== "mainnet"` evaluated true and the faucet button rendered on Production. Lowercase the network name before the comparison so a casing decision on the server can't accidentally re-enable mint on Mainnet. Add a WalletScreen unit test for both branches (Mainnet hidden, Mutinynet visible). Update the `06-balance-zero-empty-banner` e2e spec to reflect testnet reality (faucet button is the primary CTA in the empty-wallet banner on Mutinynet); the Mainnet-hidden case is covered at the unit-test layer since E2E always runs against the testnet API. * test(e2e): regenerate visual baselines * test(e2e): raise send-success timeout to account for post-#127/#129 latency The send-flow now does an extra `/api/balance` round-trip before signing (`num_sends` hydration per the thin-client rule in CONTRIBUTING.md), and the matching server-side writes from zk-coins/node #129 / #132 add a few seconds of legitimate per-send latency for the atomic `Account.num_sends` + `commitment_public_key` upsert. Observed wall-time on the bundle PR's CI runs is 180-220 s, which the previous 180 s test cap was treating as a deadlock. Lift the waitFor cap from 150 s to 250 s and the overall test cap from 180 s to 300 s. The race against the inline error banner stays in place so a genuine server-side failure still surfaces with its real message instead of timing out. * test(e2e): raise send-success cap to 480 s for Mutinynet jitter + parallel-load The 300 s cap from the previous commit was still insufficient on the bundle PR's CI. Empirically the send-success spec lands between 180 s and ~5 min on DEV. The variance is driven by: * Publisher track-tx WS jitter — 30 s timeout per attempt, up to three attempts under load when Mutinynet block-event delivery stalls (see `node/src/publisher.rs::TRACK_TX_TIMEOUT_SECS`). * Playwright `fullyParallel: true` saturates the shared DEV node with mint + balance traffic from neighbour specs while 07-send-success is its only real-send test, but everything else keeps hammering /api/balance, /api/info, and (via globalSetup) /api/mint — node mutex contention pushes the lone send past its happy path. Raise the spec cap to 480 s and the waitFor cap to 420 s. Deadlock still surfaces; well-behaved-but-slow runs no longer mask as a regression. Follow-ups (out of scope for this PR): (a) tighten the publisher's track-tx budget, (b) consider serializing the mutation-heavy specs against the DEV node. * test(e2e): harden globalSetup against cold-start navigation timeouts The CI runner's first page.goto('/') after boot occasionally exceeds Playwright's default 30 s navigation timeout — Cloudflare cold path, Next.js standalone first-paint, and the WASM bundle handshake all stack on the very first request. Subsequent navigations in the same context are fast. - Raise the context-wide navigation timeout to 90 s for both wallet- bootstrap contexts. Scoped to globalSetup only so test specs keep the 30 s default and a genuine app-side regression still surfaces as a timeout. - Wrap each wallet bootstrap (clearWalletState + createSeedWallet) in a single retry on a fresh page so a transient first-paint timeout doesn't fail the whole CI run. - Fix a TS strictness regression in 07-send.spec.ts where textContent() returns string | null and was passed directly to expect's message slot (string | undefined). * test(e2e): serialize 07-send + raise send-success cap to 12 min The 480 s cap introduced in 3abecfa was still consistently hit by the real Send through DEV under the suite's parallel load. Both attempts (test + retry) timed out, with the waitFor(420 s) draining before the test cap. Empirically the DEV Send takes 180 s to ~7 min depending on Mutinynet block-time jitter plus neighbouring specs hitting the node. Raising the cap is one half of the calibration; the other half is reducing load: - on the file keeps the 13 in-file tests in a single worker so two 07-send tests can no longer fight each other for DEV bandwidth. - (12 min) and the waitFor budgets raised to 660 s (11 min). Setup overhead (login + balance hydration) lives in the remaining 60 s of headroom. A genuine deadlock still surfaces in 11 min instead of 7. The follow-up to tighten publisher track-tx is unchanged. * ci(e2e): give send-success exclusive DEV-node bandwidth via two-step job Three consecutive E2E runs failed on `07-send-success` at progressively larger test-timeout caps (180 s -> 300 s -> 480 s -> 720 s). Every time the DEV node's single proof-gen pipeline was starved by parallel mint and balance traffic from the rest of the suite (09 / 10 / 11), pushing the real Send wall-clock above the cap. Increasing the cap further is the wrong direction. Structural fix: split the existing E2E step in two. Step 1: `npx playwright test --grep-invert "send-success"` -- the parallel-safe bulk of the suite (75 specs) running with the default fullyParallel + 2 workers. Step 2: `npx playwright test --grep "send-success" --workers=1` -- the one mutating Send test with exclusive DEV-node bandwidth, no competing proof-gen / balance traffic from neighbouring specs. Side fixes: - Drop `test.describe.configure({ mode: 'serial' })` from the spec. Serial mode caused a cascade-retry failure where send-no-funds-banner re-ran on the group retry and failed for an unrelated reason. - Lower send-success cap back to 6 min (test) + 5 min (waitFor) since exclusive bandwidth drops the realistic upper bound to ~3-4 min. globalSetup runs once per playwright invocation, so each step gets a clean fresh-Alice + fresh-Bob fixture set independent of the other. * fix(e2e): guard error.textContent() behind race-winner check ROOT CAUSE of all `07-send-success` failures since PR #104. The test races the success heading against the inline error banner via `Promise.race`, then resolves both: await error.textContent().catch(() => null) The catch handles a *rejected* promise, but Playwright's `Locator.textContent()` does not reject when no element matches — it WAITS for the element to appear, blocking up to the test-timeout. On the success page (the happy path) `send-error` is not in the DOM, so textContent never resolves and the assertion at line 216 sat spinning until the 360 s test cap fired, even though the success heading had rendered minutes earlier. Failure-screenshot evidence from run 26651174055: the page clearly shows "Sent privately" with proof #183 / #184 in both attempt 1 and the retry. The Send succeeded server-side; only the post-race assertion blocked. Fix: tag which branch of the race won, then act only on that branch. The error branch resolves textContent (which is safe because the locator just became visible). The heading branch skips the error check entirely. This was the intent of the race-against-error pattern from the start — the implementation just resolved the error too eagerly. Side-effect: with this fix the previous timeout-budget escalations (180 s -> 300 s -> 480 s -> 720 s -> 360 s) were all chasing the wrong cause. The 360 s cap stays — it's a reasonable upper bound for a real DEV-node Send, but the test now resolves as soon as the success heading appears. * test(e2e): refresh 07-send goldens for post-textContent-fix flow The 07-send-success golden was generated against the broken textContent hang (test never reached the snap() call, so no PNG was ever captured). With the race-winner guard in a15cc24 the test now actually takes a fresh screenshot of the success page and compares it to the on-branch golden, which had drifted with the post-2026-05-23 header/balance layout pass. Includes three other 07-send goldens that the same regen run picked up (confirm-dialog desktop/mobile + recipient-valid-hex) — the maxDiffPixelRatio: 0.01 tolerance had been masking the same drift under the threshold; refreshing them now keeps the diffs at 0 px for any future regression that crosses the threshold. Regen run: 26652625153 against chore/all-app-fixes-bundle. * fix: address subagent review findings MAJOR (security): - regenerate-visual-baselines.yml — command injection via `${{ inputs.spec_glob }}` interpolated into a shell string and `eval`'d. Replaced with env-var passthrough + array-form CMD, no eval. Glob expansion still works ($SPEC_GLOB unquoted), but user-controlled metachars are now data, not parser tokens. CWE-78. MINOR: - errorMessages.ts — doc claim "patterns must NOT overlap with KNOWN_SERVER_ERRORS" was wrong (overlap exists today, exact-first shadows it harmlessly). Rewrote the comment to reflect reality and added the test that the comment now references: `every KNOWN_SERVER_ERRORS string resolves via the exact-match table, not via a pattern`. Catches a future refactor that deletes an exact entry and lets a pattern silently swap the German copy. Also exported SERVER_ERROR_PATTERNS + SERVER_ERROR_TO_USER_MESSAGE so the test can introspect them. - _global-setup.ts::withPageRetry — page leaked on the success path (no close after `return await step(page)`) and on the terminal throw. Moved close into a `finally` block. - ci.yaml::e2e-tests — added `timeout-minutes: 30` so a hung browser process can't sit on the default 6 h budget. * fix(errorMessages): trim dead-code assertion + revert unused export Subagent re-review NITs: - error-mapping.test.ts had a tautological inner assertion (userMessageFor always returns the exact entry first, so the conditional check could never fire). Line 76's .toBeDefined() already enforces the real lockstep invariant — trim the rest. - SERVER_ERROR_PATTERNS no longer needs export now that the test doesn't consult it. Reverted to module-private. - SERVER_ERROR_TO_USER_MESSAGE stays exported (the test needs it), marked @internal so production callers stick to userMessageFor(). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Automatic Release PR
Commits: 1 new commit(s)