Skip to content

test(router): align mint error tests with post-Phase-E flow - #111

Merged
TaprootFreak merged 7 commits into
developfrom
fix/test-error-string-post-phase-e
May 25, 2026
Merged

test(router): align mint error tests with post-Phase-E flow#111
TaprootFreak merged 7 commits into
developfrom
fix/test-error-string-post-phase-e

Conversation

@TaprootFreak

Copy link
Copy Markdown
Contributor

Summary

Heavy-CI hotfix. Release PR #18 (develop → main) Coverage Gate + Node + Shared Tests both failed on mint_commit_tx_failure_returns_503 after PR #110 (Phase E) landed. The test asserted a pre-Phase-E error string that the new mint flow no longer produces with the dead_pool it uses.

Root cause

Pre-Phase-E the FIRST DB write in mint_handler was db::commit_mint_tx. After PR #107 the FIRST DB write is the publisher's pending_inscriptions(constructed) INSERT. With the lazy dead_pool the failure now surfaces with the publisher-wrapped string "Failed to broadcast mint inscription on-chain", not the deeper "Failed to persist mint commit transaction" that the test was pinned to.

Fix

  • Rename mint_commit_tx_failure_returns_503mint_pending_inscriptions_persist_failure_returns_503. Update assertion + doc-comment to match what the test actually exercises (publisher persistence layer failure).
  • Add mint_commit_mint_tx_failure_returns_503 to restore branch coverage of the post-broadcast commit_mint_tx Err path (router.rs ~1191). Uses a live Postgres testcontainer + a BEFORE INSERT trigger on the accounts table; pending_inscriptions persist, broadcast, in-memory state.update, and persist_state_and_mark_complete_tx all succeed, only the final commit_mint_tx raises, handler returns 503 with the original error string.

Local checks (all green)

  • cargo fmt --all --check
  • cargo clippy -p node --all-features --tests -- -D warnings

Test plan

  • CI Lint & Build green
  • ci:full label: Node + Shared Tests + Coverage Gate green (these are the failing ones today on the Release PR)

- Rename mint_commit_tx_failure_returns_503 →
  mint_pending_inscriptions_persist_failure_returns_503 to reflect
  post-PR-107/110 reality: dead_pool fails at the publisher's
  pre-broadcast pending_inscriptions INSERT, not at commit_mint_tx.
  Assertion now matches the wrapped error string.

- Add mint_commit_mint_tx_failure_returns_503 to restore branch
  coverage of the post-broadcast commit_mint_tx Err path. Uses a
  live Postgres testcontainer + a BEFORE INSERT trigger on the
  accounts table; everything earlier in the mint flow succeeds,
  only the final accounts upsert raises, and the handler returns
  503 with the 'Failed to persist mint commit transaction' body.
@TaprootFreak TaprootFreak added the ci:full Trigger heavy CI jobs (Server + Shared Tests + Coverage Gate, ~60-90 min on M3 Ultra) label May 25, 2026
The accept loop handled one connection then slept 60s before
accepting the next, which serialised sequential mint tests behind
the previous mint's keepalive sleep. The second connect_async hit
its 15s timeout and the mint flow returned 503 'WS connect failed'.

Phase E surfaced this because it added the first multi-mint-per-
test scenario (mint_handler_two_sequential_mints_with_different_recipients_advance_cleanly).
Pre-Phase-E every test ran exactly one mint so the bug was hidden.

Fix: spawn a tokio task per accepted connection so the accept loop
keeps draining new connects in parallel with the existing 60s
keepalive.
…t test

The existing race test relied on prepare_mint (~200ms in test build)
being slow enough for the test thread to acquire the SMT lock and
insert pk_0 between phase 2 entry and the phase-3 re-derive. Under
CI load this race was lost intermittently — phase 3 ran first, saw
no pk_0, proceeded to broadcast, returned 'Failed to broadcast mint
inscription on-chain' instead of 'Concurrent mint detected'.

Fix: add a second cfg(test)-only Notify (phase3_release) that the
handler awaits between prepare_mint and the phase-3 re-derive. All
test_state constructors pre-arm it so production-shaped tests
proceed immediately. The concurrent-mint race test drains the
pre-armed permit before spawning, then notify_one()s after injecting
pk_0 — a hard happens-before edge that no timing variance can lose.

This eliminates the flake observed on develop's heavy CI run
26411938492 Coverage Gate.
The previous Notify-based hold pre-armed exactly ONE permit. After the
first mint consumed it, subsequent mints in the same process (e.g.
mint_handler_two_sequential_mints_with_different_recipients_advance_cleanly)
blocked forever — the test was observed SLOW [>720s] in the heavy
gate's nextest run before timing out.

A tokio::sync::Mutex<()> is the correct primitive: pre-unlocked,
handler acquires + drops in one step for production-shaped tests
(non-blocking), and the concurrent-mint race test holds the guard
across pk_N injection then drops it. Reusable for any number of
sequential mints without manual re-arming.
The three `i64::try_from(leaf_index)` sites in db.rs and the
`debug_assert!` in state.rs::load_from_pg all guard a hypothetical
> i64::MAX (or 32-bit usize-overflow) condition that cannot fire on any
target we ship (Linux x86_64 / aarch64). The error/panic arms were
flagged by the 100% Coverage Gate as uncoverable, contributing
13 uncovered lines for zero production benefit.

Replace each `try_from + sqlx::Error::Encode` with a direct `as i64`
cast (and `as usize` in state.rs), keeping a one-line invariant comment
that documents the bound. No behaviour change on 64-bit targets, which
is the only deployment target.

Affected sites:
- db::persist_state_tx
- db::persist_state_and_mark_complete_tx
- db::insert_root_index
- state::State::load_from_pg
…t return

`create_and_broadcast_inscription` returned `Result<Option<(Txid, Txid)>>`
but the body has no path that yields `Ok(None)` — every success arm
builds `Ok(Some((c, r)))` and every failure surfaces as `Err`. The dead
`Ok(None)` arm in `router::mint_handler` together with the defensive
`if let Some(ctxid) = commit_txid_bytes { .. } else { .. }` fallback
contributed 13 uncovered lines to the Coverage Gate.

Flatten the API to `Result<(Txid, Txid), ...>`:
- publisher.rs: return tuple directly on Ok.
- router.rs: match yields `[u8; 32]` instead of `Option<[u8; 32]>`;
  collapse the `if let Some(ctxid) { .. } else { .. }` to a single block.
- publisher_tests.rs: drop the now-redundant `.expect("Some((c,r))")`
  unwrap layer at three call sites.

No behaviour change — the removed branches were unreachable.
The phase-3b state-advance Err arm in `mint_handler` (the 503 returned
when `update_and_snapshot_for_persist` fails on an SMT
key-collision-with-different-value) was uncovered by the 100% Coverage
Gate. The race that produces this state in production needs two
concurrent mints whose phase-2 re-derives BOTH pass and whose
broadcasts both land before either state lock acquires — too brittle
to reproduce deterministically with two real requests.

Add a deterministic test that exercises the same code path:

- Mirror `phase3_release_lock` with a new `state_advance_release_lock`
  test-only AppState field. The handler acquires + immediately drops
  it AFTER `create_and_broadcast_inscription` returns and BEFORE the
  phase-3b state lock. Production builds compile this out entirely
  (both the field and the hold point are `#[cfg(test)]`).

- The new test holds the guard across a colliding-SMT injection at
  `pk_0`. When the guard drops, the handler's `state.update` observes
  the collision and returns 503 with the documented
  "in-process state advance failed" reason. Asserts also confirm the
  pending row stays at `reveal_broadcast`, the on-disk SMT/MMR/
  root_index DID NOT advance (no atomic persist tx ran), and the
  scanner-replay path stays armed.
@TaprootFreak
TaprootFreak merged commit f3c7246 into develop May 25, 2026
7 checks passed
@TaprootFreak
TaprootFreak deleted the fix/test-error-string-post-phase-e branch May 25, 2026 21:03
TaprootFreak added a commit that referenced this pull request May 25, 2026
)

deploy_dev_node + deploy_prd_node steps hit `client_loop: send
disconnect: Broken pipe` exit 255 when `docker compose recreate`
runs longer than the cloudflared tunnel's idle timeout (~60s with no
remote stdout). The host-side deploy script keeps running and the
container comes up correctly, but CI marks the step failed.

Add ServerAliveInterval=30 + ServerAliveCountMax=8 — keepalive every
30s, 8 retries before declaring dead = 4 min of network silence
tolerated. The deploy script's longest silent stretch (`docker
compose pull` of a fresh image + container recreate) is bounded by
the host's network + disk speed, comfortably under that ceiling.

Observed on the PR #111 merge auto-deploy: run 26419696840 failed
with the broken-pipe pattern, but ssh dfxdev-remote showed the
container was already `Up (healthy)` — purely a CI-side artefact.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:full Trigger heavy CI jobs (Server + Shared Tests + Coverage Gate, ~60-90 min on M3 Ultra)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant