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

# Gmail Import — Behavior Spec

> Every behavior the Gmail import feature must have — connecting, scanning, classifying, reviewing, importing, and syncing — and how each behavior is verified.

## What this feature is

A user connects their Google account and lets the app find dispensary receipts in their
mailbox. There are three ways in: a one-time **historical import** that scans an archive
going back as far as they choose, a Pro-only **Quick Sync** that picks up only what has
arrived since last time, and a **broader search** that escalates a Quick Sync into a full
six-month keyword scan. A scan produces candidate emails grouped by shop; the user picks
which shops and which emails to import. Importing parses each selected receipt into a
review draft — **nothing becomes an order until the user reviews it**, except drafts that
are fully resolved, which they can confirm in one tap. They can cancel a running import,
resume a failed one, retry only the receipts that failed, and disconnect Gmail at any
time, which revokes access and purges the scan data while leaving every order they
already saved.

<Note>
  This page is the contract, not a coverage report. Every line below is true of the code
  on `main` today and is enforced by the cited tests. Behaviors that are deliberately
  unproven live in [Deliberate gaps](#deliberate-gaps-conditions), never in the tables.
</Note>

## The behavior contract

### Connecting Gmail

| Behavior                                                                                                                                                                                                              | Verified by               | Tier | Where                                                                                         |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ---- | --------------------------------------------------------------------------------------------- |
| Only the exact read-only Gmail scope counts as granted, whether the provider hands back a space-delimited string or an array — a substring match is not enough, and the client applies the same rule the server does. | GML-001, GML-004, GML-122 | A, B | `gmailScope.test.ts`, `gmail.featuretests.test.ts`, `useGmailConnection.featuretests.test.ts` |
| An unverified Google account cannot connect, and when the app cannot verify the account at all it fails closed, writing no connection row.                                                                            | GML-002, GML-003          | A    | `gmail.featuretests.test.ts`                                                                  |
| The connection is keyed to the identity provider's external account id in preference to the raw provider user id.                                                                                                     | GML-007                   | A    | `gmailSync.test.ts`                                                                           |
| A cancelled OAuth flow, a missing scope, and a genuine error are surfaced as three distinct outcomes.                                                                                                                 | GML-123                   | B    | `useGmailConnection.featuretests.test.ts`                                                     |
| The upload hub's Gmail card reflects all five connection states, including having been revoked after a successful sync.                                                                                               | GML-119                   | B    | `orderUploadGmailState.featuretests.test.ts`                                                  |

### Disconnecting and reconnecting

| Behavior                                                                                                                                                                                                                                   | Verified by               | Tier | Where                        |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- | ---- | ---------------------------- |
| Disconnecting revokes the grant, cancels in-flight runs across bounded pages, purges scan candidates and drafts, and never touches an order the user already saved — including a disconnect that lands while an import is being scheduled. | GML-008, GML-010, GML-027 | A    | `gmailSync.test.ts`          |
| A revoke whose outcome is ambiguous stays retryable, and a callback arriving after the disconnect is inert.                                                                                                                                | GML-009                   | A    | `gmailSync.test.ts`          |
| Reconnecting bumps the connection generation and clears the disconnect state, so stale callbacks from the previous connection cannot apply — and is refused while a disconnect is still settling.                                          | GML-005, GML-006          | A    | `gmail.featuretests.test.ts` |
| Deleting the account revokes Gmail and purges all five Gmail tables leaf-first, recording an unconfirmed revocation rather than silently succeeding.                                                                                       | GML-074                   | A    | `gmail.featuretests.test.ts` |
| A stuck scanning or classifying status can be cleared by the user, and only from those two statuses.                                                                                                                                       | GML-011                   | A    | `gmail.featuretests.test.ts` |

### Choosing what to scan

| Behavior                                                                                                                                                                                                                                   | Verified by                        | Tier | Where                                                        |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- | ---- | ------------------------------------------------------------ |
| Every time-range option produces a date range the backend accepts, and an invalid range is rejected before any job is created.                                                                                                             | GML-121, GML-014                   | B, A | `helpers.featuretests.test.ts`, `gmail.featuretests.test.ts` |
| Shop domains are derived from a shop's website, email, and stored domains as one lowercased, deduplicated, sorted set; malformed domains are dropped silently rather than failing the scan; and shops sharing a domain contribute it once. | GML-019, GML-020, GML-126          | A, B | `gmail.featuretests.test.ts`, `helpers.test.ts`              |
| More than 50 personal domains, more than 100 crowdsourced domains, or more than 500 raw domain inputs are refused before scheduling.                                                                                                       | GML-016, GML-017, GML-018          | A    | `gmailSync.test.ts`, `gmail.featuretests.test.ts`            |
| Restricting a scan to known shops while selecting none creates no job at all.                                                                                                                                                              | GML-015                            | A    | `gmailSync.test.ts`                                          |
| A user can add a shop domain by hand, and the shop's saved-domain limit is enforced when they do.                                                                                                                                          | `appendGmailDomainWithinShopLimit` | A    | `gmailSync.test.ts`                                          |
| The crowdsourced dispensary-domain endpoint is reachable and well-formed, and a scan accepts the full 100-domain layer.                                                                                                                    | GML-226, GML-227                   | C    | `gmail.integration.test.ts`                                  |

### Scanning the mailbox

| Behavior                                                                                                                                                                                                                                              | Verified by                                 | Tier | Where                                             |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ---- | ------------------------------------------------- |
| Historical scanning is refused outright when the feature is switched off for the deployment.                                                                                                                                                          | GML-012                                     | A    | `gmail.featuretests.test.ts`                      |
| A Quick Sync scan requires Pro, and a refusal creates no job.                                                                                                                                                                                         | GML-013                                     | A    | `gmail.featuretests.test.ts`                      |
| The scan's per-layer query budgets always sum to no more than the requested result cap, the generic keyword layer is capped at 250 whenever a trusted shop layer exists, and a restricted scan with zero domains throws before a single mailbox call. | GML-080, GML-081, GML-082                   | A    | `gmail-scan-task.featuretests.test.ts`            |
| A message found in more than one layer is kept once, credited to the layer that saw it first, and the non-cannabis blocklist filters the generic layer and its subdomains while never filtering a shop the user named or a crowdsourced domain.       | GML-083, GML-084                            | A    | `gmail-scan-task.test.ts`                         |
| Two thousand candidates arrive as exactly twenty contiguous ledger chunks followed by one completion event.                                                                                                                                           | GML-085                                     | A    | `gmail-scan-task.featuretests.test.ts`            |
| A rejected ledger chunk aborts the run rather than posting on, and a scan that finds nothing still emits its terminal event so the wizard never waits forever.                                                                                        | GML-086, GML-087                            | A    | `gmail-scan-task.test.ts`                         |
| A ledger chunk arriving out of order, one exceeding the job's receipt cap, one repeating a message, or a contradictory, stale, or wrong-owner runtime transition is rejected.                                                                         | GML-028, GML-029, GML-030, GML-031, GML-073 | A    | `gmail.featuretests.test.ts`, `gmailSync.test.ts` |
| Concurrent scan requests collapse onto one job and one pipeline run.                                                                                                                                                                                  | GML-218                                     | C    | `gmail.integration.test.ts`                       |

### Deciding what is a receipt

| Behavior                                                                                                                                                                                                                                                                                             | Verified by                       | Tier | Where                                                                                 |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ---- | ------------------------------------------------------------------------------------- |
| Mail from a shop the user named, or from a known dispensary domain, is treated as a receipt with high confidence and without spending an AI call.                                                                                                                                                    | GML-088, GML-205                  | A, C | `gmail-classify-task.test.ts`, `gmail.integration.test.ts`                            |
| A review-request, cancellation, shipping-status, autoship, or marketing email must never be classified as a receipt — the subject decides this deterministically, even from a trusted dispensary domain, and a subject that both references an order and reads as a review request is a non-receipt. | `classifyReceiptSubject`, GML-205 | A, C | `receipt-subject.test.ts`, `gmail-classify-task.test.ts`, `gmail.integration.test.ts` |
| That veto must not over-reach: a genuine order confirmation from the same domain is still auto-classified as a receipt, and the bare word "order" is not a confirmation.                                                                                                                             | `classifyReceiptSubject`, GML-205 | A, C | `receipt-subject.test.ts`, `gmail-classify-task.test.ts`, `gmail.integration.test.ts` |
| Generic candidates go through AI classification in batches, and an AI failure falls back to low confidence rather than dropping candidates.                                                                                                                                                          | GML-089, GML-206                  | A, C | `gmail-classify-task.test.ts`, `gmail.integration.test.ts`                            |

### Reviewing and selecting candidates

| Behavior                                                                                                                                                                                                                                      | Verified by               | Tier | Where                                                    |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ---- | -------------------------------------------------------- |
| Candidates are grouped by shop, fall back to their sending domain and then to "Unknown", sort by group size, and multiple dispensaries in one scan group correctly on the deployed backend.                                                   | GML-125, GML-214          | B, C | `useGmailImportJob.test.ts`, `gmail.integration.test.ts` |
| Select-all and deselect-all across more than one chunk leave the selected count consistent, a partial chunk failure leaves a detectable inconsistency rather than a silent one, and a replacement naming stale candidate ids changes nothing. | GML-053, GML-054, GML-055 | A    | `gmailSync.test.ts`, `gmail.featuretests.test.ts`        |
| A job belonging to someone else is indistinguishable from a job that does not exist — no ownership leak.                                                                                                                                      | GML-052                   | A    | `gmail.featuretests.test.ts`                             |
| The candidate list auto-loads further pages only while the job is ready for review.                                                                                                                                                           | GML-124                   | B    | `useGmailImportJob.featuretests.test.tsx`                |

### Importing selected receipts

| Behavior                                                                                                                                                                                                                                                     | Verified by                        | Tier | Where                                                                                                                                               |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Importing one selected receipt produces exactly one review draft — and no order.                                                                                                                                                                             | GML-207                            | C    | `gmail.integration.test.ts`                                                                                                                         |
| A batch import honours its chunk size, waits for each chunk before starting the next, and records every failed receipt before the batch completes so retry-only-failures always has something to work from.                                                  | GML-090, GML-092                   | A    | `gmail-batch-import-task.featuretests.test.ts`, `gmail-batch-import-task.test.ts`                                                                   |
| A receipt that parses to nothing is tallied as its own outcome, separate from successes and failures — a non-receipt email is not a failure, in the task and end to end.                                                                                     | GML-091, GML-096, GML-034, GML-213 | A, C | `gmail-batch-import-task.featuretests.test.ts`, `gmail-import-task.featuretests.test.ts`, `gmail.featuretests.test.ts`, `gmail.integration.test.ts` |
| Parser idempotency is bound to the job, the message, and the connection generation together, and the internal API credential is never disclosed in a parser error or a log line.                                                                             | GML-093, GML-094                   | A    | `gmail-import-task.test.ts`                                                                                                                         |
| Every field of a multi-item receipt survives the trip from parser to progress event to order items.                                                                                                                                                          | GML-095, GML-209                   | A, C | `gmail-import-task.featuretests.test.ts`, `gmail.integration.test.ts`                                                                               |
| Import counters are recomputed from the outcome ledger rather than incremented blindly: the first terminal outcome for a message wins, counters derive from unique messages, and repeated deliveries never push the processed count past the selected count. | GML-035, GML-036, GML-045, GML-046 | A    | `gmailSync.test.ts`, `gmail.featuretests.test.ts`                                                                                                   |
| Progress is reported as a percentage that is 0 when nothing is selected and never leaves the 0-100 range.                                                                                                                                                    | GML-116, GML-129                   | B    | `useGmailImportJob.test.ts`, `useGmailImportJob.featuretests.test.tsx`                                                                              |
| Re-scanning the same window imports nothing new, a message a committed order already owns is marked a duplicate, and a message re-scanned into a newer job takes over the earlier job's pending draft rather than producing two.                             | GML-032, GML-033, GML-212          | A, C | `gmail.featuretests.test.ts`, `gmail.integration.test.ts`                                                                                           |

### Turning a draft into an order

| Behavior                                                                                                                                                                                         | Verified by      | Tier | Where                                             |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- | ---- | ------------------------------------------------- |
| Committing a reviewed draft creates exactly one order and its items.                                                                                                                             | GML-208          | C    | `gmail.integration.test.ts`                       |
| Finalizing is refused when the order's source message does not match the draft's, and a draft whose order already committed cannot be discarded even when the client never saw the confirmation. | GML-041, GML-042 | A    | `gmail.featuretests.test.ts`, `gmailSync.test.ts` |
| Discarding one draft leaves the other pending reviews intact, and the job settles as a completed historical import only once the last draft has been reviewed or discarded.                      | GML-043, GML-044 | A    | `gmailSync.test.ts`, `gmail.featuretests.test.ts` |
| A committed Gmail order fires the same post-save effects as any other order, and is classified by the job's kind rather than by what the client sent.                                            | GML-224, GML-225 | C    | `gmail.integration.test.ts`                       |
| A draft the review screen builds normalizes an unknown unit or product type into a supported one.                                                                                                | GML-120          | B    | `gmailOrderReviewDraft.featuretests.test.ts`      |

### Confirming safe orders in one tap

| Behavior                                                                                                                                   | Verified by      | Tier | Where                                                     |
| ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- | ---- | --------------------------------------------------------- |
| A draft is only "safe" if every strain matched, confidence is high enough, the date is valid, and it has items.                            | GML-037          | A    | `gmail.featuretests.test.ts`                              |
| One-tap confirm commits only safe drafts, at most 50 per call, and is the only path that writes an order without per-receipt confirmation. | GML-038, GML-210 | A, C | `gmail.featuretests.test.ts`, `gmail.integration.test.ts` |
| Calling it twice commits nothing twice.                                                                                                    | GML-039, GML-211 | A, C | `gmail.featuretests.test.ts`, `gmail.integration.test.ts` |
| It throws without the historical-import entitlement, at every server call site.                                                            | GML-040          | A    | `gmail.featuretests.test.ts`                              |

### Quick Sync

| Behavior                                                                                                                                                                                                        | Verified by               | Tier | Where                                                             |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ---- | ----------------------------------------------------------------- |
| A refused sync is distinguishable from a started one: the answer says whether it was scheduled, what blocked it, and which job the user must deal with first.                                                   | GML-231                   | C    | `gmail.integration.test.ts`                                       |
| A first-ever sync with no cursor uses the fallback window.                                                                                                                                                      | GML-216                   | C    | `gmail.integration.test.ts`                                       |
| The history cursor advances only when discovery came back empty — a sync that found mail leaves the cursor where it was until that mail is dealt with.                                                          | GML-047                   | A    | `gmailSync.test.ts`                                               |
| A discovery payload that disagrees with the ledger it claims to describe is rejected, as is one carrying more than 500 candidates.                                                                              | GML-048, GML-049          | A    | `gmail.featuretests.test.ts`                                      |
| Each sync failure code maps to its own user-facing message, and the raw code never reaches the user.                                                                                                            | GML-050, GML-118          | A, B | `gmail.featuretests.test.ts`, `GmailWizard.test.tsx`              |
| The sync task posts its candidate ledger before declaring itself complete, a missing history cursor falls back to a full domain scan, and exhausting the history page bound fails without advancing the cursor. | GML-097, GML-098, GML-099 | A    | `gmail-sync-task.featuretests.test.ts`, `gmail-sync-task.test.ts` |

### Cancelling, resuming, and retrying

| Behavior                                                                                                                                                                                                                         | Verified by               | Tier | Where                                                                          |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ---- | ------------------------------------------------------------------------------ |
| Cancelling lands the local boundary before any provider call, so a failing provider cannot leave the job running, and cancelling an already-terminal job is a no-op.                                                             | GML-062, GML-063, GML-219 | A, C | `gmail.featuretests.test.ts`, `gmail.integration.test.ts`                      |
| Progress arriving after a cancellation creates no drafts and no orders.                                                                                                                                                          | GML-064                   | A    | `gmailSync.test.ts`                                                            |
| Resuming after a scan runtime failure forces a fresh attempt; resuming after a scheduling failure reuses the same one.                                                                                                           | GML-058, GML-059, GML-220 | A, C | `gmail.featuretests.test.ts`, `gmail.integration.test.ts`                      |
| Resuming a job that already has drafts waiting returns the user to review without scheduling anything, and a job missing its scan parameters is refused rather than guessed at.                                                  | GML-060, GML-061          | A    | `gmail.featuretests.test.ts`                                                   |
| An interrupted historical import resumes on what is left, excluding messages that already reached a terminal outcome, and retry-failed re-imports only the receipts that failed.                                                 | GML-056, GML-057, GML-221 | A, C | `gmailSync.test.ts`, `gmail.featuretests.test.ts`, `gmail.integration.test.ts` |
| A scheduling failure marks the job retryable and restores the review state the user was in; retrying after an ambiguous network failure reuses the same job and key, while a later scan after a terminal failure gets a new one. | GML-024, GML-025, GML-026 | A    | `gmailSync.test.ts`                                                            |

### The pipeline's callback into the app

| Behavior                                                                                                                                                                                    | Verified by      | Tier | Where                                    |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---- | ---------------------------------------- |
| A callback with a missing or wrong bearer secret is rejected, and the endpoint fails loudly rather than open when its secret is unset.                                                      | GML-067, GML-068 | A    | `gmail.featuretests.test.ts`             |
| A callback naming a job owned by another user, or a job-scoped event with no job id, is rejected before any state changes — a second, independent ownership check beyond the shared secret. | GML-069, GML-070 | A    | `gmailImportWebhook.test.ts`             |
| Unknown event types and extra fields are rejected by a strict schema, and a callback carrying a stale scheduling token is acknowledged and ignored rather than applied.                     | GML-071, GML-072 | A    | `gmail.featuretests.test.ts`             |
| Every pipeline task fails at load time when its required configuration is absent, rather than midway through a user's import.                                                               | GML-100          | A    | `gmail-task-config.featuretests.test.ts` |
| The deployed pipeline's callbacks reach the deployment the user is actually on.                                                                                                             | GML-201          | C    | `gmail.integration.test.ts`              |

### Quotas, privacy, and data lifecycle

| Behavior                                                                                                                                                                   | Verified by      | Tier | Where                        |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---- | ---------------------------- |
| Where paid work is metered, a scan reserves its quota unit, a second scan within 30 seconds is refused, and a seventh scan in one UTC day exceeds the daily allowance.     | GML-021, GML-022 | A    | `gmail.featuretests.test.ts` |
| The import allowance matches the historical receipt cap, so a full archive selection is admitted in one reservation rather than being refused partway.                     | GML-023          | A    | `gmail.featuretests.test.ts` |
| A data export includes sync state, jobs, and drafts with its exclusions declared — and the realtime query the wizard subscribes to withholds exactly the same identifiers. | GML-075          | A    | `gmail.featuretests.test.ts` |
| The purge cron removes per-message candidate and outcome rows for aged jobs while preserving the aggregate counters, and never touches an active job.                      | GML-065, GML-066 | A    | `gmail.featuretests.test.ts` |

### The wizard

| Behavior                                                                                                                                                                                              | Verified by               | Tier | Where                                                                           |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ---- | ------------------------------------------------------------------------------- |
| Every hook argument the wizard sends satisfies the validator on the other side, for all seven sync actions and all six job mutations.                                                                 | GML-110, GML-111          | B    | `useGmailSync.featuretests.test.tsx`, `useGmailImportJob.featuretests.test.tsx` |
| Every job status maps to a step the user can actually reach, and a deep link into a job resolves to the right step for each status and mode.                                                          | GML-112, GML-113          | B    | `useGmailWizardController.featuretests.test.ts`                                 |
| Each of the twelve wizard steps renders its loading, empty, error, and data states; scan progress is clamped to 0-100 on screen; and cancel is offered only while the import is actually cancellable. | GML-114, GML-115, GML-117 | B    | `GmailWizard.test.tsx`                                                          |
| Both Gmail routes are blocked without the entitlement, and onboarding only offers "Find Past Orders" when the flag allows it.                                                                         | GML-127, GML-128          | B    | `GmailWizard.test.tsx`                                                          |
| Reopening the app mid-import returns to the same job and the same pending draft.                                                                                                                      | GML-230                   | C    | `gmail.integration.test.ts`                                                     |

## What the live tier proves end-to-end

The Gmail live lane runs against one real, shared mailbox with a per-user pipeline queue,
so it is serialized and the disconnect cases run last.

* **The connection is real and correctly scoped**, and the deployed backend can mint a
  Google token for it — the whole lane fails fast if not — GML-200, GML-201.
* **A narrow scan completes end to end**, its candidate ledger landing before the scan
  declares itself complete — GML-202, GML-203.
* **Classification behaves on real mail**: personal-domain candidates are decided without
  an AI call, splitting into genuine receipts and vetoed lifecycle mail, while generic
  candidates do go through AI — GML-205, GML-206.
* **Import produces a draft, not an order.** One selected receipt yields one draft; the
  user's commit then creates one order with every line item, classified by job kind and
  firing its post-save effects — GML-207, GML-208, GML-209, GML-224, GML-225.
* **Nothing imports twice.** Re-scanning the same window imports nothing new, one-tap
  confirm commits only fully-resolved drafts and is idempotent, and a discarded draft
  leaves no order behind — GML-212, GML-210, GML-211, ORD-228.
* **A non-receipt stays a non-receipt.** A marketing or review-request email yields the
  empty outcome instead of a fabricated order — GML-213.
* **Interruption is survivable.** Concurrent scans collapse onto one run, cancelling
  mid-import stops draft creation, resuming after a failure produces a fresh generation,
  and retry re-imports only what failed — GML-218, GML-219, GML-220, GML-221.
* **Quick Sync tells the truth about itself**: a first sync uses the fallback window, and a
  refused sync is distinguishable from a started one, naming the job that blocked it —
  GML-216, GML-231.

## Deliberate gaps & conditions

<Warning>
  A gap listed here is a decision or a known condition, not coverage. Nothing below is
  evidence that the behavior works.
</Warning>

* **Disconnect and reconnect are opt-in.** GML-222 and GML-223 run only with
  `TIER_C_GMAIL_ALLOW_DISCONNECT=1`, because disconnecting revokes the grant on the shared
  mailbox and reconnecting may need an interactive OAuth flow. They run last when enabled.
  Skipping is the deliberate default, not a config gap.
* **Two incremental-sync cases cannot run on this mailbox.** GML-215 (the cursor advancing)
  and GML-217 (a second sync finding nothing) need a sync whose discovery is empty. Starting
  a sync has no switch to restrict it to the user's own shops, so it always also searches
  the crowdsourced domain layer and discovery is never empty here. Un-skipping needs a
  product change and a pipeline deploy, not an environment variable — and the cursor must
  never be hand-reset to force it.
* **The metered import ceiling cannot be observed live.** Staging is structurally unmetered,
  so GML-228 has no observable form there; GML-023 is the only place that contract is
  asserted.
* **The full two-thousand-candidate archive scan stays manual.** GML-204 is a twenty-minute
  run on a shared pipeline queue; the chunking contract it would exercise is proven
  deterministically by GML-085.
* **The purge cron is not run live.** GML-229 needs an aged fixture plus a cron wait; the
  purge behavior itself is proven by GML-065 and GML-066.
* **Nothing becomes an order without review** — by design. The only exception is one-tap
  confirm, and it is restricted to drafts where every strain resolved (GML-037).
* **The subject veto is a heuristic over the subject line only.** It is deliberately
  positioned upstream of the parser rather than replacing it: the parser's own
  monetary-evidence and cancelled-purchase gates remain the second and third layers, and are
  specified under [Orders & Receipts](/planning/testing/specs/orders).

## Where to extend

A new Gmail behavior gets its test in the tier that can see it: backend logic, validators,
ownership, and webhook handling in `apps/mobile/convex/tests/gmail.featuretests.test.ts`;
pipeline-task behavior in the co-located
`packages/trigger/src/tasks/gmail/*.featuretests.test.ts` beside the task, or in
`receipt-subject.test.ts` for what counts as a receipt subject; hook and wizard contracts in
the co-located `*.featuretests.test.ts(x)` or in
`apps/mobile/test/ui/features/GmailWizard.test.tsx`; real-mailbox journeys in
`apps/mobile/test/integration/suites/gmail.integration.test.ts`.

Anything added to the live suite must respect its ordering: the lane is serialized on one
shared mailbox, and the disconnect cases stay last.

Give the case a `GML-` id, add its matrix row, and follow the tier and naming rules on
[Mobile Feature Test Platform](/planning/testing/feature-test-platform).
