# Connector live verification

The engine behind `appmixer connector verify` — the live counterpart of
[`connector validate`](../connector-validators/README.md). Where validate
checks the SHAPE of the source offline, verify executes the connector's
behavior files (`require()` + `receive()`, no engine — see `context.js`)
against the real service API and checks what no static rule can see. Both
checks exist because real bugs shipped with every static gate green
(cliniko `medicare` never readable back; `type_code` labels inverted).

## Checks

- **schema-conformance** — declared outPort contract vs the live payload,
  compared as **leaf paths** (`from.username`, not just `from`) because the
  designer's variable picker offers every nested leaf as its own variable.
  Declared-but-absent = dead entry in the picker (FAIL);
  returned-but-undeclared = data no flow can reach (WARN). When the schema
  marks `required` (per level, JSON Schema style), only a *required* leaf that
  is absent fails; an *optional* leaf never observed only warns — polymorphic
  payloads (a Telegram message is a text OR a photo OR a document, and a
  user may have no `username`) would otherwise fail on every sample. A schema
  with no `required` anywhere keeps the strict reading: every leaf counts.
- **Where the declared contract comes from**: the `out` port's `schema` for a
  static port; for a **dynamic** one (`source` + `generateOutputPortOptions`)
  the behavior's `module.exports.ITEM_SCHEMA` — a complete JSON Schema
  (`{ type, required, properties }`), the same shape a static port declares.
  Without that export the port falls back to running
  `generateOutputPortOptions`, and the options list it emits has no place for
  `required`, so every field counts as mandatory: an optional one the API
  happened not to return is reported as a dead picker entry. Anything that is
  not a JSON Schema (a bare property map) or a behavior that fails to
  `require()` is ignored — the fallback still applies, the check never fails
  for reasons of its own.
- **triggers** take part through their `test()` method — the read-only sample
  fetch Flow Test Mode uses — configured through `inputs` mapped onto
  `context.properties`. A trigger without `test()` is skipped; one whose
  `test()` cancels ("no recent message") is skipped with that reason.
- **enum-roundtrip** (`--write`, opt-in — it creates records) — for a select
  input, create a record per option and read back the stored value AND the
  service's own label for it. Label comparison is the point: an inverted
  label/value map round-trips values perfectly.

## Fixtures — `<connector>/artifacts/verify.json`

Account-agnostic recipes, never concrete IDs:

```json
{
  "fixtures": { "businessId": { "from": "ListBusinesses", "path": "id" } },
  "read":     [ { "component": "FindX", "inputs": { "businessId": "{businessId}" } } ],
  "roundtrip": [ {
    "component": "CreateContact", "input": "typeCode",
    "base": { "lastName": "Verify Roundtrip" },
    "valueField": "type_code", "labelField": "type",
    "cleanup": { "component": "MakeApiCall",
                 "inputs": { "url": "/contacts/{id}/archive", "method": "POST" } }
  } ]
}
```

Cleanup runs per created record even when the check fails, through the
connector's own components. Without the file, verify still auto-discovers
read checks over every List/Find/Get component that needs no inputs.

## Recorded samples — `artifacts/samples/<Component>.json`

`--record` saves each output's SHAPE with every value replaced by a type
placeholder — live payloads carry PII and none of it may reach a repo
(`sanitizeSample` has a no-value-survives test). `--offline` then re-checks
schema conformance against the samples with no credentials, so that leg can
run in CI. Samples capture the COMPONENT's output (post-`expandIds`), i.e.
what flows actually see — not the raw API response.

A sample file holds a **list** of shapes (`{ "component", "samples": [...] }`;
the older single-`out` form still reads). Re-recording appends a shape only
when it differs from every one on file, so recording a text message, then a
photo, then a message from a user who has a username, builds up the union the
offline check compares against — "declared but never observed" is judged over
all samples together.

## Credentials

From the `appmixer test auth login` store (configstore key
`appmixer:<connector>`, `authFields`), or `--auth <file>` with a JSON object
of auth fields. `--offline` needs none. API-key connectors are fully
supported; OAuth works while the stored token is fresh (no refresh here).

Pilot connector: cliniko (appmixer-connectors#1220 — spec + samples).
