Skip to content

chore: bundle App fixes — Bug #1 + errorMessages + CI tolerate_failures + fresh Goldens - #129

Merged
TaprootFreak merged 13 commits into
developfrom
chore/all-app-fixes-bundle
May 29, 2026
Merged

TaprootFreak merged 13 commits into
developfrom
chore/all-app-fixes-bundle

Conversation

@TaprootFreak

@TaprootFreak TaprootFreak commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Single PR that bundles the App-side fixes plus a Goldens refresh, so the App can ship them as one atomic update with green CI.

What's included

  1. ci(regen): add spec_glob + tolerate_failures inputs to baseline regen — from PR ci(regen): add spec_glob + tolerate_failures inputs to baseline regen #115. Makes the baseline-regen workflow robust: partial refreshes (some specs fail) commit the goldens that DID regenerate, instead of all-or-nothing.

  2. fix(errorMessages): family pattern matching for diagnostic node error strings — from PR fix(errorMessages): family pattern matching for diagnostic node error strings #124. App now matches the node's diagnostic per-field error strings ("account_address is not valid hex", "must be 32 bytes …", "Failed to broadcast …on-chain") via regex fallback after the existing exact-match table.

  3. fix(wallet): case-insensitive network check for faucet gating — from PR fix(wallet): case-insensitive network check for faucet gating #126. The node reports network: "Mainnet" (CamelCase); the App's case-sensitive !== 'mainnet' evaluated true → faucet button rendered on Production. Lowercases the comparison so a casing decision on the server can't re-enable mint on Mainnet. Adds unit tests for both branches; updates 06-balance-zero-empty-banner E2E spec to reflect that on testnet the faucet button IS the empty-wallet CTA.

  4. test(e2e): regenerate visual baselines — fresh chromium-linux PNGs for the specs whose layout drifted since 2026-05-23. The 07-send goldens picked up a final refresh after the textContent fix below (the success page was unreachable before, so the golden was effectively stale).

  5. test(e2e): harden globalSetup against cold-start navigation timeouts — the CI runner's first page.goto('/') after boot occasionally exceeded Playwright's default 30 s navigation timeout. Context-wide nav timeout 90 s + single retry on a fresh page, scoped to globalSetup only.

  6. ci(e2e): give send-success exclusive DEV-node bandwidth via two-step job — split the E2E step in two. Step 1 runs the parallel-safe bulk of the suite (75 specs, ~3 min). Step 2 runs only send-success with --workers=1, so the one real Send through DEV doesn't compete with parallel mint/balance traffic from 09 / 10 / 11.

  7. fix(e2e): guard error.textContent() behind race-winner check — root cause of all 07-send-success failures since PR fix(e2e): 07-send 'send-success' — bump timeouts + race against error #104. The race-against-error pattern resolved error.textContent() unconditionally, but Playwright's Locator.textContent() blocks waiting for the element to appear instead of returning null. On the success page (the happy path) send-error doesn't exist, so the assertion sat spinning until the test cap fired, even though the success heading had rendered minutes earlier. Fix: tag the race winner and only resolve textContent() on the error branch.

Why one bundle PR

Bug #2 (the prove_account_update failed regression) was fixed in PR #127 and is already merged + deployed; with #127 live, all remaining changes here are independent and small. Bundling them lets us go from "CI red across every App PR" to "develop is green and ready for a Release PR" in a single merge.

Test plan

  • npx vitest run — 323 passed (+3 skipped contract.live)
  • npx next lint, npx prettier --check "src/**/*.{ts,tsx}", npm run build clean
  • Baselines regenerated for the drifted specs
  • CI fully green — 10/10 checks SUCCESS on commit 3dc332b

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.
… 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.
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.
@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

[OK] Button-Inventory-Audit — all clear

Checked 71 testid(s) in src/ against 69 reference(s) in e2e/. Nothing to do.

…atency

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.
…allel-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.
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).
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.
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.
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.
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.
@TaprootFreak
TaprootFreak marked this pull request as ready for review May 29, 2026 17:50
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.
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().
@TaprootFreak
TaprootFreak merged commit a938593 into develop May 29, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant