> ## 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.

# Mobile Feature Test Platform

> The three-tier, simulator-free test platform that proves every user-data feature in the High IQ mobile app works from screen to deployed backend.

## Overview

Every user-data feature in the High IQ mobile app is covered by automated,
simulator-free tests organized into **three tiers**. Each tier proves something
the others structurally cannot see, and each costs a different amount to run.

Cannabis and strain data (Supabase) is out of scope except where a user feature
depends on it. **User data in Convex is the focus** — `apps/mobile/convex/schema.ts`
is the source of truth for what exists.

<Note>
  This platform is separate from the Revyl cloud-device suite. Revyl drives a real
  iOS simulator through the UI; the tiers below never boot a simulator and run in
  seconds to minutes instead.
</Note>

## The three tiers

| Tier  | Name                       | What it proves                                                                                                                                                                                                      | Runner                                                                       | Home                                                    |
| ----- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------- |
| **A** | Backend unit               | Convex function logic, argument validators, auth gating, index usage, and scheduled side effects — against an in-memory fake                                                                                        | vitest + `convex-test`                                                       | `apps/mobile/convex/tests/**`                           |
| **B** | Hook / UI↔backend contract | That the arguments hooks and screens send actually match the validators; how screens render loading / empty / error / data states; optimistic updates                                                               | vitest jsdom + jest-expo                                                     | `apps/mobile/src/**`, `apps/mobile/test/ui/features/**` |
| **C** | Staging integration        | That the **deployed** backend works: real Clerk JWT verification, real validators, outbound `fetch` from Convex actions, Trigger.dev pipelines, and cross-feature effects (order → stash → notebook → achievements) | vitest node, `ConvexHttpClient` against the shared staging Convex deployment | `apps/mobile/test/integration/suites/**`                |

Tiers A and B are free, fast, and hermetic — they never touch a deployment.
**Tier C hits real infrastructure**: the shared staging Convex deployment, the
production Hono API, and Trigger.dev's prod environment. It writes real rows and
spends real credits, which is why it is invoked explicitly and never runs on push.

<Warning>
  Tier C is deliberately **not** part of `pnpm test`, `pnpm check`, or
  `turbo run test`. The Turbo `test` task is cached — a cached pass would defeat a
  suite whose entire job is detecting deployed-backend regressions — and its
  strict env mode would strip every credential the suite needs.
</Warning>

## Running each tier

All commands run from `apps/mobile` (or with `pnpm --filter @highiq/mobile` from
anywhere in the monorepo).

<Tabs>
  <Tab title="Tier A + B (free)">
    ```bash theme={null}
    pnpm test:convex   # Tier A — convex-test, in-memory
    pnpm test:unit     # Tier B — vitest jsdom (hooks, pure logic)
    pnpm test:ui       # Tier B — jest-expo (component rendering; typechecks first)

    pnpm test          # all three; Tier C is NOT included
    ```
  </Tab>

  <Tab title="Tier C (billed)">
    ```bash theme={null}
    pnpm test:integration:smoke   # harness self-test — under 30 s, no AI, no Trigger
    pnpm test:integration:fast    # everything not tagged @slow
    pnpm test:integration         # full suite (30–60 min)

    # One area at a time — orders, stash, collection, notebooks, gmail,
    # achievements, dispensaries, users, subscriptions:
    pnpm test:integration:orders

    pnpm test:integration:typecheck  # tsc only; chained into every script above
    ```
  </Tab>
</Tabs>

Tier C requires environment variables and **skips cleanly** without them, naming
exactly what is missing. See
[Tier C — Staging Integration](/planning/testing/tier-c-staging-integration) for
the full setup, the isolated-user pattern, and the three-layer production guard.

## Adding a test for a new feature

<Steps>
  <Step title="Pick the tier by what you need to prove">
    * Function logic, a validator, an auth check, a scheduled effect → **Tier A**
    * The arguments a hook sends, or how a screen renders a backend state → **Tier B**
    * "Does this actually work on the deployed backend? Does the pipeline
      complete?" → **Tier C**

    Tier C is the expensive tier. Use it only for what A and B structurally
    cannot see: real JWT verification, outbound network calls from actions,
    scheduled side effects on a real deployment, and pipeline completion.
  </Step>

  <Step title="Name the file">
    New Tier A/B suites use the `*.featuretests.test.ts(x)` suffix in a **new**
    file, so parallel authors never collide with the existing suites.

    | Tier                 | Path                                                             |
    | -------------------- | ---------------------------------------------------------------- |
    | A (Convex)           | `apps/mobile/convex/tests/<area>.featuretests.test.ts`           |
    | A (Trigger.dev task) | co-located `packages/trigger/src/**/<name>.featuretests.test.ts` |
    | B (hook/unit)        | co-located `<source>.featuretests.test.ts(x)` next to the source |
    | B (UI)               | `apps/mobile/test/ui/features/<Area>*.test.tsx`                  |
    | C                    | `apps/mobile/test/integration/suites/<area>.integration.test.ts` |
  </Step>

  <Step title="Tag it (Tier C)">
    Vitest `-t` matches the concatenated describe/test title, so **tags live in
    titles**: `@smoke`, `@slow`, `@ai`, `@trigger`, `@shared-account`, and
    `@P0`–`@P3`. A case with no tag runs in every lane.
  </Step>

  <Step title="Add the matrix row">
    Give the case an area-prefixed ID (`ORD-231`, `NB-118`, `SUB-115`, …) and add
    it to the master matrix with tier, priority, status, and implementing file.
    Do not leave a test out of the matrix — the matrix is what the weekly
    regression run is read against.
  </Step>

  <Step title="Scope every Tier C assertion to your own test user">
    Staging is shared by every non-production build plus a daily cron. Never
    assert on a global count, a "latest N" listing, or a cross-user aggregate —
    those pass or fail depending on what someone else did five minutes ago.
  </Step>
</Steps>

## Interpreting results

Per-area builder results and run evidence live in the repo under
`docs/testing/feature-audit/`. Statuses have fixed meanings:

| Status                               | Read it as                                                                                                                                               |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pass`                               | Green.                                                                                                                                                   |
| `fail-bug:<id>` / `fail-new-finding` | A **product** defect. Some tests are written deliberately to fail against today's code and are flipped when the fix lands — do not "fix" the test.       |
| `fail-blocked`                       | A correct assertion that cannot pass until an upstream finding is fixed. Not new signal on its own.                                                      |
| `fail-infra`                         | A harness problem. Fix the harness, not the product.                                                                                                     |
| `expected-fail`                      | Deliberately pinned red with `it.fails` / `test.failing`. It turns into a hard failure the moment the fix lands — that is the signal to remove the pin.  |
| `skipped-blocked`                    | A **staging configuration gap**, not a pass: a missing OAuth grant, an absent deploy key, a pre-existing non-terminal job. Every one carries its reason. |
| `skipped-paid`                       | Gated behind an explicit paid opt-in. Expected to skip in the default lane.                                                                              |

<Warning>
  **A skipped row is never evidence that a feature works.** Report pass, fail,
  `skipped-blocked`, and `skipped-paid` as four separate counts. Collapsing skips
  into "not failing" is how a run where an entire pipeline is dead still looks
  green.
</Warning>

## Two lessons worth keeping

* **A pin must assert behaviour, never a spelling.** `it.fails` only self-flips
  when the assertion observes the *effect* of the defect. A negative source scan
  (`not.toContain('…')`) fails **open** — it goes green when the literal is merely
  reworded while the defect remains. Invert unavoidable source assertions into a
  positive binding.
* **The in-memory fake and a real deployment differ on time.** On a real Convex
  deployment `Date.now()` is frozen for the duration of a mutation, so every row
  written in one handler shares a timestamp; the in-memory fake advances per call.
  A Tier A test asserting timestamp equality across rows can flake in the harness
  while being correct in production — hoist `const now = Date.now()` in the
  handler rather than quarantining the test.

## Related

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

  <Card title="Subscription-State Testing" icon="lock" href="/planning/testing/subscription-state-testing">
    How free, active-Pro, and lapsed-Pro states are proven now that staging enforces the gate.
  </Card>

  <Card title="Weekly Feature Regression" icon="calendar-check" href="/planning/testing/weekly-regression">
    Lanes, local reproduction, and why the schedule is still off.
  </Card>

  <Card title="Launch Testing Plan" icon="clipboard-check" href="/planning/launch-readiness/testing-plan">
    Where this platform sits among Revyl, Argent, and manual QA.
  </Card>
</CardGroup>
