conformance/FLOWS.md defines setup, steps, captures and expectations, all observable at the contract boundary. flows.json carries setups (peer ids), fixed test keys, statute, scenario and explicit assign steps; scenarios.json holds the callback scenarios (the stub loads them). CONTRACT.md defines the case.assign response. Runner reads only the bundle and rejects undefined keys; new flow s-006 (held over cap). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 5787@bnt-lap001 Assistant-Session: 5db26b82-138b-40cc-9856-4d5eb0f752fd
93 lines
5 KiB
Markdown
93 lines
5 KiB
Markdown
# Conformance flows (mode contract v0.1)
|
|
|
|
`flows.json` describes complete conversations that an email-side
|
|
implementation must handle. A run needs only this bundle: `flows.json`,
|
|
`scenarios.json` and `statute.json`. Every expectation is observable at
|
|
the contract boundary: messages the stub parties receive, responses to
|
|
contract requests, and mail the email side sends. Nothing depends on how an
|
|
implementation stores data or answers the gateway.
|
|
|
|
## Running a flow
|
|
|
|
For each flow, start fresh:
|
|
|
|
1. **Email side.** An email side for the statute in `statute.json`
|
|
(namespace `modes.example`), configured with the flow's `setup`, i.e.
|
|
`setups[flow.setup].roles`. Its own peer id is `email`, and it knows the
|
|
keys in `peers`. A master role needs a store for its cases. The
|
|
reference uses its ticket service, but any store that creates a case on
|
|
intake mail and correlates mail to `ticket=ID` works.
|
|
2. **Callback stub.** Peer `callback-1` speaks the contract and plays
|
|
`scenarios.json`, with `flow.scenario` as its default. It sends nothing
|
|
until a `mode` step.
|
|
3. **Outside master stub.** Peer `cms-1` records every message it gets,
|
|
and sends commands only in `master` steps.
|
|
4. **Clock.** A controllable clock for the email side's timeouts. Only
|
|
`advance` steps move it. Signatures use real time.
|
|
5. **Send cap.** When `flow.max_sent` is set, the email side's send cap is
|
|
that number, and an SMTP sink counts what is actually sent. Without it
|
|
there is no cap.
|
|
|
|
Role configuration fields are those of CONTRACT.md §3. In a setup:
|
|
|
|
- `modes: {MODE: {"peer": ID}}` means the mode endpoint of that peer;
|
|
- `master_peer` is the outside master, which implies its event URL;
|
|
- `"<mode:ID>"` stands for the handoff URL of peer ID.
|
|
|
|
The implementation wires peer ids to its own URLs. The `peers` keys are
|
|
public **test keys**, never to be used in a deployment.
|
|
|
|
## Steps
|
|
|
|
| Step | Meaning |
|
|
|---|---|
|
|
| `{"mail": {to, text, dmarc?, subject?}}` | Inbound mail from `customer@example.net` to `TO@modes.example`, through the normal gateway path. `dmarc` (default `"pass"`) is the trusted Authentication-Results verdict; `null` means no verdict. |
|
|
| `{"mode": "run"}` | The callback stub sends every status that is due, until it is waiting, silent or terminal. |
|
|
| `{"advance": N}` | Move the email side's clock N seconds and let it run its timeouts. |
|
|
| `{"master": {"command": C, ...}}` | The stub master sends a signed contract command: `assign` (`case.assign`; `resource_id`, plus `intake_ref`, by default the captured one), `send` (`mail.send`; `in_reply_to`, `body`), `cancel` (`handoff.cancel`; `handoff_id`), or `policy` (`case.policy`). The case is the captured case. |
|
|
| `{"inject": {endpoint, as?, sign?, token?, message}}` | Send a crafted request straight to the email side: `endpoint` is `master`, `status` (the captured handoff's status URL) or `events`. `as` is the claimed peer id. `sign` is `valid` (default), `none`, `stale` (timestamp 400 s old) or `wrong-key`. `token: "bogus"` replaces the status URL token. In `message`, `"case": "current"` means the captured case, and the envelope (`mas_modes_version`, `id`) is added. |
|
|
|
|
Strings in steps may contain the captures `{case_id}`, `{message_ref}`,
|
|
`{intake_ref}` and `{handoff_id}`.
|
|
|
|
## Captures
|
|
|
|
Captures are updated after every step, from what that step produced, in
|
|
this order:
|
|
|
|
1. every message the mode stub and the master stub received;
|
|
2. the response to a `master` or `inject` step.
|
|
|
|
The latest value wins.
|
|
|
|
| Capture | From |
|
|
|---|---|
|
|
| case (`address`, `key`) | the `case` of any of those messages or responses |
|
|
| `{case_id}` | the resource id in the captured case address (`TYPE=ID@D`) |
|
|
| `{message_ref}` | `message_ref` of `case.message`, or of the `case.assign` response |
|
|
| `{intake_ref}` | `intake_ref` of `case.intake` |
|
|
| `{handoff_id}` | `handoff_id` of `handoff.request` (mode) or `handoff.requested` (master) |
|
|
|
|
## Expectations
|
|
|
|
`expect` is checked after the step. Every key listed must match.
|
|
|
|
| Key | Value |
|
|
|---|---|
|
|
| `event_accepted` | the email side answered the step's inbound mail event with 2xx |
|
|
| `mode_statuses` | statuses of every `handoff.status` the mode stub sent, in order |
|
|
| `mode_received` | number of contract messages the mode stub received |
|
|
| `mode_received_last` | `type` of the last one |
|
|
| `master_received` | `{type: count}` of messages the master stub received; only the listed types are compared |
|
|
| `relayed` | statuses of every `handoff.status` the master stub received, in order |
|
|
| `mails_sent` | number of mails the email side sent (held and refused mail excluded) |
|
|
| `last_mail_contains` | the body of the last mail sent contains this text |
|
|
| `response_ok` | `ok` in the response to the step's command |
|
|
| `response_reason` | `reason` in that response |
|
|
| `response_held` | `held` in that response (CONTRACT.md §6) |
|
|
| `case_address` | `case.address` in that response |
|
|
| `http_status` | HTTP status of that response |
|
|
| `error` | `error` in that response |
|
|
|
|
A runner MUST reject a flow that uses a step or expectation key not
|
|
defined here.
|