Skip to content

test(receipts): cover PostReceiptHelper workflow metadata resolution#3636

Merged
vegaro merged 3 commits into
cesar/receipt-presented-paywall-workflow-idsfrom
facu/pr3603-postreceipt-workflow-tests
Jun 23, 2026
Merged

test(receipts): cover PostReceiptHelper workflow metadata resolution#3636
vegaro merged 3 commits into
cesar/receipt-presented-paywall-workflow-idsfrom
facu/pr3603-postreceipt-workflow-tests

Conversation

@facumenzella

@facumenzella facumenzella commented Jun 22, 2026

Copy link
Copy Markdown
Member

Motivation

Follow-up to #3603 (cc @vegaro). That PR threads presented_workflow_id / presented_step_id through the post-receipt pipeline. While reviewing it I found two coverage gaps, one of which is significant. This PR targets the #3603 branch so the tests land with the feature.

Changes

1. PostReceiptHelper resolution coverage (PostReceiptHelperTest)

The new resolution logic in PostReceiptHelper was only exercised indirectly (BackendTest covers the wire format given explicit metadata; PaywallViewModelWorkflowTest covers event creation). Added:

  • resolves workflow metadata from the live presented paywall event
  • omits workflow metadata when the presented event has no workflow (pins the "both-or-neither" rule of WorkflowMetadata.from)
  • replays cached workflow metadata on unsynced retry (postRemainingCachedTransactionMetadata)

Supporting: mockPostReceiptSuccess now matches workflowMetadata = any() (an omitted defaulted arg in MockK binds eq(null)), so the success path can post non-null workflow metadata. Existing tests unaffected.

2. Fixed a vacuous re-presentation test + added EXIT_OFFER coverage (PaywallViewModelWorkflowTest)

The feature's own regression test, purchase initiated after same paywall workflow re-presentation carries current step metadata, did not actually exercise withCurrentWorkflowMetadata. Its two steps pointed at different screens (screen-1 vs screen-2), producing different presentation fingerprints, so the second impression took the else branch and recreated fresh event data, reading the live step. The test passed even with withCurrentWorkflowMetadata removed.

  • Reworked that test so both steps render the same screen (identical fingerprint → de-dup branch is taken), with a "no new impression" assertion proving the branch was reached.
  • Added a sibling test for EXIT_OFFER after the same-fingerprint re-presentation.
  • Both verified RED → GREEN: with withCurrentWorkflowMetadata neutralized they fail (expected "step-2" but was "step-1"); restored, they pass.

Validation

  • ./gradlew :purchases:testDefaultsBc8DebugUnitTest --tests "com.revenuecat.purchases.PostReceiptHelperTest" , 86 tests, 0 failures.
  • ./gradlew :ui:revenuecatui:testDefaultsBc8DebugUnitTest --tests "com.revenuecat.purchases.ui.revenuecatui.data.PaywallViewModelWorkflowTest" , 0 failures.
  • RED→GREEN proof captured by neutralizing the withCurrentWorkflowMetadata line (run under JDK 21).

Important

Before #3603 merges: its withCurrentWorkflowMetadata change had no test that exercised it (the existing regression test was vacuous). This PR closes that gap. Recommend not approving #3603 until these tests are in.

AI session context

AI Context

Metadata

  • PR: this PR (base: cesar/receipt-presented-paywall-workflow-ids, the Add presented_workflow_id and presented_step_id to /receipts #3603 branch)
  • Branch: facu/pr3603-postreceipt-workflow-tests
  • Author / human owner: facumenzella
  • Agent(s): Claude Code, Opus 4.8 (1M context)
  • Session source: current conversation
  • Generated: 2026-06-22
  • Context document version: 2

Goal

Close test-coverage gaps found while reviewing #3603: (a) PostReceiptHelper-layer workflow-metadata resolution, and (b) the withCurrentWorkflowMetadata re-presentation behavior, whose existing regression test turned out to be vacuous.

Initial Prompt

Review #3603. The review surfaced coverage gaps; follow-ups were "write a test for 1", "open a draft targeting this pr and mention this", then "write it [finding #2] and put it in the same pr".

Important Follow-up Prompts

Agent Contribution

  • Added 3 PostReceiptHelperTest tests + made mockPostReceiptSuccess workflow-agnostic.
  • Added an EXIT_OFFER re-presentation test in PaywallViewModelWorkflowTest.
  • Discovered (via a neutralize + sentinel experiment) that the existing PURCHASE_INITIATED re-presentation test never reached the withCurrentWorkflowMetadata branch; reworked it to share one screen and added a "no new impression" guard.
  • Verified RED→GREEN for both UI tests by neutralizing the production line.

Human Decisions

Key Implementation Decisions

  • Decision: force the same presentation fingerprint by pointing both workflow steps at the same screenId.
    • Rationale: the de-dup branch (where withCurrentWorkflowMetadata lives) only runs when fingerprints match; different screens silently routed the test through fresh re-creation.
    • Rejected: leaving the original different-screen fixture (it does not test the fix).
  • Decision: assert "no new IMPRESSION" after the re-presentation.
    • Rationale: proves the de-dup branch was taken; this is the guard that distinguishes a real test from a vacuous one.
  • Decision: mockPostReceiptSuccess uses workflowMetadata = any().
    • Rationale: omitted defaulted MockK arg binds eq(null).

Files / Symbols Touched

  • purchases/src/test/java/com/revenuecat/purchases/PostReceiptHelperTest.kt
    • Why: PostReceiptHelper-layer resolution coverage.
    • Symbols: mockPostReceiptSuccess, new @Tests (presented-paywall resolution, no-workflow omission, postRemainingCachedTransactionMetadata).
  • ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelWorkflowTest.kt
    • Why: make the re-presentation test exercise withCurrentWorkflowMetadata; add EXIT_OFFER coverage.
    • Symbols: purchase initiated after same paywall workflow re-presentation..., exit offer after same paywall workflow re-presentation....

Dependencies / Config / Migrations

  • None.

Validation

  • Commands run:
    • :purchases:testDefaultsBc8DebugUnitTest --tests "...PostReceiptHelperTest": 86 tests, 0 failures.
    • :ui:revenuecatui:testDefaultsBc8DebugUnitTest --tests "...PaywallViewModelWorkflowTest": 0 failures.
    • RED check: with withCurrentWorkflowMetadata neutralized, both re-presentation tests fail (expected "step-2" but was "step-1"); restored, they pass.
  • Manual verification: Not run (test-only change).
  • CI: Not captured at time of writing.

Validation Gaps

  • The cached-vs-live precedence in PostReceiptHelper is exercised via the live and retry paths but not via a dedicated synchronous-purchase test where getLocalTransactionMetadata returns metadata.
  • Local runs required JDK 21 (project .sdkmanrc pins 21.0.6-librca); default daemon here was JDK 17, which Robolectric rejects for Android SDK 36.

Review Focus

  • Confirm the "no new impression" assertion correctly captures the de-dup branch for both UI tests.
  • Confirm mockPostReceiptSuccess's workflowMetadata = any() relaxation does not weaken the negative-case test (...has no workflow), which asserts workflowMetadata = null explicitly.

Risks / Reviewer Notes

  • Risk: test-only change; no production behavior altered.
    • Evidence: diff touches only *Test.kt files (the PaywallViewModel edit during RED→GREEN was reverted; prod tree confirmed clean).
    • Mitigation: none needed.

Non-goals / Out of Scope

  • No production code changes; withCurrentWorkflowMetadata itself is unchanged.

Omitted Context

  • Raw transcript, unrelated exploration, sensitive details, repetitive attempts, and chain-of-thought-style content were omitted.

Note

Low Risk
Only test files change; behavior under test lives in the feature branch, not in this diff.

Overview
Test-only follow-up that locks in workflow presented_workflow_id / presented_step_id behavior from the parent feature PR—no production code changes.

PostReceiptHelperTest adds coverage for how workflowMetadata is chosen when calling backend.postReceiptData: from the presented paywall event when workflow ids are present, null when they are not, and replay from LocalTransactionMetadata on postRemainingCachedTransactionMetadata. mockPostReceiptSuccess now stubs workflowMetadata = any() so success-path mocks accept non-null metadata (MockK default binding issue).

PaywallViewModelWorkflowTest fixes a regression test that never hit the impression de-dup path: both workflow steps now share the same screen so withCurrentWorkflowMetadata must refresh step ids on re-presentation. Adds a no new IMPRESSION assertion and a sibling EXIT_OFFER test for the same scenario.

Reviewed by Cursor Bugbot for commit 8527bcd. Bugbot is set up for automated code reviews on this repo. Configure here.

Adds PostReceiptHelper-layer coverage for the workflow metadata
threading introduced in #3603, which was only exercised at the
Backend wire-format and PaywallViewModel event layers.

- posts workflow metadata resolved from the live presented paywall event
- omits workflow metadata when the presented event has no workflow
- replays cached workflow metadata on unsynced retry
  (postRemainingCachedTransactionMetadata)

Makes mockPostReceiptSuccess workflow-agnostic (workflowMetadata = any())
so the success path can post non-null workflow metadata.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qCKrb3h5JJyBvgP9sw11m
facumenzella and others added 2 commits June 22, 2026 15:51
…ries current step

Adds a regression test for withCurrentWorkflowMetadata covering the
review finding #2 (CLOSE/EXIT_OFFER after a same-fingerprint step change).

Both workflow steps render the same screen so the presentation fingerprint
is identical and the impression is de-duped, forcing the code through the
withCurrentWorkflowMetadata branch (asserted via "no new impression"). The
exit offer event then must carry the updated step id.

Verified RED -> GREEN: with withCurrentWorkflowMetadata neutralized the test
fails; with it restored it passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qCKrb3h5JJyBvgP9sw11m
The existing `purchase initiated after same paywall workflow re-presentation`
test pointed its two steps at different screens (screen-1 vs screen-2), which
produce different presentation fingerprints. The second impression therefore
took the else branch and recreated fresh event data, so the test passed even
with withCurrentWorkflowMetadata removed (verified: it stayed green when the
line was neutralized). It did not guard the feature it was named for.

Rework it so both steps render the same screen (identical fingerprint), add a
"no new impression" assertion to prove the de-dup branch was taken, and confirm
RED -> GREEN: with withCurrentWorkflowMetadata neutralized it now fails
(expected "step-2" but was "step-1"); restored, it passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qCKrb3h5JJyBvgP9sw11m
@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.28%. Comparing base (ed0ea29) to head (8527bcd).
⚠️ Report is 2 commits behind head on cesar/receipt-presented-paywall-workflow-ids.

Additional details and impacted files
@@                              Coverage Diff                              @@
##           cesar/receipt-presented-paywall-workflow-ids    #3636   +/-   ##
=============================================================================
  Coverage                                         80.28%   80.28%           
=============================================================================
  Files                                               380      380           
  Lines                                             15582    15582           
  Branches                                           2179     2179           
=============================================================================
  Hits                                              12510    12510           
  Misses                                             2203     2203           
  Partials                                            869      869           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

vegaro pushed a commit that referenced this pull request Jun 23, 2026
### Motivation

Follow-up test coverage for #3603. That PR's refactor commit (`b2bfd86`)
fixed a bug where `workflowId`/`stepId` were `@Transient` on the cached
transaction metadata and were lost on the disk round-trip, so
`presented_workflow_id` / `presented_step_id` would be sent as `null` on
retry or unsynced replay. The field was promoted to a first-class
`workflowMetadata` on `LocalTransactionMetadata` with
`@SerialName("workflow_metadata")`, but there was no regression test
asserting it actually survives serialization.

### What this adds

One test in `LocalTransactionMetadataStoreTest`,
`cacheLocalTransactionMetadata round-trips workflow metadata`, mirroring
the existing `handles paywall data` test. The JSON written to
`SharedPreferences` is captured and fed back to
`getLocalTransactionMetadata`, so the real `JsonTools.json`
encode/decode runs (only SharedPreferences I/O is mocked).

### Verification (RED → GREEN)

- **RED**: with `workflowMetadata` marked `@Transient`, the test fails
at the assertion (`retrieved?.workflowMetadata` is `null`).
- **GREEN**: with `@SerialName("workflow_metadata")` (current state), it
passes.

Ran `:purchases:testDefaultsBc8DebugUnitTest --tests
"...LocalTransactionMetadataStoreTest"` locally (JDK 21).

Companion to #3636 (which covers the `PostReceiptHelper` threading and
replay paths). Together they close the two test gaps identified in
review of #3603.

<details>
<summary>AI session context</summary>

**Task:** Review #3603 ("is anything missing?"), then close the
identified test gaps.

**Findings from review of #3603:**
1. (important) Disk round-trip of `workflowMetadata` untested, the exact
bug `b2bfd86` fixed. `LocalTransactionMetadataStoreTest` had the
analogous `handles paywall data` round-trip test but nothing for
`workflowMetadata`.
2. (important) `PostReceiptHelper` threading untested (only an `any()`
count bumped). → covered by #3636.

**This PR** addresses finding #1.

**Decisions:**
- Reused the existing capture-the-written-JSON pattern so the test
exercises real serialization, not a mock, which is what makes it a
genuine guard for the `@Transient` regression.
- Based on `cesar/receipt-presented-paywall-workflow-ids` because the
`workflowMetadata` field only exists there, not on `main`.

**Verified:** RED→GREEN as described above. Production code
(`LocalTransactionMetadata.kt`) left unchanged; only the test file is
added.

**Not done:** The remaining review items on #3603 were questions, not
test gaps (all-or-nothing `WorkflowMetadata.from`, impression/purchase
step-id divergence under dedup, `pr:other` vs `pr:fix` label). Not
addressed here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
</details>

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vegaro vegaro marked this pull request as ready for review June 23, 2026 12:27
@vegaro vegaro requested a review from a team as a code owner June 23, 2026 12:27
@vegaro vegaro merged commit 39976ae into cesar/receipt-presented-paywall-workflow-ids Jun 23, 2026
37 checks passed
@vegaro vegaro deleted the facu/pr3603-postreceipt-workflow-tests branch June 23, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants