> ## Documentation Index
> Fetch the complete documentation index at: https://docs.highailabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Subscription-State Testing

> How free, active-Pro, and lapsed-Pro entitlement states are proven on staging now that the subscription gate is enforced there.

## Why this changed

Staging historically ran with the subscription gate **bypassed**. That was
convenient — no test needed an entitlement to reach a paid path — but it made
every paywall assertion on that deployment *vacuous*: a denial test could pass for
the wrong reason, and an enforcement test could only ever pass. Paywall behaviour
had to stay a Tier A concern, and the live paywall was the one launch-critical
surface no automated test could observe.

Staging now runs the gate in **enforce** mode. Two consequences follow, and both
matter when you write a test:

<CardGroup cols={2}>
  <Card title="Paid paths need a real entitlement" icon="key">
    An identity with no subscription is genuinely denied on staging. Tests that
    exercise a paid feature must provision Pro first.
  </Card>

  <Card title="Denial is now observable" icon="shield-halved">
    A free identity really is refused, so the paywall can be asserted against the
    deployed backend instead of only in the in-memory tier.
  </Card>
</CardGroup>

## The three states

The subscription suite covers the three states a real account can be in, plus the
two transitions between them — each on a brand-new, really-logged-in Clerk
identity.

| State           | How the harness produces it                                                      | Expected verdict                                       |
| --------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------ |
| **FREE**        | `withTestUser(fn, { pro: false })` — the identity is created with no entitlement | Every gated surface closes                             |
| **ACTIVE\_PRO** | The default. `withTestUser` provisions an active Pro record at creation          | Every gated surface opens                              |
| **LAPSED\_PRO** | A stored Pro tier whose expiry is in the past                                    | Every gated surface closes, exactly as for a free user |

Transitions are asserted in both directions: free → active Pro opens every gate,
and active Pro → lapsed closes every gate.

```ts theme={null}
// A deliberately free identity
await withTestUser(async (user) => { /* … */ }, { persona: 'paywall-free', pro: false });

// Move an existing identity between states mid-test
await configureSubscriptionState(user, 'lapsed_pro');
```

## Three surfaces, checked together

An entitlement is only correct if it agrees on all three surfaces the app
actually depends on. Every state case checks the same identity against all three:

<Steps>
  <Step title="Convex">
    A Pro-gated action runs its entitlement check before any work. A denied caller
    gets a paywall error; an admitted caller gets past the gate.
  </Step>

  <Step title="The deployed Hono API">
    A Pro-gated route's middleware order is body limit → entitlement → validator,
    so the **status code is the verdict**: `401` means identity failed, `402` means
    entitlement denied, `400` means the request passed both gates and was stopped
    by the schema.
  </Step>

  <Step title="The entitlement projection">
    The internal endpoint the API actually reads, probed directly. This separates
    "Convex says free" from "the API asked the wrong deployment" — a distinction
    that is invisible if you only look at the API's answer.
  </Step>
</Steps>

## The zero-cost probe

Both gate probes send a **deliberately invalid body** — an empty selection on the
Convex side, a malformed field on the API side.

<Tip>
  Because each route checks entitlement *before* it validates input, the failure
  **mode** is the verdict: paywall vs validation error on Convex, `402` vs `400` on
  the API. A correctly-closed gate costs nothing, and a wrongly-**open** gate costs
  a validation error rather than a paid AI turn.
</Tip>

"Past the gate" deliberately does not mean "succeeded". It means the entitlement
check passed and a later, cheaper check stopped the call — which is the only
evidence a zero-cost probe can give, and exactly the evidence needed.

Exactly one case in the suite spends real money: a single end-to-end paid parse
that proves an active-Pro identity can complete a real paid operation. Everything
else is a status code.

## The preflight, and why it skips loudly

Every state assertion is meaningless unless the deployment is actually enforcing.
The suite therefore reads the effective gate first and, if it finds the gate
bypassed, **skips the state matrix with a loud warning** rather than letting the
denial assertions invert into silent passes.

<Warning>
  This is the single most likely false-green in the whole platform. A denial test
  on a bypassed deployment does not fail — it passes for the wrong reason. Assert
  the gate state before asserting anything about the paywall, and treat a skip
  here as a staging-configuration regression, not a pass.
</Warning>

The same preflight pattern applies anywhere else you write a paywall assertion
against a real deployment.

## Running it

```bash theme={null}
cd apps/mobile
pnpm test:integration:subscriptions
```

The suite is part of the full Tier C run and the fast lane. Its smoke-tagged
cases (the gate preflight and the per-state subscription reads) also run in the
smoke lane, so a gate flip on staging is caught in under 30 seconds rather than
30 minutes.

## Related

<CardGroup cols={2}>
  <Card title="Tier C — Staging Integration" icon="plug-circle-check" href="/planning/testing/tier-c-staging-integration">
    The identity model, production guard, and cost controls these tests run under.
  </Card>

  <Card title="What's in Pro" icon="star" href="/help/plans/whats-in-pro">
    The user-facing definition of what the gate protects.
  </Card>
</CardGroup>
