@crvouga/mockingbird-service-junction 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +647 -143
- package/dist/chunk-C2UGIPPM.js +145758 -0
- package/dist/chunk-C2UGIPPM.js.map +7 -0
- package/dist/chunk-ISDNA2TU.js +23 -0
- package/dist/chunk-ISDNA2TU.js.map +7 -0
- package/dist/chunk-KLBGX23P.js +474 -0
- package/dist/chunk-KLBGX23P.js.map +7 -0
- package/dist/chunk-NFWUEPW4.js +8849 -0
- package/dist/chunk-NFWUEPW4.js.map +7 -0
- package/dist/cli.js +214 -0
- package/dist/cli.js.map +7 -0
- package/dist/corpus.d.ts +105 -0
- package/dist/corpus.js +8 -0
- package/dist/corpus.js.map +7 -0
- package/dist/index.d.ts +947 -35
- package/dist/index.js +97 -6082
- package/dist/index.js.map +4 -4
- package/dist/server.d.ts +1210 -0
- package/dist/server.js +16 -0
- package/dist/server.js.map +7 -0
- package/package.json +23 -4
package/README.md
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
# @crvouga/mockingbird-service-junction
|
|
2
2
|
|
|
3
|
-
Stateful
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
Stateful mock of the [Junction (formerly Vital) API](https://docs.junction.com/): users
|
|
4
|
+
(`/v2/user`), the lab-testing catalog, lab orders (create, cancel, simulate, results,
|
|
5
|
+
requisitions), at-home phlebotomy and patient-service-center (PSC) scheduling, and the
|
|
6
|
+
`labtest.order.*` / `labtest.appointment.updated` webhooks. All 40 operations in the vendored
|
|
7
|
+
OpenAPI subset are served, and behaviour is checked by differential property tests against the
|
|
8
|
+
Junction sandbox.
|
|
9
9
|
|
|
10
|
-
Use it
|
|
11
|
-
|
|
10
|
+
Use it to take Junction off your suite's critical path: no sandbox key, no shared 50-user sandbox
|
|
11
|
+
cap (the mock enforces no sandbox limit unless a test [asks for one](#sandbox-limits)), no
|
|
12
|
+
cross-suite interference. Serve it as a local origin with one command, or run it in-process.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx mockingbird-junction serve # http://127.0.0.1:8787, recorded sandbox corpus loaded
|
|
16
|
+
```
|
|
12
17
|
|
|
13
18
|
- Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/SUPPORT.md)
|
|
14
|
-
- Drop-in readiness
|
|
19
|
+
- Drop-in readiness and the sandbox quirks the mock mirrors:
|
|
15
20
|
[docs/drop-in.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/docs/drop-in.md)
|
|
16
|
-
- Consumer wiring checklist: [docs/qa-followon.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/docs/qa-followon.md)
|
|
17
21
|
- Behaviour notes: [docs/behavior.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/docs/behavior.md)
|
|
22
|
+
- **What the mock does not model:** [below](#what-is-and-is-not-modelled). Read it before trusting a
|
|
23
|
+
green suite.
|
|
18
24
|
|
|
19
25
|
## Install
|
|
20
26
|
|
|
@@ -22,22 +28,125 @@ run offline, deterministically, with no sandbox key.
|
|
|
22
28
|
npm install -D @crvouga/mockingbird-service-junction
|
|
23
29
|
```
|
|
24
30
|
|
|
25
|
-
ESM only
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
ESM only; Node >= 22 or Bun >= 1.2 (CommonJS callers on Node >= 22.12 can `require()` it). No
|
|
32
|
+
native dependencies: state lives in an in-memory SQLite engine written in TypeScript, bundled in.
|
|
33
|
+
|
|
34
|
+
| Entry point | Runtime | What it is |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `@crvouga/mockingbird-service-junction` | any (Node, Bun, Workers, browsers) | `JunctionAPI`, `createRuntime`, corpus and verify tools |
|
|
37
|
+
| `@crvouga/mockingbird-service-junction/server` | Node | `createServer()` — a listening HTTP server |
|
|
38
|
+
| `@crvouga/mockingbird-service-junction/corpus` | any | `defaultCorpus` — the recording shipped in the package (a separate entry, so importing the mock never parses it) |
|
|
39
|
+
| `mockingbird-junction` (bin) | Node | `serve`, `corpus pull`, `corpus diff`, `verify` |
|
|
28
40
|
|
|
29
41
|
## Usage
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
### `mockingbird-junction serve`
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx mockingbird-junction serve --port 8787
|
|
47
|
+
npx mockingbird-junction serve --corpus ./test/junction-corpus.json --lab-accounts ./test/lab-accounts.json
|
|
48
|
+
npx mockingbird-junction serve --webhook-url http://127.0.0.1:3100/webhooks/junction --webhook-secret whsec_...
|
|
49
|
+
npx mockingbird-junction serve --help
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| Flag | Default | Meaning |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `--port <n>` | `8787` | Port to listen on (`0` for any free port; the bound URL is printed) |
|
|
55
|
+
| `--host <host>` | `127.0.0.1` | Interface to bind |
|
|
56
|
+
| `--corpus <default\|none\|file>` | `default` | The shipped corpus, no corpus (synthetic data), or a file from `corpus pull` |
|
|
57
|
+
| `--geo <corpus\|synthetic>` | `corpus` with a corpus | How unknown ZIPs are answered; see [Geo](#geo-corpus-or-synthetic) |
|
|
58
|
+
| `--lab-accounts <file>` | built-in fixtures | [Lab accounts](#lab-accounts): a JSON array, or `{ "presets": [...], "accounts": [...] }` |
|
|
59
|
+
| `--team-id <uuid>` | the corpus's team | The [team](#team-identity) the mock answers as |
|
|
60
|
+
| `--max-users <n>` | unlimited | Enforce the sandbox's [live-user cap](#sandbox-limits) |
|
|
61
|
+
| `--identity <strict\|adopt-users>` | `strict` | Unknown `user_id`s: 404, or [create on first use](#fixtures-and-identity) |
|
|
62
|
+
| `--fixtures <file>` | — | [Users and orders](#fixtures-and-identity) every namespace starts with |
|
|
63
|
+
| `--journal-size <n>` | `1000` | Requests each namespace's [journal](#is-this-the-mock) keeps |
|
|
64
|
+
| `--webhook-url <url>` / `--webhook-secret <whsec_…>` | off | Deliver [signed webhooks](#webhooks) (secret also from `MOCKINGBIRD_JUNCTION_WEBHOOK_SECRET`) |
|
|
65
|
+
| `--webhook-retry-delays <ms,…>` | Svix's schedule | Delay before each delivery attempt, e.g. `0,1000,5000` in tests |
|
|
66
|
+
| `--webhook-scope <scope>` | — | Sent as `x-mockingbird-scope` on every delivery |
|
|
67
|
+
| `--admin-key <key>` | open | Require `x-mockingbird-admin-key` on `/__admin/*` (also `MOCKINGBIRD_ADMIN_KEY`) |
|
|
68
|
+
| `--seed <seed>` | `0` | Seeds fault rates and retry jitter |
|
|
69
|
+
| `--log <pretty\|json\|off>` | `pretty` | One line per request: operation id, status, duration, namespace, fault |
|
|
70
|
+
| `--log-requests` | — | Same as `--log json`: one JSON line per request with the ids it touched, never bodies |
|
|
71
|
+
| `--seed-url <url>` / `--seed-key <key>` | — | Pull a corpus from a live team at boot. Slow and a live dependency; prefer a committed `corpus pull` file |
|
|
72
|
+
| `--config <file>` | — | Serve every service in a `mockingbird.json` instead ([below](#many-services-from-one-config)) |
|
|
73
|
+
|
|
74
|
+
At startup it prints the listen address, the loaded corpus (its version label, observation, ZIP,
|
|
75
|
+
lab-test and lab-account counts, recording date and source), the geo mode, the team id, the
|
|
76
|
+
lab-account count, which sandbox limits are on (normally none), the identity mode, and whether
|
|
77
|
+
webhook delivery is on.
|
|
78
|
+
|
|
79
|
+
### Many services from one config
|
|
80
|
+
|
|
81
|
+
Any Mockingbird service's CLI can boot every service in a config file, so a stack adds a config
|
|
82
|
+
entry per vendor instead of a wrapper process per vendor. Each service named must be installed
|
|
83
|
+
(`junction` loads `@crvouga/mockingbird-service-junction`).
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"log": "json",
|
|
88
|
+
"services": {
|
|
89
|
+
"junction": {
|
|
90
|
+
"port": 8787,
|
|
91
|
+
"adminKey": "local-admin",
|
|
92
|
+
"options": { "corpus": "./test/junction-corpus.json", "webhook-url": "http://127.0.0.1:3100/webhooks/junction" }
|
|
93
|
+
},
|
|
94
|
+
"stripe": { "port": 12111 }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npx mockingbird-junction serve --config mockingbird.json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`options` takes the service's own `serve` flags by long name.
|
|
104
|
+
|
|
105
|
+
### `createServer` (Node)
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { createServer } from "@crvouga/mockingbird-service-junction/server"
|
|
109
|
+
|
|
110
|
+
const server = await createServer() // shipped corpus, any free port
|
|
111
|
+
const health = await fetch(`${server.url}/health`)
|
|
112
|
+
console.log(health.status) // 200
|
|
113
|
+
|
|
114
|
+
// Point the Vital SDK (or your backend's Junction base URL) at server.url, then:
|
|
115
|
+
await server.close()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`createServer(options)` takes every [`createRuntime`](#createruntime-any-fetch-server) option plus
|
|
119
|
+
`port` (default `0`), `host` (default `127.0.0.1`) and `corpus` (`"default"`, `"none"`, a file path
|
|
120
|
+
or a loaded corpus). The result has `url`, `port`, `runtime` and `close()`.
|
|
121
|
+
|
|
122
|
+
### `createRuntime` (any Fetch server)
|
|
123
|
+
|
|
124
|
+
`createRuntime` is the whole served mock — health, admin, namespaces, clock, faults, metrics — as a
|
|
125
|
+
single runtime-neutral `fetch(request)`. Hand it to `Bun.serve`, a Worker, or call it directly:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { createRuntime } from "@crvouga/mockingbird-service-junction"
|
|
129
|
+
import { defaultCorpus } from "@crvouga/mockingbird-service-junction/corpus"
|
|
130
|
+
|
|
131
|
+
const junction = createRuntime({ corpus: defaultCorpus })
|
|
132
|
+
const as = (worker: string) => ({
|
|
133
|
+
"x-vital-api-key": "sk_us_mockingbird",
|
|
134
|
+
"x-mockingbird-namespace": worker, // isolates this worker's data
|
|
135
|
+
"content-type": "application/json",
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
await junction.fetch(
|
|
139
|
+
new Request("http://junction.local/v2/user", {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: as("worker-1"),
|
|
142
|
+
body: JSON.stringify({ client_user_id: "patient-1" }),
|
|
143
|
+
}),
|
|
144
|
+
)
|
|
145
|
+
const other = await junction.fetch(new Request("http://junction.local/v2/user", { headers: as("worker-2") }))
|
|
146
|
+
console.log(await other.json()) // worker-2 sees none of worker-1's users
|
|
32
147
|
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
look like `sk_us_*` / `sk_eu_*`). Without it the response is `401 {"detail":"Missing x-vital-api-key"}`.
|
|
36
|
-
State is not partitioned by key: all requests share one team (`team_id`
|
|
37
|
-
`11111111-1111-4111-8111-111111111111`).
|
|
38
|
-
- Bodies are JSON with the API's snake_case field names.
|
|
39
|
-
- A default synthetic catalog is installed at construction (lab tests with methods
|
|
40
|
-
`walk_in_test`, `testkit` and `at_home_phlebotomy`); `installCorpus` replaces it with a recording.
|
|
148
|
+
await junction.reset("worker-1") // or junction.reset("*") for every namespace
|
|
149
|
+
```
|
|
41
150
|
|
|
42
151
|
### In-process (inject `fetch`)
|
|
43
152
|
|
|
@@ -95,169 +204,559 @@ console.log(junction.webhookEvents().map((event) => event.event_type)) // ["labt
|
|
|
95
204
|
simulations (`POST /v3/order/{id}/test?final_status=completed&delay=<seconds>`) become due; the
|
|
96
205
|
delayed transition is applied on the next `GET /v3/order/{id}` whose `now` is past the due time.
|
|
97
206
|
|
|
98
|
-
###
|
|
207
|
+
### Pointing `@tryvital/vital-node` at it
|
|
99
208
|
|
|
100
|
-
|
|
101
|
-
|
|
209
|
+
The official SDK takes the base URL as `environment` (this is how the package's SDK drop-in test
|
|
210
|
+
constructs it):
|
|
102
211
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
212
|
+
```js
|
|
213
|
+
import { VitalClient } from "@tryvital/vital-node"
|
|
214
|
+
|
|
215
|
+
const client = new VitalClient({ apiKey: "sk_us_mockingbird", environment: baseUrl })
|
|
216
|
+
const user = await client.user.create({ clientUserId: "app-user-1" })
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
If your app guards Junction hosts with an allowlist, allow `127.0.0.1` / `localhost` for tests
|
|
220
|
+
while keeping the production and sandbox guards intact.
|
|
221
|
+
|
|
222
|
+
## The service contract
|
|
223
|
+
|
|
224
|
+
Every Mockingbird service answers the same control surface, outside the vendor's auth gate:
|
|
225
|
+
|
|
226
|
+
- `GET /health` — unauthenticated readiness probe:
|
|
227
|
+
`{ "status": "ok", "service": "junction", "corpus": "<label>", "geo": "corpus", … }`.
|
|
228
|
+
- `/__admin/*` — the control plane. Open by default; with `--admin-key` / `adminKey` it requires
|
|
229
|
+
`x-mockingbird-admin-key`, which is separate from any vendor key. Admin errors have one shape,
|
|
230
|
+
`{ "error": { "type": "mockingbird_admin", "message": "…" } }`, so they can never be confused
|
|
231
|
+
with a Junction error. `GET /__admin` lists every route.
|
|
232
|
+
- `x-mockingbird-namespace: <name>` — isolates a request's data (`[A-Za-z0-9_.-]{1,64}`). Each
|
|
233
|
+
namespace is a separate team over one shared database, so parallel workers share one process
|
|
234
|
+
without seeing each other. The corpus is shared read-only; creating a namespace is cheap. Admin
|
|
235
|
+
routes take the namespace from `?namespace=`, then the header, then `default`. Configuration set
|
|
236
|
+
through the admin API (lab accounts, limits, identity, geo) belongs to one namespace and survives
|
|
237
|
+
that namespace's `POST /__admin/reset`; data does not.
|
|
238
|
+
- `x-mockingbird: junction@<version>; ns=<namespace>` — on **every** response: vendor answers,
|
|
239
|
+
errors, faults, admin and health. Junction never sends it, so it tells the mock from the vendor
|
|
240
|
+
(see [Is this the mock?](#is-this-the-mock)).
|
|
241
|
+
|
|
242
|
+
| Route | Does |
|
|
243
|
+
| --- | --- |
|
|
244
|
+
| `POST /__admin/reset` | Reset the request's namespace; `?all=1` resets every namespace |
|
|
245
|
+
| `GET /__admin/namespaces` | Namespaces created so far |
|
|
246
|
+
| `POST /__admin/snapshots` | Snapshot the namespace; returns `{ id }` |
|
|
247
|
+
| `POST /__admin/snapshots/{id}/restore` | Restore it (an assignment: later writes are discarded) |
|
|
248
|
+
| `DELETE /__admin/snapshots/{id}` | Forget a snapshot |
|
|
249
|
+
| `GET` / `POST /__admin/clock` | Read or move the clock: `{ "set": "2030-01-01T00:00:00Z" }`, `{ "advance": "2h" }`, `{ "freeze": true }`, `{ "reset": true }` |
|
|
250
|
+
| `GET` / `POST` / `DELETE /__admin/faults` | List, add or clear [fault rules](#faults-and-error-shapes) (`DELETE ?id=` removes one) |
|
|
251
|
+
| `POST /__admin/faults/presets/{name}` | Add a named Junction fault; body may override `count`, `rate`, `operationId`… |
|
|
252
|
+
| `GET /__admin/metrics` | Requests by operation and status, fault count, and **unmatched routes** |
|
|
253
|
+
| `GET /__admin/orders` | Orders in the namespace with their current status |
|
|
254
|
+
| `POST /__admin/orders/{id}/transition` | Move an order: `{ "to": "completed", "result": "abnormal" }` ([order control](#order-control)) |
|
|
255
|
+
| `GET` / `PUT /__admin/results/{id}` | Read or install an exact result payload / PDF for an order |
|
|
256
|
+
| `GET /__admin/result-fixtures` | The named results |
|
|
257
|
+
| `GET` / `PUT /__admin/lab-accounts` | Read or replace the namespace's [lab accounts](#lab-accounts) (`{ "accounts": [...], "presets": [...] }`; `{ "accounts": null }` restores the default) |
|
|
258
|
+
| `POST /__admin/lab-accounts` | Add one account; 409 if the id exists |
|
|
259
|
+
| `PATCH` / `DELETE /__admin/lab-accounts/{id}` | Merge fields into an account (`{ "status": "suspended" }`, `{ "states": [...] }`) / remove it; 404 if absent |
|
|
260
|
+
| `GET /__admin/lab-accounts/presets` | Every [preset](#lab-account-presets), with its full record |
|
|
261
|
+
| `POST /__admin/lab-accounts/presets/{name}` | Add a preset account; body `{ "id": "…" }` overrides its id |
|
|
262
|
+
| `GET /__admin/team` | `{ "teamId" }` — the [team](#team-identity) the namespace answers as |
|
|
263
|
+
| `GET` / `PUT /__admin/limits` | Read or change the namespace's [sandbox limits](#sandbox-limits) |
|
|
264
|
+
| `GET` / `PUT /__admin/identity` | Read or set `{ "mode": "strict" \| "adopt-users" }` ([identity](#fixtures-and-identity)) |
|
|
265
|
+
| `POST /__admin/users`, `POST /__admin/users/bulk` | Insert a user (or `{ "users": [...] }`, all or none) with a chosen `user_id`; 409 on a duplicate id or `client_user_id` |
|
|
266
|
+
| `DELETE /__admin/users/{id}` | Remove a user outright, leaving no deletion tombstone |
|
|
267
|
+
| `POST /__admin/orders` | Insert an order, optionally already in a `status`, through `create_order`'s record builder |
|
|
268
|
+
| `POST /__admin/import` | `{ "users": [...], "orders": [...] }` in one call |
|
|
269
|
+
| `GET` / `DELETE /__admin/requests` | The namespace's [request journal](#is-this-the-mock) (`?operationId=`, `?status=`, `?since=`, `?limit=`, `?all=1`) / clear it |
|
|
270
|
+
| `GET /__admin/corpus`, `PUT /__admin/geo` | The loaded corpus; switch [geo mode](#geo-corpus-or-synthetic) |
|
|
271
|
+
| `GET /__admin/webhooks` | [Deliveries](#webhooks) with every attempt |
|
|
272
|
+
| `POST /__admin/webhooks/{id}/replay`, `POST /__admin/webhooks/flush` | Redeliver one message; run pending retries now |
|
|
273
|
+
| `GET /__admin/webhooks/events` | Every event recorded, delivered or not |
|
|
274
|
+
|
|
275
|
+
`GET /__admin/metrics` → `unmatched` counts requests to paths the contract does not have. It is how
|
|
276
|
+
a new SDK call shows up — as a count — before it fails a suite as a 404.
|
|
277
|
+
|
|
278
|
+
## Corpus
|
|
279
|
+
|
|
280
|
+
Provider-owned inventory — area serviceability, PSC site lists, the lab catalog, labs, lab
|
|
281
|
+
accounts — cannot be synthesized faithfully. A corpus is an exact recording of those
|
|
282
|
+
parameter-stable reads, and the mock answers them byte for byte.
|
|
283
|
+
|
|
284
|
+
**The shipped corpus** (`defaultCorpus`, and what `serve` loads unless told otherwise) was recorded
|
|
285
|
+
from the Junction sandbox. It covers area and PSC serviceability for 57 ZIPs across the US, the PSC
|
|
286
|
+
labs those ZIPs reach, and the recording team's catalog. The catalog and lab accounts belong to the
|
|
287
|
+
**recording team**: lab-test ids are per team, so your product's ids will not be in it. For your
|
|
288
|
+
catalog and your lab accounts, record your own team and commit the file:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
npx mockingbird-junction corpus pull --real-key "$JUNCTION_SANDBOX_KEY" --out test/junction-corpus.json
|
|
292
|
+
npx mockingbird-junction corpus pull --real-key "$JUNCTION_SANDBOX_KEY" --base test/junction-corpus.json \
|
|
293
|
+
--zip 10001,94105 --coverage-only --out test/junction-corpus.json # add ZIPs to it
|
|
294
|
+
npx mockingbird-junction corpus diff test/junction-corpus.json /tmp/fresh.json # exit 1 when they differ
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`corpus pull` only issues GETs, refuses non-sandbox keys (`sk_us_…` / `sk_eu_…`) unless given
|
|
298
|
+
`--allow-any-key`, and stamps the file with a SHA-256 `fingerprint` of its content (not its
|
|
299
|
+
recording time), so re-pulling unchanged data yields the same fingerprint. Availability is never
|
|
300
|
+
recorded: its slot dates and single-use `booking_key`s cannot be replayed, so it always comes from
|
|
301
|
+
the deterministic generator.
|
|
302
|
+
|
|
303
|
+
### Geo: corpus or synthetic
|
|
304
|
+
|
|
305
|
+
With a corpus loaded, serviceability reads (`area/info`, `psc/info`, phlebotomy and PSC
|
|
306
|
+
availability) answer **only** for ZIPs the corpus covers. Any other well-formed ZIP gets:
|
|
307
|
+
|
|
308
|
+
```json
|
|
309
|
+
{
|
|
310
|
+
"detail": {
|
|
311
|
+
"error_type": "MOCKINGBIRD_UNKNOWN_ZIP",
|
|
312
|
+
"error_message": "ZIP 00501 is not in the loaded Junction corpus (57 ZIPs covered). Record it with `mockingbird-junction corpus pull --zip 00501`, or serve with --geo synthetic to invent coverage.",
|
|
313
|
+
"zip_code": "00501",
|
|
314
|
+
"corpus": "v1-2026-09-18-HqFDuEAuhkwD"
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
with status **424**, which Junction never sends and HTTP clients do not retry: a fixture gap fails
|
|
320
|
+
loudly instead of silently disagreeing with production. A malformed ZIP still gets Junction's own
|
|
321
|
+
422. `--geo synthetic` (the default without a corpus) invents plausible, deterministic coverage
|
|
322
|
+
instead — right for property tests that walk random ZIPs, wrong for a suite standing in for
|
|
323
|
+
production.
|
|
110
324
|
|
|
111
|
-
|
|
112
|
-
|
|
325
|
+
## Lab accounts
|
|
326
|
+
|
|
327
|
+
`create_order` routes, accepts and rejects `lab_account_id` against the team's lab accounts,
|
|
328
|
+
following Junction's documented rules: an explicit id must exist, be linked to the team, belong to
|
|
329
|
+
the ordered lab and be active; with no id, one active account for the lab is selected, several are
|
|
330
|
+
an error, and a lab with none falls back to Junction's platform account. The billing type must be
|
|
331
|
+
allowed in the patient's state by the account's `allowed_billing`.
|
|
332
|
+
|
|
333
|
+
The accounts come from, in order: `labAccounts` / `--lab-accounts`, else the corpus (a pulled
|
|
334
|
+
corpus carries your team's real accounts), else built-in fixtures. Configure them as
|
|
335
|
+
`ClientFacingLabAccount`s, with `states` as shorthand for client-bill states:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
import { createRuntime, US_STATES } from "@crvouga/mockingbird-service-junction"
|
|
339
|
+
|
|
340
|
+
const junction = createRuntime({
|
|
341
|
+
labAccounts: [
|
|
342
|
+
{ id: "acct-bioref-nynj", lab: "bioreference", states: ["NY", "NJ"] },
|
|
343
|
+
{
|
|
344
|
+
id: "acct-labcorp-47",
|
|
345
|
+
lab: "labcorp",
|
|
346
|
+
states: US_STATES.filter((state) => !["NY", "NJ", "RI"].includes(state)),
|
|
347
|
+
},
|
|
348
|
+
{ id: "acct-quest", lab: "quest", delegated_flow: "order_delegated" },
|
|
349
|
+
],
|
|
113
350
|
})
|
|
114
|
-
console.log(
|
|
351
|
+
console.log(junction.instance().labAccounts().length) // 3
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Rejections keep Junction's body shape, `400 {"detail": "Lab account is not associated with lab labcorp"}`.
|
|
355
|
+
`delegated_flow` is stored and listed, but see [not modelled](#what-is-and-is-not-modelled).
|
|
356
|
+
|
|
357
|
+
Availability reads that take a `lab_account_id` (`GET /v3/order/area/info`) check it against the
|
|
358
|
+
same live list, so an id `create_order` accepts is never a 404 there.
|
|
115
359
|
|
|
116
|
-
|
|
360
|
+
### Changing accounts at runtime
|
|
361
|
+
|
|
362
|
+
Each namespace has its own layout, kept across its own reset. Change one account at a time, or
|
|
363
|
+
replace them all:
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
curl -X POST localhost:8787/__admin/lab-accounts/presets/bioreference_ny_nj_delegated
|
|
367
|
+
curl -X PATCH localhost:8787/__admin/lab-accounts/$ID -d '{"status": "suspended"}'
|
|
368
|
+
curl -X POST localhost:8787/__admin/lab-accounts -d '{"id": "…", "lab": "quest", "states": ["AZ"]}'
|
|
369
|
+
curl -X DELETE localhost:8787/__admin/lab-accounts/$ID
|
|
370
|
+
curl -X PUT localhost:8787/__admin/lab-accounts -d '{"presets": ["quest_platform"], "accounts": []}'
|
|
117
371
|
```
|
|
118
372
|
|
|
119
|
-
|
|
120
|
-
|
|
373
|
+
A bad field is a 400 in the admin shape naming it, e.g. `lab account x: states: ZZ is not a US
|
|
374
|
+
state code`. `--lab-accounts` and the `labAccounts` option take the same layout, so a stack can
|
|
375
|
+
declare it at boot with no admin call:
|
|
121
376
|
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
const server = serve(
|
|
125
|
-
{ fetch: (request) => junction.fetch(request), port: 0, hostname: "127.0.0.1" },
|
|
126
|
-
(info) => console.log(`http://127.0.0.1:${info.port}`),
|
|
127
|
-
)
|
|
128
|
-
// ... later: server.close()
|
|
377
|
+
```json
|
|
378
|
+
{ "presets": ["bioreference_ny_nj_delegated", { "name": "quest_platform", "id": "…" }], "accounts": [] }
|
|
129
379
|
```
|
|
130
380
|
|
|
131
|
-
###
|
|
381
|
+
### Lab-account presets
|
|
132
382
|
|
|
133
|
-
|
|
134
|
-
|
|
383
|
+
Named after what they model; ids are `deterministicUuid("junction:lab-account:<name>")` unless
|
|
384
|
+
given. Where a preset copies an account recorded from a real BioReference-linked sandbox team
|
|
385
|
+
(2026-09-21), its billing states are the recording's. `PLATFORM_ACCOUNT_STATES` is every state
|
|
386
|
+
but NJ, NY and RI (47).
|
|
135
387
|
|
|
136
|
-
|
|
137
|
-
|
|
388
|
+
| Preset | `lab` | `delegated_flow` | `allowed_billing` | `team_id_allowlist` | Source |
|
|
389
|
+
| --- | --- | --- | --- | --- | --- |
|
|
390
|
+
| `bioreference_ny_nj_delegated` | `bioreference` | `order_delegated` | `client_bill`: NY, NJ | the team | requested shape, **not** a recorded account |
|
|
391
|
+
| `bioreference_delegated_multi_state` | `bioreference` | `order_delegated` | `client_bill`: every state but NY, NJ (48) | the team | recorded |
|
|
392
|
+
| `bioreference_customer_multi_state` | `bioreference` | `not_delegated` | `client_bill`: `PLATFORM_ACCOUNT_STATES` | `[]` | recorded ("Junction BioReference Account") |
|
|
393
|
+
| `bioreference_patient_bill_passthrough` | `bioreference` | `not_delegated` | `patient_bill_passthrough`: NJ, NY | the team | recorded |
|
|
394
|
+
| `quest_platform` / `labcorp_platform` | `quest` / `labcorp` | `not_delegated` | `client_bill`: `PLATFORM_ACCOUNT_STATES` | `[]` | recorded ("Junction Quest/Labcorp Account") |
|
|
395
|
+
| `suspended_bioreference` / `suspended_quest` / `suspended_labcorp` | as above | `not_delegated` | as the platform accounts | `[]` | the platform shapes with `status: "suspended"` |
|
|
138
396
|
|
|
139
|
-
|
|
140
|
-
|
|
397
|
+
Every preset is `status: "active"` unless named `suspended_*`. `GET /__admin/lab-accounts/presets`
|
|
398
|
+
returns the full records.
|
|
399
|
+
|
|
400
|
+
### Team identity
|
|
401
|
+
|
|
402
|
+
The mock answers as one team: `team_id` on users, orders and webhooks, and the team that
|
|
403
|
+
`team_id_allowlist` is checked against. It is, in order: `teamId` / `--team-id`, the team a
|
|
404
|
+
version-2 corpus recorded (`corpus pull` reads it from a listed user, else from the accounts'
|
|
405
|
+
allowlists), else the fixed `MOCK_TEAM_ID`. `GET /__admin/team` reports it.
|
|
406
|
+
|
|
407
|
+
A version-2 corpus keeps each account's allowlist **verbatim**, so an account the real team is not
|
|
408
|
+
linked to answers `400 {"detail": "Lab account is not linked to your team"}` exactly as it does
|
|
409
|
+
live. A version-1 corpus (no `teamId`) still loads and behaves as 0.2.0 did: every recorded account
|
|
410
|
+
is also linked to the mock team.
|
|
411
|
+
|
|
412
|
+
**Open question — empty allowlists.** Junction's own Quest, Labcorp and BioReference accounts carry
|
|
413
|
+
`team_id_allowlist: []`, and a real team's listing returns them, so the mock lists them and treats
|
|
414
|
+
them as linked. Whether *ordering* through one — by explicit id, or when it is one of several
|
|
415
|
+
active accounts for a lab with the id omitted — behaves the same live is **not verified**.
|
|
416
|
+
`verify --orders` checks it whenever the corpus has such an account; until a run records the
|
|
417
|
+
answer, a suite that depends on it should run against the sandbox too.
|
|
418
|
+
|
|
419
|
+
## Sandbox limits
|
|
420
|
+
|
|
421
|
+
The mock exists to escape the sandbox's restrictions, so it enforces none of them unless a test
|
|
422
|
+
asks — this is a tested guarantee (500 users in a loop all succeed), not an accident.
|
|
423
|
+
|
|
424
|
+
| Sandbox limit | Modelled | Default | Turn it on |
|
|
425
|
+
| --- | --- | --- | --- |
|
|
426
|
+
| 50 live users per team | yes, byte-exact body; deleted users free their slot | off (unlimited) | `limits: { maxUsers: 50 }`, `--max-users 50`, `PUT /__admin/limits {"maxUsers": 50}` |
|
|
427
|
+
| `POST /v3/order/{id}/test` only in sandbox | as a switch: refuses a key that is not `sk_us_…`/`sk_eu_…` (400, body not recorded) | off | `simulateRequiresSandbox: true` |
|
|
428
|
+
| Rate limits | as a per-namespace budget: 429 `{"detail":"Too Many Requests"}`, `retry-after: 1` (shape not recorded) | off | `rateLimitPerSecond: n` |
|
|
429
|
+
|
|
430
|
+
`PUT /__admin/limits` merges (`null` lifts a numeric limit) and is per namespace;
|
|
431
|
+
`GET /__admin/limits` returns the effective values. For a one-off failure instead of a standing
|
|
432
|
+
limit, the `sandbox_user_quota` [fault preset](#faults-and-error-shapes) is still there.
|
|
433
|
+
|
|
434
|
+
## Fixtures and identity
|
|
435
|
+
|
|
436
|
+
A suite often needs state the public API cannot build: database fixtures that already carry real
|
|
437
|
+
Junction `user_id`s, or an order already in some status. Insert them directly:
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
curl -X POST localhost:8787/__admin/users -d '{"user_id": "3f0c…", "client_user_id": "patient-1"}'
|
|
441
|
+
curl -X POST localhost:8787/__admin/orders -d '{"user_id": "3f0c…", "lab_test_id": "…", "status": "completed"}'
|
|
442
|
+
curl -X POST localhost:8787/__admin/import -d @test/junction-fixtures.json
|
|
141
443
|
```
|
|
142
444
|
|
|
143
|
-
|
|
144
|
-
|
|
445
|
+
- Users take `user_id?`, `client_user_id`, `created_on?`, `fallback_time_zone?`,
|
|
446
|
+
`fallback_birth_date?`, `ingestion_start?`, `ingestion_end?`. A duplicate `user_id` or
|
|
447
|
+
`client_user_id` is a 409; `POST /__admin/users/bulk` inserts all or none.
|
|
448
|
+
- Orders take `order_id?`, `user_id`, `lab_test_id`, `lab_account_id?`, `status?` (any
|
|
449
|
+
[transition target](#order-control)), `collection_method?`, `patient_details?`,
|
|
450
|
+
`patient_address?`, `billing_type?`, `created_at?` and `result_fixture?` (as
|
|
451
|
+
`PUT /__admin/results/{id}` takes it). Request validation is skipped; references are not — the
|
|
452
|
+
user and lab test must exist (404 otherwise).
|
|
453
|
+
- Inserted records go through the same builders as `create_user` / `create_order`, so every read
|
|
454
|
+
path (`GET /v2/user/{id}`, resolve by `client_user_id`, `GET /v3/order/{id}`,
|
|
455
|
+
`GET /v3/orders?user_id=`) sees them as API-created. Default timestamps read the mock clock.
|
|
456
|
+
- **No webhook fires** unless the body has `"emitWebhooks": true`: fixtures should not trigger
|
|
457
|
+
backend side effects.
|
|
458
|
+
- `fixtures` / `--fixtures <file>` loads `{ "users": [...], "orders": [...] }` into every namespace
|
|
459
|
+
at creation and again on each reset.
|
|
460
|
+
|
|
461
|
+
For flow suites that do not care about Junction identity, `identity: "adopt-users"` (or
|
|
462
|
+
`--identity adopt-users`, or `PUT /__admin/identity {"mode": "adopt-users"}` per namespace) creates
|
|
463
|
+
an unknown, well-formed `user_id` on first use — in `create_order` and `GET /v3/orders?user_id=` —
|
|
464
|
+
and the request proceeds. The journal marks the request `adopted: true`. **Orders are never
|
|
465
|
+
adopted**: an unknown order id always 404s, because inventing an order hides real bugs. The default,
|
|
466
|
+
`strict`, is Junction's own behaviour.
|
|
467
|
+
|
|
468
|
+
## Is this the mock?
|
|
469
|
+
|
|
470
|
+
A mock 404 and a sandbox 404 are byte-identical, which is how a backend half-pointed at the sandbox
|
|
471
|
+
goes unnoticed. Three things make it visible:
|
|
472
|
+
|
|
473
|
+
- **`x-mockingbird` on every response.** Assert on it in a test helper:
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
import { expect } from "vitest"
|
|
477
|
+
|
|
478
|
+
/** Wrap the fetch your Junction client uses; fails any call that did not reach the mock. */
|
|
479
|
+
export const mockOnlyFetch =
|
|
480
|
+
(inner: typeof fetch = fetch): typeof fetch =>
|
|
481
|
+
async (input, init) => {
|
|
482
|
+
const response = await inner(input, init)
|
|
483
|
+
expect(response.headers.get("x-mockingbird"), `${String(input)} did not reach the Junction mock`).toMatch(
|
|
484
|
+
/^junction@/,
|
|
485
|
+
)
|
|
486
|
+
return response
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
- **The request journal.** Each namespace keeps its last 1000 requests (`--journal-size`,
|
|
491
|
+
`journalSize`): operation id, method, path, status, duration, fault id, the user / order /
|
|
492
|
+
lab-account ids it touched, and when (mock clock). `GET /__admin/requests?operationId=create_order_v3_order_post`
|
|
493
|
+
proves an order was placed here; an empty answer proves it was not. Never bodies.
|
|
494
|
+
- **Miss headers on 404s.** A missing user or order keeps Junction's body byte for byte and adds
|
|
495
|
+
`x-mockingbird-miss: order <id>` (or `user <id>`, `user client:<client_user_id>`) and
|
|
496
|
+
`x-mockingbird-known: users=<n> orders=<n>`.
|
|
497
|
+
|
|
498
|
+
`--log-requests` prints the same fields as one JSON line per request.
|
|
499
|
+
|
|
500
|
+
## Order control
|
|
501
|
+
|
|
502
|
+
`simulate_order` (`POST /v3/order/{id}/test`) behaves like the sandbox's, including its stepwise
|
|
503
|
+
rules. For direct control, the admin API moves an order straight to a status, recording the same
|
|
504
|
+
event, dates and appointment cascade, and publishing the same webhook, a real transition does:
|
|
145
505
|
|
|
146
|
-
|
|
506
|
+
```bash
|
|
507
|
+
curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "collected"}'
|
|
508
|
+
curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "completed", "result": "critical"}'
|
|
509
|
+
curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "cancelled.testkit.do_not_process"}'
|
|
510
|
+
```
|
|
147
511
|
|
|
148
|
-
`
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
`
|
|
152
|
-
|
|
512
|
+
`to` is a full status (`completed.testkit.completed`), a `<phase>.<event>` pair, or a name that maps
|
|
513
|
+
onto the contract's own status for the order's collection method:
|
|
514
|
+
|
|
515
|
+
| Name | `at_home_phlebotomy` | `on_site_collection` | `testkit` | `walk_in_test` |
|
|
516
|
+
| --- | --- | --- | --- | --- |
|
|
517
|
+
| `requisition_created` | `received.…requisition_created` | same | same | same |
|
|
518
|
+
| `scheduled` | `collecting_sample.…appointment_scheduled` | — | — | `collecting_sample.…appointment_scheduled` |
|
|
519
|
+
| `shipped` / `delivered` | — | — | `collecting_sample.…transit_customer` / `…with_customer` | — |
|
|
520
|
+
| `collected` | `collecting_sample.…draw_completed` | `sample_with_lab.…draw_completed` | `collecting_sample.…transit_lab` | — |
|
|
521
|
+
| `at_lab` | — | `sample_with_lab.…draw_completed` | `sample_with_lab.…delivered_to_lab` | — |
|
|
522
|
+
| `partial_results` | `sample_with_lab.…partial_results` | same | — | same |
|
|
523
|
+
| `completed` / `corrected` / `cancelled` / `failed` | `completed.…completed` / `completed.…corrected` / `cancelled.…cancelled` / `failed.…sample_error` | same | same | same |
|
|
524
|
+
|
|
525
|
+
A name with no real status for the method (`collected` for a walk-in test) is refused with the
|
|
526
|
+
valid choices, never approximated. `result` picks a named result — `normal`, `abnormal`,
|
|
527
|
+
`critical`, `missing_results` — which steers the generator so the payload keeps Junction's exact
|
|
528
|
+
shape. For an exact payload, `PUT /__admin/results/{id}` with `{ "results": [...],
|
|
529
|
+
"missing_results": null, "interpretation": "critical", "pdf_base64": "…" }`.
|
|
530
|
+
|
|
531
|
+
There is no `isTestCancelled` in Junction's contract; cancelling produces the vendor's
|
|
532
|
+
`cancelled.<method>.cancelled` event and webhook, which is what a consumer derives that flag from.
|
|
533
|
+
|
|
534
|
+
**Time.** Every timestamp reads the runtime clock. `POST /__admin/clock {"advance": "2h"}` makes
|
|
535
|
+
delayed simulations (`?delay=<seconds>`) due and moves appointment windows, with no sleeping.
|
|
536
|
+
|
|
537
|
+
## Faults and error shapes
|
|
538
|
+
|
|
539
|
+
Error bodies the mock reproduces byte for byte, checked against the sandbox by `verify` and the
|
|
540
|
+
parity suite (`ORDER_NOT_FOUND` holds the unknown-order text per operation id):
|
|
541
|
+
|
|
542
|
+
| Case | Status | Body |
|
|
543
|
+
| --- | --- | --- |
|
|
544
|
+
| `GET /v2/user/{unknown id}` | 404 | `{"detail":"Not found"}` |
|
|
545
|
+
| `GET /v2/user/resolve/{unknown client_user_id}` | 404 | `{"detail":"User not found"}` |
|
|
546
|
+
| Unknown order: get order, requisition PDF | 404 | `{"detail":"This order doesn't exist"}` |
|
|
547
|
+
| Unknown order: cancel, simulate | 404 | `{"detail":"Order doesn't exist"}` |
|
|
548
|
+
| Unknown order: result, result metadata, result PDF | 404 | `{"detail":"Order not found"}` |
|
|
549
|
+
| Unknown order: every phlebotomy / PSC appointment route | 404 | `{"detail":"This order doesn't exist."}` (trailing period included) |
|
|
550
|
+
| Missing `x-vital-api-key` | 401 | `{"detail":"Missing x-vital-api-key"}` |
|
|
551
|
+
| Request validation | 422 | `{"detail":[{"type":…,"loc":[…],"msg":…,"input":…}]}` (Pydantic shape) |
|
|
552
|
+
| Business rule | 400 | `{"detail":"<message>"}` or `{"detail":{"error_type":"INVALID_REQUEST","error_message":"…"}}` |
|
|
553
|
+
|
|
554
|
+
Faults reproduce the sandbox's failure modes on demand, for retry paths and error handling:
|
|
555
|
+
|
|
556
|
+
| Preset | Fault | Fidelity |
|
|
557
|
+
| --- | --- | --- |
|
|
558
|
+
| `sandbox_user_quota` | `create_user` → 400 `{"detail":{"error_type":"INVALID_REQUEST","error_message":"You have reached the maximum of 50 Sandbox users"}}` | byte-exact, as observed from a sandbox team |
|
|
559
|
+
| `rate_limited` | 429 `{"detail":"Too Many Requests"}`, `retry-after: 1` | shape-plausible, not verified |
|
|
560
|
+
| `server_error` / `bad_gateway` / `unavailable` | 500 / 502 / 503 | shape-plausible, not verified |
|
|
153
561
|
|
|
154
562
|
```ts
|
|
155
|
-
import {
|
|
156
|
-
import { JunctionAPI } from "@crvouga/mockingbird-service-junction"
|
|
563
|
+
import { createRuntime, FAULT_PRESETS } from "@crvouga/mockingbird-service-junction"
|
|
157
564
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const junction = new JunctionAPI({
|
|
162
|
-
onWebhook: (event) => {
|
|
163
|
-
const timestamp = Math.floor(Date.now() / 1000).toString()
|
|
164
|
-
const body = JSON.stringify(event)
|
|
165
|
-
const key = Buffer.from(WEBHOOK_SECRET.replace(/^whsec_/, ""), "base64")
|
|
166
|
-
const signature = createHmac("sha256", key).update(`${timestamp}.${body}`).digest("base64")
|
|
167
|
-
void fetch(WEBHOOK_URL, {
|
|
168
|
-
method: "POST",
|
|
169
|
-
headers: {
|
|
170
|
-
"content-type": "application/json",
|
|
171
|
-
"svix-id": randomUUID(),
|
|
172
|
-
"svix-timestamp": timestamp,
|
|
173
|
-
"svix-signature": `v1,${signature}`,
|
|
174
|
-
},
|
|
175
|
-
body,
|
|
176
|
-
}).catch(() => undefined)
|
|
177
|
-
},
|
|
178
|
-
})
|
|
179
|
-
console.log(junction.webhookEvents().length) // 0
|
|
565
|
+
const junction = createRuntime()
|
|
566
|
+
junction.faults.add({ ...FAULT_PRESETS.sandbox_user_quota, id: "quota", count: 1 }) // next create_user only
|
|
567
|
+
junction.faults.add({ id: "flaky-orders", operationId: "create_order_v3_order_post", status: 503, rate: 0.25 })
|
|
180
568
|
```
|
|
181
569
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
`
|
|
570
|
+
A rule matches on `operationId`, `method`, `pathPrefix` and `namespace` (all optional), fires
|
|
571
|
+
`count` times or forever, for a `rate` of matching requests (seeded, so a run replays identically),
|
|
572
|
+
after an optional `delayMs`. A rule added through the admin API defaults to the calling namespace,
|
|
573
|
+
so one worker's injected failure never lands on another's request; one added in-process, as above,
|
|
574
|
+
applies to every namespace unless given a `namespace`. Over HTTP: `POST /__admin/faults` with the same fields, or
|
|
575
|
+
`POST /__admin/faults/presets/sandbox_user_quota`.
|
|
576
|
+
|
|
577
|
+
## Webhooks
|
|
185
578
|
|
|
186
|
-
|
|
579
|
+
With `webhooks` / `--webhook-url`, every event is delivered signed, with retries:
|
|
187
580
|
|
|
188
|
-
|
|
189
|
-
|
|
581
|
+
- **Standard Svix signatures**: `svix-id`, `svix-timestamp`, and `svix-signature: v1,<base64
|
|
582
|
+
HMAC-SHA256 of "<svix-id>.<svix-timestamp>.<body>">`, keyed by the base64 part of `whsec_…`. The
|
|
583
|
+
official `svix` verifier, and a receiver written for real Junction, accept it unchanged. (Signing
|
|
584
|
+
`"<timestamp>.<body>"` without the message id, as this package's own server script did before,
|
|
585
|
+
is not Svix-compatible.)
|
|
586
|
+
- Timestamps are wall-clock even when the mock clock is moved, because receivers reject timestamps
|
|
587
|
+
outside a few minutes of their own clock.
|
|
588
|
+
- Any 2xx is delivered; anything else is retried on Svix's schedule (immediately, 5 s, 5 min,
|
|
589
|
+
30 min, 2 h, 5 h, 10 h, 10 h) or `retryDelaysMs`. `POST /__admin/webhooks/flush` runs pending
|
|
590
|
+
retries now; `POST /__admin/webhooks/{id}/replay` redelivers one message.
|
|
190
591
|
|
|
191
592
|
```ts
|
|
192
|
-
import {
|
|
193
|
-
import { JunctionAPI } from "@crvouga/mockingbird-service-junction"
|
|
593
|
+
import { createRuntime, verifySvix } from "@crvouga/mockingbird-service-junction"
|
|
194
594
|
|
|
195
|
-
const
|
|
196
|
-
|
|
595
|
+
const secret = "whsec_bW9ja2luZ2JpcmQtdGVzdC1zZWNyZXQ="
|
|
596
|
+
const junction = createRuntime({
|
|
597
|
+
webhooks: { url: "http://127.0.0.1:3100/webhooks/junction", secret, retryDelaysMs: [0, 500, 2000] },
|
|
598
|
+
})
|
|
197
599
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
600
|
+
// In a test receiver:
|
|
601
|
+
const accept = async (request: Request) =>
|
|
602
|
+
verifySvix(
|
|
603
|
+
secret,
|
|
604
|
+
{
|
|
605
|
+
"svix-id": request.headers.get("svix-id") ?? "",
|
|
606
|
+
"svix-timestamp": request.headers.get("svix-timestamp") ?? "",
|
|
607
|
+
"svix-signature": request.headers.get("svix-signature") ?? "",
|
|
608
|
+
},
|
|
609
|
+
await request.text(),
|
|
203
610
|
)
|
|
204
|
-
|
|
205
|
-
})
|
|
611
|
+
console.log(typeof accept, junction.webhooks?.deliveries().length) // "function" 0
|
|
206
612
|
```
|
|
207
613
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
Provider-owned inventory (area serviceability, PSC site lists, the team's lab catalog, labs, lab
|
|
211
|
-
accounts) cannot be synthesized faithfully. A sealed corpus is an exact sandbox recording of those
|
|
212
|
-
parameter-stable GETs; installing it makes the mock answer them byte-for-byte. The recording in the
|
|
213
|
-
repo (`corpus/sandbox-sealed.json`) is not shipped in the npm package; copy it or record your own
|
|
214
|
-
(see Development).
|
|
614
|
+
## Verify
|
|
215
615
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
import { JunctionAPI, parseSealedCorpus } from "@crvouga/mockingbird-service-junction"
|
|
616
|
+
`verify` is the fidelity guarantee: it runs the same requests against real Junction and the mock
|
|
617
|
+
and reports every divergence.
|
|
219
618
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
junction
|
|
619
|
+
```bash
|
|
620
|
+
npx mockingbird-junction verify --real-key "$JUNCTION_SANDBOX_KEY" --corpus test/junction-corpus.json
|
|
621
|
+
npx mockingbird-junction verify --real-key "$JUNCTION_SANDBOX_KEY" --orders --sample 50 --json
|
|
223
622
|
```
|
|
224
623
|
|
|
225
|
-
|
|
226
|
-
|
|
624
|
+
- **Drift**: re-fetches each recorded observation (or `--sample n` of them) and reports any that no
|
|
625
|
+
longer match production — the signal to re-pull.
|
|
626
|
+
- **Scenario**: creates a user, reads it back, provokes the documented 404s (including every
|
|
627
|
+
order-scoped operation against a random unknown order), reads the catalog (paged and bare) and a
|
|
628
|
+
covered ZIP's serviceability, and deletes the user; with `--orders`, also places, reads and
|
|
629
|
+
cancels an order for the corpus's first lab test, and — when the corpus has an account with an
|
|
630
|
+
empty `team_id_allowlist` — orders through it with and without its id. Status codes must match;
|
|
631
|
+
bodies must match in shape, and byte for byte for the documented error bodies.
|
|
632
|
+
|
|
633
|
+
It exits 1 on any divergence. The scenario always deletes its user (and cancels its order), because
|
|
634
|
+
it runs against the same capped sandbox the mock exists to relieve. `verifyAgainstReal` is the same
|
|
635
|
+
check as a function. This repo runs it in CI against the Junction sandbox.
|
|
636
|
+
|
|
637
|
+
## Determinism
|
|
638
|
+
|
|
639
|
+
With a fixed clock (`clock` / `POST /__admin/clock {"set": …, "freeze": true}`) and `seed`, a run
|
|
640
|
+
replays exactly. Stable across runs: user ids, order ids, event ids, sample ids, booking keys and
|
|
641
|
+
webhook message ids (derived from sequence numbers), timestamps (from the clock), generated
|
|
642
|
+
availability, and which requests a partial-`rate` fault hits. Wall-clock by design: webhook
|
|
643
|
+
`svix-timestamp`, `/health` uptime, and request log durations.
|
|
644
|
+
|
|
645
|
+
## What is and is not modelled
|
|
646
|
+
|
|
647
|
+
A mock that is silent about its gaps is how a green suite starts lying. Specifically:
|
|
648
|
+
|
|
649
|
+
- **Corpus-backed** (byte-exact): area and PSC serviceability for covered ZIPs; the catalog, labs,
|
|
650
|
+
expected results and lab accounts of the team the corpus was recorded from.
|
|
651
|
+
- **Synthetic** (deterministic, shape-faithful, values invented): availability slots and booking
|
|
652
|
+
keys; results (biomarker lines and PDFs) unless a fixture is installed; requisition PDFs;
|
|
653
|
+
coverage for any ZIP in `--geo synthetic` mode.
|
|
654
|
+
- **Not modelled**:
|
|
655
|
+
- `delegated_flow`-specific ordering. Accounts carry the field and orders follow the documented
|
|
656
|
+
selection and billing rules above, but any additional behaviour Junction applies to delegated
|
|
657
|
+
accounts is not reproduced. Run `verify --orders` with your team's corpus to check what you rely on.
|
|
658
|
+
- Rate limits and outages, except when injected as faults or switched on as
|
|
659
|
+
[limits](#sandbox-limits).
|
|
660
|
+
- Whether an account with an empty `team_id_allowlist` can be ordered through (see
|
|
661
|
+
[Team identity](#team-identity)); the mock assumes it can.
|
|
662
|
+
- Adopted users (`adopt-users`) have `client_user_id` equal to their `user_id`; nothing in
|
|
663
|
+
Junction corresponds to adoption.
|
|
664
|
+
- Real 429 / 5xx bodies: the presets are plausible, not recorded.
|
|
665
|
+
- Phlebotomy availability breadth: the sandbox serves it only for `85004`; the mock generates slots
|
|
666
|
+
for any covered ZIP.
|
|
667
|
+
- Webhook retry jitter across a snapshot restore (the jitter stream is not part of a snapshot).
|
|
668
|
+
- Operations outside the vendored OpenAPI subset: they return 404 and are counted as
|
|
669
|
+
`unmatched` in `/__admin/metrics`.
|
|
227
670
|
|
|
228
671
|
## API
|
|
229
672
|
|
|
673
|
+
Main entry (`@crvouga/mockingbird-service-junction`, runtime-neutral):
|
|
674
|
+
|
|
230
675
|
| Export | Description |
|
|
231
676
|
| --- | --- |
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
677
|
+
| `createRuntime` | `(options?: JunctionRuntimeOptions) => JunctionRuntime` — the served mock (health, admin, namespaces, clock, faults, metrics, journal, webhooks) as one `fetch`. Options: `corpus`, `geo`, `labAccounts`, `teamId`, `limits`, `identity`, `fixtures`, `journalSize`, `webhooks`, `onWebhook`, `sqlite`, `clock`, `seed`, `adminKey`, `onLog`. |
|
|
678
|
+
| `JunctionAPI` | Class. `new JunctionAPI(options?)`: one namespace's mock, implementing `fetch(request: Request): Promise<Response>`. |
|
|
679
|
+
| `JUNCTION_NAMESPACE` | `"junction"` — the default namespace's storage key when sharing a `sqlite` client. |
|
|
234
680
|
| `document` | The vendored Junction OpenAPI document (Mockingbird subset) that drives routing. |
|
|
235
681
|
| `operationIds` | Every `operationId` in `document`. |
|
|
236
682
|
| `supportedOperationIds` | The `operationId`s the mock implements (all of them). |
|
|
683
|
+
| `pullCorpus` | `(options: PullCorpusOptions) => Promise<SealedCorpus>` — record a corpus from a real team (GETs only). |
|
|
684
|
+
| `diffCorpus` | `(before, after) => CorpusDiff` — ZIPs, lab tests, lab accounts and observations added / removed / changed. |
|
|
685
|
+
| `fingerprintCorpus` | `(corpus) => Promise<string>` — SHA-256 of a corpus's content. |
|
|
686
|
+
| `corpusLabel` | `(corpus) => string` — the short label `/health` and logs show, e.g. `v1-2026-09-18-HqFDuEAuhkwD`. |
|
|
237
687
|
| `parseSealedCorpus` | `(value: unknown) => SealedCorpus`; throws on a non-object or unsupported `version`. |
|
|
238
|
-
| `SEALED_CORPUS_VERSION` | Current
|
|
239
|
-
| `
|
|
240
|
-
| `
|
|
688
|
+
| `SEALED_CORPUS_VERSION` | Current corpus format version (`2`: adds `teamId`). |
|
|
689
|
+
| `SUPPORTED_SEALED_CORPUS_VERSIONS` | Versions `parseSealedCorpus` loads (`[1, 2]`). |
|
|
690
|
+
| `MOCK_TEAM_ID` | The team id used when neither `teamId` nor the corpus gives one. |
|
|
691
|
+
| `OTHER_TEAM_ID` | A team that is not the mock's, for fixtures modelling another team's account. |
|
|
692
|
+
| `isLinkedToTeam` | `(account, teamId) => boolean` — the allowlist rule (`[]` counts as linked). |
|
|
693
|
+
| `LAB_ACCOUNT_PRESETS` | The [lab-account presets](#lab-account-presets), without ids. |
|
|
694
|
+
| `presetAccountId` | `(name) => string` — a preset's default id. |
|
|
695
|
+
| `PLATFORM_ACCOUNT_STATES` / `DELEGATED_ACCOUNT_STATES` | The recorded 47- and 48-state client-bill lists. |
|
|
696
|
+
| `LabAccountConflict` | Thrown by `addLabAccount` for an id that exists. |
|
|
697
|
+
| `DEFAULT_LIMITS` | Every [sandbox limit](#sandbox-limits), off. |
|
|
698
|
+
| `sandboxUserQuotaBody` | `(max) => body` — the sandbox's user-cap 400 body. |
|
|
699
|
+
| `IDENTITY_MODES` | `["strict", "adopt-users"]`. |
|
|
700
|
+
| `FixtureError` | Thrown by the fixture inserts; `status` is 400, 404 or 409. |
|
|
701
|
+
| `ORDER_NOT_FOUND` | Junction's unknown-order `detail`, by operation id. |
|
|
702
|
+
| `MISS_HEADER` / `KNOWN_HEADER` | `"x-mockingbird-miss"` / `"x-mockingbird-known"`. |
|
|
703
|
+
| `verifyAgainstReal` | `(options: VerifyOptions) => Promise<VerifyReport>` — the `verify` command as a function. |
|
|
704
|
+
| `DEFAULT_JUNCTION_BASE_URL` | `"https://api.sandbox.tryvital.io"`. |
|
|
705
|
+
| `isSandboxKey` / `SANDBOX_KEY_PREFIXES` | Whether a key is a sandbox team key (`sk_us_` / `sk_eu_`). |
|
|
706
|
+
| `UNKNOWN_ZIP_STATUS` / `UNKNOWN_ZIP_ERROR_TYPE` | `424` / `"MOCKINGBIRD_UNKNOWN_ZIP"` — the corpus-mode unknown-ZIP error. |
|
|
707
|
+
| `labAccountFromInput` | `(input: LabAccountInput) => LabAccountRecord` — normalize (and validate) a configured account. |
|
|
708
|
+
| `TEAM_LAB_ACCOUNTS` | The built-in lab-account fixtures. |
|
|
709
|
+
| `US_STATES` | The 50 `USState` codes Junction accepts. |
|
|
710
|
+
| `ORDER_STATUSES_BY_METHOD` | Every order status in the contract, by collection method. |
|
|
711
|
+
| `TRANSITION_ALIASES` | The transition names above, by method. |
|
|
712
|
+
| `resolveTransition` | `(method, target) => { status } \| { error }` — how `/__admin/orders/{id}/transition` reads `to`. |
|
|
713
|
+
| `RESULT_FIXTURES` | Named results (`normal`, `abnormal`, `critical`, `missing_results`). |
|
|
714
|
+
| `FAULT_PRESETS` | Named Junction faults (`sandbox_user_quota`, `rate_limited`, `server_error`, `bad_gateway`, `unavailable`). |
|
|
715
|
+
| `createWebhookDispatcher` | `(endpoint: WebhookEndpoint) => WebhookDispatcher` — signed delivery with retries, standalone. |
|
|
716
|
+
| `signSvix` / `verifySvix` | Produce / check a standard Svix `svix-signature`. |
|
|
717
|
+
| `observationCacheKey` | `(url, method = "GET", body?) => string` — the `"<METHOD> <path>?<sorted query> <json>"` key corpora use. |
|
|
718
|
+
| `prefetchCoverageObservations` | Parity helper: records coverage-ZIP reads from the sandbox into an observation cache before `seedFrom`. |
|
|
241
719
|
| `reshapeCoverageGeoCommand` | Parity-walk hook that pins geo/availability parameters onto the coverage corpus. |
|
|
242
|
-
| `COVERAGE_ZIPS` |
|
|
720
|
+
| `COVERAGE_ZIPS` | The ZIPs the shipped corpus covers. |
|
|
243
721
|
| `PSC_AVAILABILITY_ZIPS` | ZIPs that also get PSC availability observations. |
|
|
244
|
-
| `PHLEBOTOMY_AVAILABILITY_ZIPS` | ZIPs
|
|
722
|
+
| `PHLEBOTOMY_AVAILABILITY_ZIPS` | ZIPs the sandbox serves phlebotomy availability for (`["85004"]`). |
|
|
245
723
|
| `PSC_LAB_IDS` | Lab ids used for PSC reads (`[4, 6, 13, 25]`). |
|
|
246
724
|
| `AVAILABILITY_ADDRESS` | Stable Phoenix, AZ address used in availability requests so cache keys match. |
|
|
247
725
|
| `AVAILABILITY_START_DATE` | Far-future availability `start_date` (`"2099-06-15"`). |
|
|
248
726
|
|
|
727
|
+
`./server` (Node): `createServer(options?) => Promise<JunctionServer>`, `serveTarget` (what
|
|
728
|
+
`serve` and `serve --config` use), `loadCorpus(source)`, `DEFAULT_PORT` (`8787`).
|
|
729
|
+
`./corpus`: `defaultCorpus`.
|
|
730
|
+
|
|
731
|
+
`JunctionRuntime` (from `createRuntime`) members: `fetch`, `instance(namespace?)`, `namespaces()`,
|
|
732
|
+
`reset(namespace? | "*")`, `snapshot(namespace?)`, `restore(snapshot, namespace?)`, `clock`
|
|
733
|
+
(`now`, `set`, `advance`, `freeze`, `unfreeze`, `reset`, `state`), `faults` (`add`, `list`,
|
|
734
|
+
`remove`, `clear`), `metrics` (`report`, `reset`), `journal` (`list`, `clear`, `size`), `webhooks`
|
|
735
|
+
(`deliveries`, `replay`, `flush`, `idle`, `clear`), `sqlite`, `rng`.
|
|
736
|
+
|
|
249
737
|
`JunctionAPI` members:
|
|
250
738
|
|
|
251
739
|
| Member | Description |
|
|
252
740
|
| --- | --- |
|
|
253
741
|
| `fetch(request)` | Handle one Junction REST request. |
|
|
254
|
-
| `reset()` | `Promise<void>` — clear state; re-install the default catalog
|
|
255
|
-
| `installCorpus(corpus)` | Install a `SealedCorpus
|
|
742
|
+
| `reset()` | `Promise<void>` — clear state; re-install the corpus or default catalog, and the configured lab accounts. |
|
|
743
|
+
| `installCorpus(corpus)` | Install a `SealedCorpus`: observations (shared, read-only), catalog, labs, lab accounts, ZIP coverage. |
|
|
744
|
+
| `corpusInfo()` | `CorpusInfo \| undefined` — label, recording date, source and counts. |
|
|
745
|
+
| `geoMode` | `"corpus" \| "synthetic"`, settable. |
|
|
746
|
+
| `configureLabAccounts(layout?)` / `labAccounts()` | Replace (a list, or `{ presets, accounts }`; `undefined` restores the default) / read the team's lab accounts. |
|
|
747
|
+
| `addLabAccount(input)` / `addLabAccountPreset(name, id?)` / `patchLabAccount(id, patch)` / `removeLabAccount(id)` | Change one account; kept across `reset()`. |
|
|
748
|
+
| `teamId` | The team this namespace answers as. |
|
|
749
|
+
| `limits` / `configureLimits(input)` | Read / change the sandbox limits; kept across `reset()`. |
|
|
750
|
+
| `identity` | `"strict" \| "adopt-users"`, settable. |
|
|
751
|
+
| `insertUsers(users)` / `insertOrders(orders, { emitWebhooks? })` / `importFixtures(fixtures, options?)` / `hardDeleteUser(id)` | The [fixture backdoor](#fixtures-and-identity). |
|
|
752
|
+
| `order(id)` / `orders()` | Read orders. |
|
|
753
|
+
| `transitionOrder(id, status, { now, flags? })` | Move an order to a full status and publish its webhook. |
|
|
754
|
+
| `installResultFixture(orderId, fixture)` / `resultFixture(orderId)` | Serve an exact result payload / PDF for an order. |
|
|
256
755
|
| `seedFrom(source, observations?)` | `Promise<SeedReport>` — copy users, orders, appointments, catalog and labs from a real Junction environment. |
|
|
257
756
|
| `ensureLabTests(source, ids)` / `ensureOrders(source, ids)` | `Promise<number>` — fetch specific lab tests / orders from `source` if missing. |
|
|
258
757
|
| `markUserDeleted(userId)` | Mark a user as deleted (subsequent reads behave like a deleted user). |
|
|
259
758
|
| `webhookEvents()` | `JunctionWebhookEvent[]`, oldest first. |
|
|
260
|
-
| `webhookDeliveryAttempts()` |
|
|
759
|
+
| `webhookDeliveryAttempts()` | The retry schedule Junction would follow per event (a record; see `webhooks` for delivery). |
|
|
261
760
|
| `app` / `sqlite` | The underlying Hono app and `SqliteClient`. |
|
|
262
761
|
|
|
263
762
|
Options and types:
|
|
@@ -265,21 +764,31 @@ Options and types:
|
|
|
265
764
|
```text
|
|
266
765
|
type JunctionAPIOptions = {
|
|
267
766
|
sqlite?: SqliteClient // share one client across services; default: fresh in-memory DB
|
|
767
|
+
namespace?: string // storage namespace; default "junction"
|
|
268
768
|
now?: () => number // clock in ms; default Date.now
|
|
769
|
+
corpus?: SealedCorpus // installed now and on every reset()
|
|
770
|
+
geo?: "corpus" | "synthetic" // default "corpus" with a corpus, else "synthetic"
|
|
771
|
+
labAccounts?: LabAccountInput[] | { presets?: (string | { name; id? })[]; accounts?: LabAccountInput[] }
|
|
772
|
+
teamId?: string // default: the corpus's recorded team, else MOCK_TEAM_ID
|
|
773
|
+
limits?: { maxUsers?: number | null; simulateRequiresSandbox?: boolean; rateLimitPerSecond?: number | null }
|
|
774
|
+
identity?: "strict" | "adopt-users" // default "strict"
|
|
775
|
+
fixtures?: { users?: UserFixture[]; orders?: OrderFixture[] } // loaded now and on every reset()
|
|
269
776
|
onWebhook?: WebhookPublisher // called with every recorded event
|
|
270
777
|
webhook?: JunctionWebhookOptions
|
|
271
778
|
}
|
|
272
|
-
type
|
|
779
|
+
type LabAccountInput = { id; lab; status?; delegated_flow?; account_name?; provider_account_id?; org_id?;
|
|
780
|
+
business_units?; default_clinical_notes?; allowed_billing?: Record<billingType, State[]>; states?: State[];
|
|
781
|
+
team_id_allowlist? }
|
|
782
|
+
type WebhookEndpoint = { url; secret: "whsec_…"; retryDelaysMs?: number[]; timeoutMs?; headers?; fetch? }
|
|
783
|
+
type ResultFixture = { name; results?: unknown[]; missing_results?: unknown[] | null; interpretation?; pdf_base64? }
|
|
784
|
+
type FaultRule = { id; operationId?; method?; pathPrefix?; status; body?; headers?; count?; rate?; delayMs? }
|
|
785
|
+
type JunctionWebhookOptions = { seed?: number; jitterRatio?: number /* 0..1, default 0.1 */; timeoutMs?: number }
|
|
273
786
|
type JunctionWebhookEvent = {
|
|
274
787
|
event_type: "labtest.order.created" | "labtest.order.updated" | "labtest.appointment.updated"
|
|
275
788
|
data: Record<string, unknown>; team_id: string; user_id: string; client_user_id: string
|
|
276
789
|
}
|
|
277
|
-
type
|
|
790
|
+
type SealedCorpus = { version: 1 | 2; recordedAt; source; teamId?; fingerprint?; observations; catalog: { labTests; labs; expectedResults }; labAccounts }
|
|
278
791
|
type SeedSource = { fetch: (request: Request) => Promise<Response>; baseUrl: string; headers: Record<string, string> }
|
|
279
|
-
type SeedObservations = { getCache?: ReadonlyMap<string, GetCacheEntry> }
|
|
280
|
-
type SeedReport = { users; orders; appointments; catalogTests; labs; cacheEntries: number }
|
|
281
|
-
type GetCacheEntry = { status: number; headers: Record<string, string>; body: unknown }
|
|
282
|
-
type SealedCorpus = { version: 1; recordedAt; source; observations; catalog: { labTests; labs; expectedResults }; labAccounts }
|
|
283
792
|
type OperationId / SupportedOperationId // string unions of operationIds / supportedOperationIds
|
|
284
793
|
```
|
|
285
794
|
|
|
@@ -299,23 +808,19 @@ console.log(report.users, report.orders)
|
|
|
299
808
|
|
|
300
809
|
## Development
|
|
301
810
|
|
|
302
|
-
For contributors to the mockingbird repo only
|
|
303
|
-
npm package.
|
|
811
|
+
For contributors to the mockingbird repo only.
|
|
304
812
|
|
|
305
813
|
```bash
|
|
306
814
|
bun test # offline suites, incl. scheduling state space
|
|
307
|
-
bun test junction.
|
|
815
|
+
bun test junction.runtime.property.test.ts # service contract, corpus, lab accounts, faults, webhooks, verify
|
|
308
816
|
bun test junction.sdk.property.test.ts # drives the served mock through @tryvital/vital-node
|
|
309
817
|
|
|
310
|
-
bun run mock:serve # HOST
|
|
311
|
-
|
|
312
|
-
bun run corpus:record
|
|
818
|
+
bun run mock:serve # env-configured `serve`: HOST, PORT, MOCKINGBIRD_JUNCTION_CORPUS,
|
|
819
|
+
# MOCKINGBIRD_JUNCTION_WEBHOOK_URL / _SECRET / _SCOPE
|
|
820
|
+
bun run corpus:record -- --force # re-record the shipped corpus/sandbox-sealed.json (Vault key)
|
|
821
|
+
bun run verify -- --real-key "$KEY" # `mockingbird-junction verify` from source
|
|
313
822
|
```
|
|
314
823
|
|
|
315
|
-
`mock:serve` installs `corpus/sandbox-sealed.json` when present, and signs webhooks to
|
|
316
|
-
`MOCKINGBIRD_JUNCTION_WEBHOOK_URL` with `MOCKINGBIRD_JUNCTION_WEBHOOK_SECRET` (delivery is disabled
|
|
317
|
-
without a secret). `/health` and `/__admin/reset` bypass the `x-vital-api-key` gate.
|
|
318
|
-
|
|
319
824
|
Live parity (primary proof is seedParity: warmup N on the sandbox, `seedFrom`, then lockstep M):
|
|
320
825
|
|
|
321
826
|
```bash
|
|
@@ -326,6 +831,5 @@ bun run parity -- --mode=empty # legacy empty-start differenti
|
|
|
326
831
|
|
|
327
832
|
Webhook parity needs a public receiver; see
|
|
328
833
|
[docs/webhook-parity.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/docs/webhook-parity.md).
|
|
329
|
-
The `client-parity*.ts` SDK scenarios are deprecated as proof and kept only as manual probes.
|
|
330
834
|
|
|
331
835
|
Part of [mockingbird](https://github.com/crvouga/mockingbird) — agent integration guide: [README](https://github.com/crvouga/mockingbird#readme) · [llms.txt](https://github.com/crvouga/mockingbird/blob/main/llms.txt).
|