test(payments/transfer): close V6-RECOVER test-coverage gap + nightly soak CI (Audit #333 follow-up)#358
Merged
Conversation
2 tasks
1d8d5d8 to
bd31d05
Compare
ecece6c to
f88293f
Compare
5 tasks
bd31d05 to
c7d2f27
Compare
f88293f to
cd2bc84
Compare
c7d2f27 to
d4efc06
Compare
cd2bc84 to
c404e86
Compare
d4efc06 to
06e0572
Compare
c404e86 to
370f602
Compare
06e0572 to
5d624ea
Compare
370f602 to
6948091
Compare
5d624ea to
3d725b9
Compare
6948091 to
d7ef54e
Compare
3d725b9 to
43ada2d
Compare
d7ef54e to
56e5f9b
Compare
43ada2d to
c211530
Compare
56e5f9b to
fa46ca7
Compare
c211530 to
654edea
Compare
fa46ca7 to
be939f2
Compare
be939f2 to
d04fa54
Compare
5 tasks
…ER test-coverage gap The soak (manual-test-full-recovery.sh) routinely surfaces `[ERROR] [V6-RECOVER] Stranded receive <hex> hit permanent recipient- address mismatch (HD-index recovery exhausted) (no retry): VerificationError: Recipient address mismatch` at §C → §D. The existing unit tests cover helpers in isolation (PaymentsModule.recipient-address-mismatch-recovery.test.ts) and the error CLASSIFIER (PaymentsModule.proof-polling-persistence.test.ts #269), but neither proves the composition: "given a sibling HD index in the recipient's inventory, tryRecover finds it AND the SDK's verifyRecipient agrees on the derived address". This left CI blind to the regression mode where tryRecover returns a false-positive candidate that Token.update later rejects. This commit closes the gap at three layers: L1. Real-SDK integration test (tests/integration/payments/v6-recover-real-sdk-recovery.test.ts): - Replaces the file-level vi.mock for SigningService / UnmaskedPredicate / TokenId / TokenType / HashAlgorithm with REAL SDK imports. - Builds two real HD-derived signing services. - Constructs the sender-targeted DIRECT address via real UnmaskedPredicate.create → reference.toAddress (the EXACT path the SDK's verifyRecipient uses). - Drives `tryRecoverSigningServiceForRecipient` against real TokenId / TokenType / Uint8Array salt. - 4 tests: happy path, no-match negative, deriveAddressInfo throws, and (L2 composition) tryRecover + predicate construction round-trip equals the transferTx target address. L3. CI workflow (.github/workflows/soak-nightly.yml): - Nightly cron + workflow_dispatch trigger. - Probes testnet aggregator + Nostr relay; skip-not-fail when either is unreachable (third-party outages cannot block releases). - Checks out sphere-cli alongside sphere-sdk, links the local build, runs manual-test-full-recovery.sh with SPHERE_DEBUG=*. - Captures soak.log + workspace as artifacts (30-day retention on failure, 7-day on success). - Surfaces metrics in the workflow summary: V6-RECOVER count, Stranded receive count, POINTER_MONOTONICITY_VIOLATION count, bcast_pub>0 count, exit code. What's tested by what (audit roll-up): - SDK address-derivation regression in tryRecover → L1 (real SDK). - tryRecover ↔ predicate-construction composition → L1 composition test (proves verifyRecipient cannot throw against the recovered state). - Cross-process / Nostr / IPFS / multi-daemon regressions → L3 soak. - Helper-level edge cases (mock-friendly) → existing PaymentsModule.recipient-address-mismatch-recovery.test.ts. - Error-classification on hard fail → existing #269 test in PaymentsModule.proof-polling-persistence.test.ts. Tests pass: 4/4 new integration tests + all 8185 unit tests. tsc clean. Refs: #333 (V6-RECOVER test-coverage gap, follow-up to H1-H7 stack). Stacked on #355 (H7) → #354 (H5) → #353 (H4) → #352 (H3) → #351 (H2) → #349 (H1) → #348 (C3) → #347 (C2) → #346 (C1).
d04fa54 to
7508d1c
Compare
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.
Why this PR
While running a verbose-debug soak on
integration/all-fixes-vintage code I observed 100+[ERROR] [V6-RECOVER] Stranded receive ... permanent recipient-address mismatchlines in the §C → §D handoff. Investigating: the soak still passed (exit 0), but the question came up — why doesn't any unit/e2e test catch this regression mode?Reviewing the test coverage:
PaymentsModule.recipient-address-mismatch-recovery.test.ts(12 tests)PaymentsModule.proof-polling-persistence.test.ts#269 (1 test)finalizeTransferTokento throw; asserts the throw is classified as permanenttryRecoverSigningServiceForRecipient+ real SDK predicate constructionSo the regression mode "
tryRecoverreturns a candidate but the SDK'sverifyRecipientlater rejects it" was structurally invisible to CI. The soak surfaces it, but only on a developer's laptop.What this PR adds
L1 — Real-SDK integration test
tests/integration/payments/v6-recover-real-sdk-recovery.test.ts(4 tests):tryRecoverfinds it AND the recovered signer'sUnmaskedPredicate.create(...).getReference().toAddress()(REAL SDK call, no mock) derives the exact same address the sender computed.tryRecoverreturns null (no false-positive candidate that the SDK would later reject).deriveAddressInfo()throws for one index; the loop continues and finds the match further down.finalizeTransferTokenintegration at the address layer: primary derived ≠ expected →tryRecover→ realUnmaskedPredicate.createfrom recovered signer → the resulting recipient address byte-equals the transferTx target. Proves the SDK's downstreamverifyRecipientcannot throw "Recipient address mismatch" against this state.L3 — Nightly CI workflow
.github/workflows/soak-nightly.yml:schedule(06:00 UTC nightly, off-peak for testnet) +workflow_dispatch(on-demand triage).spherebinary discoverable on PATH.manual-test-full-recovery.shwithSPHERE_DEBUG=*, captured tosoak.log.L2 —
resolveUnconfirmedend-to-endRather than write a separate heavyweight test that constructs a full SDK Token instance, I folded the L2 value into the L1 file as the composition test above. The composition test exercises steps 3-5 of
finalizeTransferTokenwith real SDK objects, which is structurally equivalent to drivingresolveUnconfirmed→resolveLegacyReceivedTokenViaGetProof→onProofReceived→finalizeStrandedReceivedTokenat the address-derivation layer — the layer where the V6-RECOVER error originates.The full
stClient.finalizeTransactionround-trip (state + proof verification) requires constructing real SDKToken+TransferTransactioninstances with real authenticated proofs — that's L3's job (the soak does the full thing for real).Soak confirmation
While developing this PR I ran one full soak with
SPHERE_DEBUG=*:EXIT=0(pass), 16 section banners reached, all §A/§B/§C/§D/§E assertions cleared.Test plan
js-yaml+python3 yaml.safe_load)tsc --noEmitcleaneslintclean on the new filemanual-test-full-recovery.sh) exits 0 against the current branch stateAudit traceability