# Test plan

Turn every contract clause into a case with an expected result written before the run, then test the final destination and the automations that read it, not only the transport. Start in a sandbox with records that cannot reach a customer. Template: [../assets/test-plan-template.csv](../assets/test-plan-template.csv).

## Evidence per layer

Every case records evidence at four layers:

- Source: the record or event ID that started it.
- Transport: the receipt, run or execution ID, and attempts.
- Destination: the record ID and the field values read back.
- Downstream: the workflow, sequence, journey or message log that shows what acted on it (or that nothing did).

A green run status is transport evidence only. Write the evidence in the `evidence` column as IDs and short descriptions, never payloads or personal data.

## Core cases (every flow)

- new_record: a new source record produces one destination record with only the owned fields set.
- update_owned: one owned field changes once; the revision or version advances.
- non_owned_untouched: a field this flow does not own keeps its manual value after an owned change.
- blank_semantics: four inputs, four results. Absent field: no change. Explicit clear from the owner: cleared. Null from a failed lookup: skipped and flagged. Zero: written as zero.
- duplicate_delivery: the same event delivered twice (resend, replay or a second Event object) has one business effect.
- out_of_order: a newer change then an older one; the destination keeps the newer state.
- unknown_outcome_retry: the worker times out after the destination accepted the write; the retry reads by key first and creates nothing.
- rate_limited: a forced 429 with Retry-After; the wait honours the header in the vendor's unit, backoff has jitter, nothing is lost.
- ambiguous_match: two destination records match; no write, an exception with both IDs.
- environment_check: correct account, sandbox against production endpoints, API version, credentials of the service identity.

## Conditional cases (the linter requires them when the trigger applies)

- inbound_webhook yes: signature_invalid (altered body, wrong secret, stale timestamp), unknown_event_type (acknowledged and stored), malformed_payload (rejected or quarantined per contract).
- batch_size above 1: partial_batch_failure (one bad record in a batch; the rest written; counts add up).
- creates_records yes: unmatched_record (exception, or create only where this flow owns creation).
- removal_policy other than none or not_applicable, or membership_contract current: removal (the destination applies the removal policy).
- tier 1: backfill_no_outreach (no messages, enrolments or tasks during the pilot), replay_no_duplicate (an overlapping replay repeats nothing and re-checks eligibility), credential_revoked (failure alert names the flow and runbook step; events kept for replay).
- side_effect message, or any field-map row with pii yes: consent_change (every destination stops sending within the freshness target).
- any field-map row two_way, or other_writers listed: loop_check (one write per side; no ping-pong; no-op writes skipped).

## Manual cases (add when they apply; the linter does not require them)

- merge: the crosswalk moves to the survivor; nothing recreates the merged-away record.
- delete: tombstones stop stale writes recreating it; erasure reaches every copy in scope.
- enum_unmapped: a value outside allowed_values is quarantined, not mapped to a default.
- schema_change: test the seven drift types (field added, removed, renamed, type changed, new enum value, changed relationship, changed meaning); the flow fails visibly or ignores new fields, never mis-maps silently.
- cross_account_isolation: a valid event naming another account or portal is rejected or routed to that account's partition only.
- permission_check: before go-live, every mapped field is read and written as the integration identity in the sandbox (Salesforce field-level security, HubSpot app scopes); a hidden or read-only field fails visibly instead of being skipped.
- late_change: on a scheduled read, a change whose modified time falls inside an already processed window is picked up by the next run (BF04).

## Environments and tools

- Sandbox first. Sandboxes differ from production: Stripe's sandbox retries three times over a few hours instead of up to three days, and its rate limit is 25 requests per second instead of 100.
- Stripe: `stripe listen` forwards test events to a local endpoint; a local forwarding test and a deployed endpoint test cover different boundaries.
- GA4 Measurement Protocol: send to the validation server first; its events never reach reports.
- Google Ads: `validate_only` on conversion uploads.
- HubSpot: a test account or developer test account; Salesforce: a sandbox org.
- n8n and Make: a copy of the workflow or scenario pointed at sandbox credentials, never the production version (n8n's MCP `execute_workflow` runs the published production version by default).

## Approval and live tests

- Any case that writes, in a sandbox or in production, runs only after the user approves that case.
- A case that runs in production also needs a named approver in the `approver` column (TP02), named test records and a cleanup list.
- Tell the owners which test records to exclude from reports and which test messages, tasks or conversions to expect.
- Record what rollback does not undo: messages sent, tasks created, conversions uploaded.

## Writing and judging results

- Write the expected value for every case before running it.
- Compare counts by period and status, then inspect identifiers, timestamps, currency and state.
- Keep unmatched records and explained exclusions; forcing totals to agree by dropping differences defeats the test.
- A FAIL stays a FAIL until fixed, or until the user approves an exception with its owner and it is recorded in `decisions.md`.

## Rules checked by the linter

- TP01 FAIL: a flow misses a mandatory case (the core set plus the conditional cases above). A flow with no rows fails with "no test cases".
- TP02 FAIL: environment production_approved with a blank approver.
- TP03 WARN: status fail or blocked.
- TP04 WARN: evidence blank.
