Skip to content

feat(coverage): enable 80% coverage gate via 28+ new tests (Story 15)#38

Merged
RBKunnela merged 1 commit into
mainfrom
feat/story-15-coverage-gate-v2
May 22, 2026
Merged

feat(coverage): enable 80% coverage gate via 28+ new tests (Story 15)#38
RBKunnela merged 1 commit into
mainfrom
feat/story-15-coverage-gate-v2

Conversation

@RBKunnela
Copy link
Copy Markdown
Owner

Summary

Closes Task #15. Enables the 80% coverage gate in CI on paybot-sdk by adding 31 new tests across two modules and wiring npm run coverage into the CI build matrix. Unblocks the 0.3.0 release (Story 14 §Out of Scope).

This PR builds on Story 15.1 (PR #37 @ 23694dd), which fixed the EIP-55 checksum and 0x-prefix bugs that the first Story 15 attempt surfaced in src/micropayment-engine.ts. Without the 15.1 fix, Track B (the new micropayment-engine.test.ts suite) could not run.

Why this matters

vitest.config.ts defined coverage thresholds at 80/80/70/80 since project start, but npm test (what CI runs) never enforced them. Two large untested surfaces blocked the gate from being turned on:

Module Before After Δ
src/x402-v2.ts 63.74% 97.32% +33.58
src/micropayment-engine.ts 0% 100% +100
Global lines 69.93% 97.52% +27.59

Per-file detail (vitest v8 reporter, on this commit):

File               | % Stmts | % Branch | % Funcs | % Lines
-------------------|---------|----------|---------|--------
All files          |   97.52 |    86.75 |     100 |   97.52
 client.ts         |   95.16 |    83.45 |     100 |   95.16
 crypto.ts         |     100 |      100 |     100 |     100
 errors.ts         |     100 |      100 |     100 |     100
 micropayment-...  |     100 |     94.2 |     100 |     100
 middleware.ts     |     100 |       90 |     100 |     100
 networks.ts       |     100 |      100 |     100 |     100
 x402-handler.ts   |   98.47 |    79.16 |     100 |   98.47
 x402-v2.ts        |   97.32 |       86 |     100 |   97.32

npm run coverage now exits 0 locally and (expected) in CI.

What's included

Track A — tests/x402-v2.test.ts (extended; tests #14-#26)

13 new tests on previously-untested public surface. Reuses Story 14's determinism scaffolding (fake timers + module-mocked generateEIP3009Nonce):

  • on402Response: happy / non-402 status / missing+malformed header (covers private extractPaymentIntentHeader + parsePaymentIntent + parsePaymentResponseBody transitively)
  • submitPayment: 200 OK with full Receipt parse / non-2xx error rewrap / network error rewrap (fetch stubbed via vi.stubGlobal)
  • verifyReceipt: 200 OK + verified:true / non-2xx returns false / network error returns false without throwing (locks the differing contract from submitPayment)
  • createPaymentIntentHeader: happy round-trip / optional merchant+meta undefined
  • negotiatePaymentIntent: deterministic intent_<ts>_<rnd> shape / TODO branch locked (currently always returns protocol='dual', ignoring _supportedProtocols — flagged by Story 14 QA, deferred per @sm)

Track B — tests/micropayment-engine.test.ts (new file; B-1 through B-18)

18 new tests covering all 7 public methods of MicropaymentEngine + transitive private helpers:

  • constructor (B-1..B-3): 0x-prefix guard / default thresholds (60s/100/$1.0) / custom thresholds honored
  • queuePayment (B-4..B-6): paymentId shape mp_<ts>_<rnd> with deterministic Math.random / usdToBaseUnits regex errors (empty / non-numeric / leading-dot) / auto-settle triggered when thresholds met
  • batchPayments (B-7..B-9): EIP-712 BatchSettlement signing produces a 130-hex-char signature + correct totals/recipientCount/expiresAt + 'pending' status mutation / missing-IDs throw / skipGasEstimate: true short-circuits to '0.000000'
  • getGasEstimate (B-10..B-11): 6-decimal USD string / inverse scaling with paymentCount
  • setBatchWindow (B-12..B-13): seconds→ms conversion / observable window-boundary change via vi.advanceTimersByTime
  • getQueueStatistics (B-14..B-15): empty-queue zeroes / multi-window aggregation + per-window auto-settle contract (only current windowKey batches)
  • clearOldPayments (B-16..B-17): nothing-to-clear returns 0 / removes only status==='settled' items older than cutoff and deletes empty window keys
  • getPaymentStatus (B-18): find-or-undefined contract across multiple windows

Track C — .github/workflows/ci.yml

Two new steps inside the existing build matrix job, between Run tests and Build:

  1. Coverage gate — runs npm run coverage (enforces 80/80/70/80 thresholds)
  2. Upload coverage reportactions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 (v4, pinned per supply-chain hygiene policy from PR ci(security): add CodeQL, Dependabot, and OSV-Scanner bundle #11), if: always(), 14-day retention, named coverage-report-node-${{ matrix.node-version }}

The coverage step runs inside the existing matrix job, so the GitHub check context name remains build (18) / build (20) — no new required_status_checks entry expected. @devops to confirm post-merge.

Test count breakdown

Source Count Notes
Baseline (pre-Story-15) 115 102 pre-Story-14 + 13 Story-14
Track A — new tests #14-#26 +13 Extends existing tests/x402-v2.test.ts
Track B — new B-1..B-18 +18 New file tests/micropayment-engine.test.ts
Total 146 all PASS, zero skipped

How the previously-failing tests now pass

The first Story 15 attempt produced 5 failing Track B tests, all rooted in two source bugs:

  1. Primary: src/micropayment-engine.ts:232 verifyingContract had wrong EIP-55 checksum → viem 2.49+ rejected.
  2. Secondary: generateNonce() returned 64 hex chars without 0x prefix → mismatch with the 0x${string} cast.

Both fixed by Story 15.1 (PR #37 @ 23694dd). The 5 previously-failing tests (B-3, B-6, B-7, B-9, B-15) now exercise the full signing path successfully against the corrected source.

One additional fixture adjustment in this PR (NOT a source change):

  • RECIPIENT_B test fixture corrected from '0x...FACE' (wrong checksum) to '0x...Face' (viem-validated). This exposed Story 15.1's deferred backlog item — line 240 of src/micropayment-engine.ts is an undefended recipient as 0x${string} re-cast, meaning caller-supplied recipient addresses must be EIP-55 valid until the source-side guard lands. Test fixtures now respect this constraint; the broader source hardening is on the post-Story-15 backlog per @dev (Story 15.1 implementer).

Acceptance Criteria mapping

# AC Status Evidence
1 src/x402-v2.ts ≥80% line coverage PASS 97.32% (see coverage table)
2 src/micropayment-engine.ts ≥80% line coverage, all 7 public methods PASS 100% lines; B-1..B-18 cover constructor + queuePayment + batchPayments + getGasEstimate + setBatchWindow + getQueueStatistics + clearOldPayments + getPaymentStatus
3 tests/micropayment-engine.test.ts exists as new file PASS 663 LOC, this PR
4 npm run coverage exits 0 PASS Verified locally; CI run on this PR will confirm
5 .github/workflows/ci.yml Coverage gate step in build matrix PASS Between Run tests and Build, this PR
6 Coverage report uploaded as CI artifact PASS actions/upload-artifact@ea165f8 v4, if: always(), 14-day retention
7 required_status_checks update PENDING @devops — likely no new context needed (coverage runs inside existing build (18) / build (20))
8 All 115 existing tests still pass PASS 115 baseline preserved byte-for-byte; total run 146 PASS
9 No skipped/.todo/xit/xdescribe PASS grep clean
10 vitest.config.ts thresholds NOT lowered PASS File untouched (git diff --stat)

Files changed (3)

 .github/workflows/ci.yml          |  11 ++
 tests/x402-v2.test.ts             | 427 ++++++++++++++++++++
 tests/micropayment-engine.test.ts | 663 ++++++++++++++++++++++++ (new)

Source-file modification policy honored: zero changes to src/ or vitest.config.ts. Confirmed via git diff --stat origin/main from this branch.

Test plan

  • npm test — 146/146 PASS, zero skipped, zero .todo
  • npm run coverage — exit 0; global ≥80% lines/functions/statements, ≥70% branches
  • npm run lint — clean
  • npm run type-check — clean
  • No source modifications (git diff --stat: only tests + ci.yml)
  • No deleted tests
  • Determinism: every test uses fake timers + frozen Math.random or stubbed fetch; no real network, no Date.now leakage
  • CI green on both Node 18 + Node 20 matrix entries — pending GH Actions run
  • @qa 12-check matrix PASS (2-pass discipline)

Chain governance

Full SINKRA — story-driven:

DO NOT MERGE. Per automated-pr-merge-authority.md: this PR awaits @qa PASS verdict, then @devops clicks merge. Per Orion's pre-approval pattern for Story 15: once @qa issues PASS, @devops merges directly without re-asking operator.

Out of scope (unchanged from Story 15 spec)

  • Releasing paybot-sdk@0.3.0 — happens AFTER this PR lands.
  • Hardening the recipient as 0x${string} cast on line 240 — Story 15.1 deferred to backlog.
  • Honoring _supportedProtocols in negotiatePaymentIntent — Story 14 QA TODO, separate future story.
  • Mutation testing / property-based testing — future hardening.
  • Integration tests against real network — fetch is stubbed throughout.

…ory 15)

Closes Task #15. Builds on Story 15.1 (PR #37 @ 23694dd) which unblocked
this work by fixing the EIP-55 checksum + 0x-prefix issues in
src/micropayment-engine.ts.

Track A — extend tests/x402-v2.test.ts (tests #14-#26)
  - on402Response: happy / non-402 status / missing+malformed header
  - submitPayment: 200 OK / non-2xx / network error rewrap
  - verifyReceipt: 200+verified / non-2xx / network error (no throw)
  - createPaymentIntentHeader: happy / optional fields undef
  - negotiatePaymentIntent: happy / TODO branch locked
  - src/x402-v2.ts: 63.74% → 97.32% line coverage

Track B — create tests/micropayment-engine.test.ts (B-1 through B-18)
  - constructor (3): 0x-guard, defaults, custom thresholds
  - queuePayment (3): paymentId shape, usdToBaseUnits errors, auto-settle
  - batchPayments (3): EIP-712 BatchSettlement sign / missing IDs / skipGas
  - getGasEstimate (2): 6-decimal USD, inverse scaling
  - setBatchWindow (2): s→ms conversion, window-boundary observable
  - getQueueStatistics (2): empty zeroes, multi-window aggregation
  - clearOldPayments (2): nothing-to-clear, settled+old removal
  - getPaymentStatus (1): find-or-undefined cross-window
  - src/micropayment-engine.ts: 0% → 100% line coverage

Track C — .github/workflows/ci.yml
  - Add `Coverage gate` step (npm run coverage) between Run tests + Build
  - Add `Upload coverage report` step (actions/upload-artifact@ea165f8 v4)
    with if: always(), 14-day retention, per matrix Node version

Test count: 115 baseline + 13 Track A + 18 Track B = 146 tests, all PASS
Coverage: global 97.52% lines / 86.75% branches / 100% funcs / 97.52% stmts
  (exceeds vitest.config.ts thresholds 80/80/70/80 → npm run coverage exit 0)

Determinism: all tests use vi.useFakeTimers + vi.setSystemTime + frozen
Math.random; fetch is stubbed via vi.stubGlobal; zero real network I/O.

Verbatim source preservation: 0 changes to src/ (verified git diff --stat).
Zero skipped tests, zero .todo/xit/xdescribe/.only.

Story chain: full SINKRA — @sm draft (v0.1) → @po GO-conditional → @sm patch
(v0.2) → @dev attempt 1 → bug escalation → @aios-master roundtable → Story
15.1 source fix (PR #37) → @dev resume (this PR) → @qa pending → @devops
pending merge.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

Warning

Review limit reached

@RBKunnela, we couldn't start this review because you've used your available PR reviews for now.

Your plan currently allows 1 review/hour. Refill in 40 minutes and 11 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5d423f46-0b6f-4e90-b07a-a58c104dff03

📥 Commits

Reviewing files that changed from the base of the PR and between 23694dd and 8273a7f.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • tests/micropayment-engine.test.ts
  • tests/x402-v2.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/story-15-coverage-gate-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive unit tests for the MicropaymentEngine and X402Handler, significantly increasing test coverage. The added tests verify core functionalities such as payment queuing, batching, gas estimation, and 402 response handling using deterministic fixtures. Feedback suggests improving test robustness by avoiding direct mutation of internal state in MicropaymentEngine tests and strengthening header assertions in X402Handler tests through round-trip verification.

Comment on lines +611 to +612
const item1 = engine.getPaymentStatus(id1)!;
item1.status = 'settled';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Directly mutating the status property of the object returned by getPaymentStatus relies on the internal implementation detail that the engine returns a direct reference to the object stored in its private queue Map. While effective for unit testing the clearOldPayments logic (which only targets 'settled' items), it creates a tight coupling with the internal state management of the MicropaymentEngine class.

Comment thread tests/x402-v2.test.ts
expect(init.method).toBe('POST');
expect(init.headers['Content-Type']).toBe('application/json');
expect(init.headers['Authorization']).toBe('Bearer bearer-token-xyz');
expect(init.headers['Payment-Intent-Authorization']).toMatch(/^x402:v2:/);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The assertion for Payment-Intent-Authorization only verifies the prefix. Since this header is a critical part of the protocol transit, consider adding a round-trip verification or checking that the base64-encoded payload contains the expected protocol and signature fields to ensure encodeSignedPayment is working correctly.

@RBKunnela
Copy link
Copy Markdown
Owner Author

Merge authorization per .claude/rules/automated-pr-merge-authority.md (8th application — second story-driven merge of the day):

  • @sm: Drafted Story 15 v0.1 → patched to v0.2 after @po findings (Track B under-spec on MicropaymentEngine surface)
  • @po: PASS 10/10 + 5/5 (story validation)
  • @dev: First implementation pass — escalated on bug discovery (EIP-55 checksum + generateNonce 0x-prefix); Track A completed cleanly (x402-v2.ts → 97.32%)
  • @sm: Drafted Story 15.1 block-lifter
  • @po: PASS 10/10 + 5/5 (canonical checksum verified via viem.getAddress())
  • @dev: Implemented Story 15.1 (PR fix(micropayment-engine): correct EIP-55 checksum on verifyingContract (Story 15.1) #37 merged 23694dd)
  • @dev: Resumed Story 15 — Track B unblocked, all tests pass
  • @qa: PASS — 2 QA passes, 10/10 AC, deterministic re-runs (2.62s + 2.23s, no flake)
    • Coverage: x402-v2.ts 63.74%→97.32%, micropayment-engine.ts 0%→100%, global 69.93%→97.52%
    • Zero src/ modifications verified
    • No threshold lowering
    • 5 previously-failing Track B tests (B-3, B-6, B-7, B-9, B-15) now pass against fixed source
  • @aiox-master (Orion): Chain routing approved
  • @devops: Executing merge under operator pre-approval pattern

Closes Task #15. Enables 80% coverage gate in CI.

Non-blocking observations (backlog candidates):

  • B-7 signature byte-identity softening (acceptable per current contract)
  • negotiatePaymentIntent TODO branch locked (pre-existing)
  • lint-scope src/-only (pre-existing convention, separate task to extend to tests/)
  • src/micropayment-engine.ts:240 recipient re-cast → undefended caller burden; Story 15.1 backlog

Pattern note — coverage work surfaced 2 product bugs today (Tasks #14 + #18), now both fixed:

  1. Dual-mode dead code (PR feat(x402-v2): fix dual-mode dead-code bug via Option C refactor (Story 14) #36)
  2. EIP-55 checksum + generateNonce 0x prefix (PR fix(micropayment-engine): correct EIP-55 checksum on verifyingContract (Story 15.1) #37)

Precedent chain (8 PRs today):

This makes 10 paybot ecosystem merges in a single day.

@RBKunnela RBKunnela merged commit 553cabf into main May 22, 2026
8 checks passed
@RBKunnela RBKunnela deleted the feat/story-15-coverage-gate-v2 branch May 22, 2026 22:52
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