@crvouga/mockingbird-service-junction 0.1.2 → 0.2.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 +31 -0
- package/README.md +447 -144
- package/dist/chunk-4C6VYRJN.js +18 -0
- package/dist/chunk-4C6VYRJN.js.map +7 -0
- package/dist/chunk-KKNULV7X.js +406 -0
- package/dist/chunk-KKNULV7X.js.map +7 -0
- package/dist/chunk-MA3HSUVD.js +7771 -0
- package/dist/chunk-MA3HSUVD.js.map +7 -0
- package/dist/chunk-MD4KYEIM.js +145758 -0
- package/dist/chunk-MD4KYEIM.js.map +7 -0
- package/dist/cli.js +214 -0
- package/dist/cli.js.map +7 -0
- package/dist/corpus.d.ts +97 -0
- package/dist/corpus.js +8 -0
- package/dist/corpus.js.map +7 -0
- package/dist/index.d.ts +641 -2
- package/dist/index.js +65 -6082
- package/dist/index.js.map +4 -4
- package/dist/server.d.ts +978 -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,25 @@
|
|
|
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 39 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, no cross-suite interference. Serve it as a local origin with one command, or run it in-process.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx mockingbird-junction serve # http://127.0.0.1:8787, recorded sandbox corpus loaded
|
|
15
|
+
```
|
|
12
16
|
|
|
13
17
|
- Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/SUPPORT.md)
|
|
14
|
-
- Drop-in readiness
|
|
18
|
+
- Drop-in readiness and the sandbox quirks the mock mirrors:
|
|
15
19
|
[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
20
|
- Behaviour notes: [docs/behavior.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/junction/docs/behavior.md)
|
|
21
|
+
- **What the mock does not model:** [below](#what-is-and-is-not-modelled). Read it before trusting a
|
|
22
|
+
green suite.
|
|
18
23
|
|
|
19
24
|
## Install
|
|
20
25
|
|
|
@@ -22,22 +27,118 @@ run offline, deterministically, with no sandbox key.
|
|
|
22
27
|
npm install -D @crvouga/mockingbird-service-junction
|
|
23
28
|
```
|
|
24
29
|
|
|
25
|
-
ESM only
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
ESM only; Node >= 22 or Bun >= 1.2 (CommonJS callers on Node >= 22.12 can `require()` it). No
|
|
31
|
+
native dependencies: state lives in an in-memory SQLite engine written in TypeScript, bundled in.
|
|
32
|
+
|
|
33
|
+
| Entry point | Runtime | What it is |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `@crvouga/mockingbird-service-junction` | any (Node, Bun, Workers, browsers) | `JunctionAPI`, `createRuntime`, corpus and verify tools |
|
|
36
|
+
| `@crvouga/mockingbird-service-junction/server` | Node | `createServer()` — a listening HTTP server |
|
|
37
|
+
| `@crvouga/mockingbird-service-junction/corpus` | any | `defaultCorpus` — the recording shipped in the package (a separate entry, so importing the mock never parses it) |
|
|
38
|
+
| `mockingbird-junction` (bin) | Node | `serve`, `corpus pull`, `corpus diff`, `verify` |
|
|
28
39
|
|
|
29
40
|
## Usage
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
### `mockingbird-junction serve`
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx mockingbird-junction serve --port 8787
|
|
46
|
+
npx mockingbird-junction serve --corpus ./test/junction-corpus.json --lab-accounts ./test/lab-accounts.json
|
|
47
|
+
npx mockingbird-junction serve --webhook-url http://127.0.0.1:3100/webhooks/junction --webhook-secret whsec_...
|
|
48
|
+
npx mockingbird-junction serve --help
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| Flag | Default | Meaning |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `--port <n>` | `8787` | Port to listen on (`0` for any free port; the bound URL is printed) |
|
|
54
|
+
| `--host <host>` | `127.0.0.1` | Interface to bind |
|
|
55
|
+
| `--corpus <default\|none\|file>` | `default` | The shipped corpus, no corpus (synthetic data), or a file from `corpus pull` |
|
|
56
|
+
| `--geo <corpus\|synthetic>` | `corpus` with a corpus | How unknown ZIPs are answered; see [Geo](#geo-corpus-or-synthetic) |
|
|
57
|
+
| `--lab-accounts <file>` | built-in fixtures | JSON array of [lab accounts](#lab-accounts) |
|
|
58
|
+
| `--webhook-url <url>` / `--webhook-secret <whsec_…>` | off | Deliver [signed webhooks](#webhooks) (secret also from `MOCKINGBIRD_JUNCTION_WEBHOOK_SECRET`) |
|
|
59
|
+
| `--webhook-retry-delays <ms,…>` | Svix's schedule | Delay before each delivery attempt, e.g. `0,1000,5000` in tests |
|
|
60
|
+
| `--webhook-scope <scope>` | — | Sent as `x-mockingbird-scope` on every delivery |
|
|
61
|
+
| `--admin-key <key>` | open | Require `x-mockingbird-admin-key` on `/__admin/*` (also `MOCKINGBIRD_ADMIN_KEY`) |
|
|
62
|
+
| `--seed <seed>` | `0` | Seeds fault rates and retry jitter |
|
|
63
|
+
| `--log <pretty\|json\|off>` | `pretty` | One line per request: operation id, status, duration, namespace, fault |
|
|
64
|
+
| `--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 |
|
|
65
|
+
| `--config <file>` | — | Serve every service in a `mockingbird.json` instead ([below](#many-services-from-one-config)) |
|
|
66
|
+
|
|
67
|
+
At startup it prints the listen address, the loaded corpus (its version label, observation, ZIP,
|
|
68
|
+
lab-test and lab-account counts, recording date and source), the geo mode, and whether webhook
|
|
69
|
+
delivery is on.
|
|
70
|
+
|
|
71
|
+
### Many services from one config
|
|
72
|
+
|
|
73
|
+
Any Mockingbird service's CLI can boot every service in a config file, so a stack adds a config
|
|
74
|
+
entry per vendor instead of a wrapper process per vendor. Each service named must be installed
|
|
75
|
+
(`junction` loads `@crvouga/mockingbird-service-junction`).
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"log": "json",
|
|
80
|
+
"services": {
|
|
81
|
+
"junction": {
|
|
82
|
+
"port": 8787,
|
|
83
|
+
"adminKey": "local-admin",
|
|
84
|
+
"options": { "corpus": "./test/junction-corpus.json", "webhook-url": "http://127.0.0.1:3100/webhooks/junction" }
|
|
85
|
+
},
|
|
86
|
+
"stripe": { "port": 12111 }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx mockingbird-junction serve --config mockingbird.json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`options` takes the service's own `serve` flags by long name.
|
|
96
|
+
|
|
97
|
+
### `createServer` (Node)
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { createServer } from "@crvouga/mockingbird-service-junction/server"
|
|
101
|
+
|
|
102
|
+
const server = await createServer() // shipped corpus, any free port
|
|
103
|
+
const health = await fetch(`${server.url}/health`)
|
|
104
|
+
console.log(health.status) // 200
|
|
105
|
+
|
|
106
|
+
// Point the Vital SDK (or your backend's Junction base URL) at server.url, then:
|
|
107
|
+
await server.close()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`createServer(options)` takes every [`createRuntime`](#createruntime-any-fetch-server) option plus
|
|
111
|
+
`port` (default `0`), `host` (default `127.0.0.1`) and `corpus` (`"default"`, `"none"`, a file path
|
|
112
|
+
or a loaded corpus). The result has `url`, `port`, `runtime` and `close()`.
|
|
113
|
+
|
|
114
|
+
### `createRuntime` (any Fetch server)
|
|
115
|
+
|
|
116
|
+
`createRuntime` is the whole served mock — health, admin, namespaces, clock, faults, metrics — as a
|
|
117
|
+
single runtime-neutral `fetch(request)`. Hand it to `Bun.serve`, a Worker, or call it directly:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { createRuntime } from "@crvouga/mockingbird-service-junction"
|
|
121
|
+
import { defaultCorpus } from "@crvouga/mockingbird-service-junction/corpus"
|
|
122
|
+
|
|
123
|
+
const junction = createRuntime({ corpus: defaultCorpus })
|
|
124
|
+
const as = (worker: string) => ({
|
|
125
|
+
"x-vital-api-key": "sk_us_mockingbird",
|
|
126
|
+
"x-mockingbird-namespace": worker, // isolates this worker's data
|
|
127
|
+
"content-type": "application/json",
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
await junction.fetch(
|
|
131
|
+
new Request("http://junction.local/v2/user", {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: as("worker-1"),
|
|
134
|
+
body: JSON.stringify({ client_user_id: "patient-1" }),
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
const other = await junction.fetch(new Request("http://junction.local/v2/user", { headers: as("worker-2") }))
|
|
138
|
+
console.log(await other.json()) // worker-2 sees none of worker-1's users
|
|
32
139
|
|
|
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.
|
|
140
|
+
await junction.reset("worker-1") // or junction.reset("*") for every namespace
|
|
141
|
+
```
|
|
41
142
|
|
|
42
143
|
### In-process (inject `fetch`)
|
|
43
144
|
|
|
@@ -95,39 +196,6 @@ console.log(junction.webhookEvents().map((event) => event.event_type)) // ["labt
|
|
|
95
196
|
simulations (`POST /v3/order/{id}/test?final_status=completed&delay=<seconds>`) become due; the
|
|
96
197
|
delayed transition is applied on the next `GET /v3/order/{id}` whose `now` is past the due time.
|
|
97
198
|
|
|
98
|
-
### Over HTTP
|
|
99
|
-
|
|
100
|
-
```ts
|
|
101
|
-
import { JunctionAPI } from "@crvouga/mockingbird-service-junction"
|
|
102
|
-
|
|
103
|
-
const junction = new JunctionAPI()
|
|
104
|
-
const server = Bun.serve({
|
|
105
|
-
port: 0, // ephemeral
|
|
106
|
-
hostname: "127.0.0.1",
|
|
107
|
-
fetch: (request) => junction.fetch(request),
|
|
108
|
-
})
|
|
109
|
-
const baseUrl = `http://127.0.0.1:${server.port}`
|
|
110
|
-
|
|
111
|
-
const response = await fetch(`${baseUrl}/v2/user`, {
|
|
112
|
-
headers: { "x-vital-api-key": "sk_us_mockingbird" },
|
|
113
|
-
})
|
|
114
|
-
console.log(response.status) // 200
|
|
115
|
-
|
|
116
|
-
server.stop()
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
On Node use any Fetch-style server, e.g. `@hono/node-server` (`npm install -D @hono/node-server`),
|
|
120
|
-
whose callback receives the bound port:
|
|
121
|
-
|
|
122
|
-
```js
|
|
123
|
-
import { serve } from "@hono/node-server"
|
|
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()
|
|
129
|
-
```
|
|
130
|
-
|
|
131
199
|
### Pointing `@tryvital/vital-node` at it
|
|
132
200
|
|
|
133
201
|
The official SDK takes the base URL as `environment` (this is how the package's SDK drop-in test
|
|
@@ -143,121 +211,355 @@ const user = await client.user.create({ clientUserId: "app-user-1" })
|
|
|
143
211
|
If your app guards Junction hosts with an allowlist, allow `127.0.0.1` / `localhost` for tests
|
|
144
212
|
while keeping the production and sandbox guards intact.
|
|
145
213
|
|
|
146
|
-
|
|
214
|
+
## The service contract
|
|
147
215
|
|
|
148
|
-
|
|
149
|
-
(`labtest.order.created`, `labtest.order.updated`, `labtest.appointment.updated`). Delivery and
|
|
150
|
-
signing are up to you. Junction delivers through Svix; the contributor server script signs with
|
|
151
|
-
`svix-id`, `svix-timestamp` and `svix-signature: v1,<base64 HMAC-SHA256 of "<timestamp>.<body>">`,
|
|
152
|
-
keyed by the base64-decoded secret after stripping a `whsec_` prefix:
|
|
216
|
+
Every Mockingbird service answers the same control surface, outside the vendor's auth gate:
|
|
153
217
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
218
|
+
- `GET /health` — unauthenticated readiness probe:
|
|
219
|
+
`{ "status": "ok", "service": "junction", "corpus": "<label>", "geo": "corpus", … }`.
|
|
220
|
+
- `/__admin/*` — the control plane. Open by default; with `--admin-key` / `adminKey` it requires
|
|
221
|
+
`x-mockingbird-admin-key`, which is separate from any vendor key. Admin errors have one shape,
|
|
222
|
+
`{ "error": { "type": "mockingbird_admin", "message": "…" } }`, so they can never be confused
|
|
223
|
+
with a Junction error. `GET /__admin` lists every route.
|
|
224
|
+
- `x-mockingbird-namespace: <name>` — isolates a request's data (`[A-Za-z0-9_.-]{1,64}`). Each
|
|
225
|
+
namespace is a separate team over one shared database, so parallel workers share one process
|
|
226
|
+
without seeing each other. The corpus is shared read-only; creating a namespace is cheap. Admin
|
|
227
|
+
routes take the namespace from `?namespace=`, then the header, then `default`.
|
|
157
228
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
229
|
+
| Route | Does |
|
|
230
|
+
| --- | --- |
|
|
231
|
+
| `POST /__admin/reset` | Reset the request's namespace; `?all=1` resets every namespace |
|
|
232
|
+
| `GET /__admin/namespaces` | Namespaces created so far |
|
|
233
|
+
| `POST /__admin/snapshots` | Snapshot the namespace; returns `{ id }` |
|
|
234
|
+
| `POST /__admin/snapshots/{id}/restore` | Restore it (an assignment: later writes are discarded) |
|
|
235
|
+
| `DELETE /__admin/snapshots/{id}` | Forget a snapshot |
|
|
236
|
+
| `GET` / `POST /__admin/clock` | Read or move the clock: `{ "set": "2030-01-01T00:00:00Z" }`, `{ "advance": "2h" }`, `{ "freeze": true }`, `{ "reset": true }` |
|
|
237
|
+
| `GET` / `POST` / `DELETE /__admin/faults` | List, add or clear [fault rules](#faults-and-error-shapes) (`DELETE ?id=` removes one) |
|
|
238
|
+
| `POST /__admin/faults/presets/{name}` | Add a named Junction fault; body may override `count`, `rate`, `operationId`… |
|
|
239
|
+
| `GET /__admin/metrics` | Requests by operation and status, fault count, and **unmatched routes** |
|
|
240
|
+
| `GET /__admin/orders` | Orders in the namespace with their current status |
|
|
241
|
+
| `POST /__admin/orders/{id}/transition` | Move an order: `{ "to": "completed", "result": "abnormal" }` ([order control](#order-control)) |
|
|
242
|
+
| `GET` / `PUT /__admin/results/{id}` | Read or install an exact result payload / PDF for an order |
|
|
243
|
+
| `GET /__admin/result-fixtures` | The named results |
|
|
244
|
+
| `GET` / `PUT /__admin/lab-accounts` | Read or replace the namespace's [lab accounts](#lab-accounts) (`{ "accounts": null }` restores the default) |
|
|
245
|
+
| `GET /__admin/corpus`, `PUT /__admin/geo` | The loaded corpus; switch [geo mode](#geo-corpus-or-synthetic) |
|
|
246
|
+
| `GET /__admin/webhooks` | [Deliveries](#webhooks) with every attempt |
|
|
247
|
+
| `POST /__admin/webhooks/{id}/replay`, `POST /__admin/webhooks/flush` | Redeliver one message; run pending retries now |
|
|
248
|
+
| `GET /__admin/webhooks/events` | Every event recorded, delivered or not |
|
|
249
|
+
|
|
250
|
+
`GET /__admin/metrics` → `unmatched` counts requests to paths the contract does not have. It is how
|
|
251
|
+
a new SDK call shows up — as a count — before it fails a suite as a 404.
|
|
252
|
+
|
|
253
|
+
## Corpus
|
|
254
|
+
|
|
255
|
+
Provider-owned inventory — area serviceability, PSC site lists, the lab catalog, labs, lab
|
|
256
|
+
accounts — cannot be synthesized faithfully. A corpus is an exact recording of those
|
|
257
|
+
parameter-stable reads, and the mock answers them byte for byte.
|
|
258
|
+
|
|
259
|
+
**The shipped corpus** (`defaultCorpus`, and what `serve` loads unless told otherwise) was recorded
|
|
260
|
+
from the Junction sandbox. It covers area and PSC serviceability for 57 ZIPs across the US, the PSC
|
|
261
|
+
labs those ZIPs reach, and the recording team's catalog. The catalog and lab accounts belong to the
|
|
262
|
+
**recording team**: lab-test ids are per team, so your product's ids will not be in it. For your
|
|
263
|
+
catalog and your lab accounts, record your own team and commit the file:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
npx mockingbird-junction corpus pull --real-key "$JUNCTION_SANDBOX_KEY" --out test/junction-corpus.json
|
|
267
|
+
npx mockingbird-junction corpus pull --real-key "$JUNCTION_SANDBOX_KEY" --base test/junction-corpus.json \
|
|
268
|
+
--zip 10001,94105 --coverage-only --out test/junction-corpus.json # add ZIPs to it
|
|
269
|
+
npx mockingbird-junction corpus diff test/junction-corpus.json /tmp/fresh.json # exit 1 when they differ
|
|
180
270
|
```
|
|
181
271
|
|
|
182
|
-
`
|
|
183
|
-
|
|
184
|
-
|
|
272
|
+
`corpus pull` only issues GETs, refuses non-sandbox keys (`sk_us_…` / `sk_eu_…`) unless given
|
|
273
|
+
`--allow-any-key`, and stamps the file with a SHA-256 `fingerprint` of its content (not its
|
|
274
|
+
recording time), so re-pulling unchanged data yields the same fingerprint. Availability is never
|
|
275
|
+
recorded: its slot dates and single-use `booking_key`s cannot be replayed, so it always comes from
|
|
276
|
+
the deterministic generator.
|
|
277
|
+
|
|
278
|
+
### Geo: corpus or synthetic
|
|
279
|
+
|
|
280
|
+
With a corpus loaded, serviceability reads (`area/info`, `psc/info`, phlebotomy and PSC
|
|
281
|
+
availability) answer **only** for ZIPs the corpus covers. Any other well-formed ZIP gets:
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"detail": {
|
|
286
|
+
"error_type": "MOCKINGBIRD_UNKNOWN_ZIP",
|
|
287
|
+
"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.",
|
|
288
|
+
"zip_code": "00501",
|
|
289
|
+
"corpus": "v1-2026-09-18-HqFDuEAuhkwD"
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
185
293
|
|
|
186
|
-
|
|
294
|
+
with status **424**, which Junction never sends and HTTP clients do not retry: a fixture gap fails
|
|
295
|
+
loudly instead of silently disagreeing with production. A malformed ZIP still gets Junction's own
|
|
296
|
+
422. `--geo synthetic` (the default without a corpus) invents plausible, deterministic coverage
|
|
297
|
+
instead — right for property tests that walk random ZIPs, wrong for a suite standing in for
|
|
298
|
+
production.
|
|
187
299
|
|
|
188
|
-
|
|
189
|
-
catalog, or the corpus passed to `installCorpus` if there was one:
|
|
300
|
+
## Lab accounts
|
|
190
301
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
302
|
+
`create_order` routes, accepts and rejects `lab_account_id` against the team's lab accounts,
|
|
303
|
+
following Junction's documented rules: an explicit id must exist, be linked to the team, belong to
|
|
304
|
+
the ordered lab and be active; with no id, one active account for the lab is selected, several are
|
|
305
|
+
an error, and a lab with none falls back to Junction's platform account. The billing type must be
|
|
306
|
+
allowed in the patient's state by the account's `allowed_billing`.
|
|
194
307
|
|
|
195
|
-
|
|
196
|
-
|
|
308
|
+
The accounts come from, in order: `labAccounts` / `--lab-accounts`, else the corpus (a pulled
|
|
309
|
+
corpus carries your team's real accounts), else built-in fixtures. Configure them as
|
|
310
|
+
`ClientFacingLabAccount`s, with `states` as shorthand for client-bill states:
|
|
197
311
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
312
|
+
```ts
|
|
313
|
+
import { createRuntime, US_STATES } from "@crvouga/mockingbird-service-junction"
|
|
314
|
+
|
|
315
|
+
const junction = createRuntime({
|
|
316
|
+
labAccounts: [
|
|
317
|
+
{ id: "acct-bioref-nynj", lab: "bioreference", states: ["NY", "NJ"] },
|
|
318
|
+
{
|
|
319
|
+
id: "acct-labcorp-47",
|
|
320
|
+
lab: "labcorp",
|
|
321
|
+
states: US_STATES.filter((state) => !["NY", "NJ", "RI"].includes(state)),
|
|
322
|
+
},
|
|
323
|
+
{ id: "acct-quest", lab: "quest", delegated_flow: "order_delegated" },
|
|
324
|
+
],
|
|
205
325
|
})
|
|
326
|
+
console.log(junction.instance().labAccounts().length) // 3
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Rejections keep Junction's body shape, `400 {"detail": "Lab account is not associated with lab labcorp"}`.
|
|
330
|
+
`PUT /__admin/lab-accounts` swaps them per namespace at runtime. `delegated_flow` is stored and
|
|
331
|
+
listed, but see [not modelled](#what-is-and-is-not-modelled).
|
|
332
|
+
|
|
333
|
+
## Order control
|
|
334
|
+
|
|
335
|
+
`simulate_order` (`POST /v3/order/{id}/test`) behaves like the sandbox's, including its stepwise
|
|
336
|
+
rules. For direct control, the admin API moves an order straight to a status, recording the same
|
|
337
|
+
event, dates and appointment cascade, and publishing the same webhook, a real transition does:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "collected"}'
|
|
341
|
+
curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "completed", "result": "critical"}'
|
|
342
|
+
curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "cancelled.testkit.do_not_process"}'
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`to` is a full status (`completed.testkit.completed`), a `<phase>.<event>` pair, or a name that maps
|
|
346
|
+
onto the contract's own status for the order's collection method:
|
|
347
|
+
|
|
348
|
+
| Name | `at_home_phlebotomy` | `on_site_collection` | `testkit` | `walk_in_test` |
|
|
349
|
+
| --- | --- | --- | --- | --- |
|
|
350
|
+
| `requisition_created` | `received.…requisition_created` | same | same | same |
|
|
351
|
+
| `scheduled` | `collecting_sample.…appointment_scheduled` | — | — | `collecting_sample.…appointment_scheduled` |
|
|
352
|
+
| `shipped` / `delivered` | — | — | `collecting_sample.…transit_customer` / `…with_customer` | — |
|
|
353
|
+
| `collected` | `collecting_sample.…draw_completed` | `sample_with_lab.…draw_completed` | `collecting_sample.…transit_lab` | — |
|
|
354
|
+
| `at_lab` | — | `sample_with_lab.…draw_completed` | `sample_with_lab.…delivered_to_lab` | — |
|
|
355
|
+
| `partial_results` | `sample_with_lab.…partial_results` | same | — | same |
|
|
356
|
+
| `completed` / `corrected` / `cancelled` / `failed` | `completed.…completed` / `completed.…corrected` / `cancelled.…cancelled` / `failed.…sample_error` | same | same | same |
|
|
357
|
+
|
|
358
|
+
A name with no real status for the method (`collected` for a walk-in test) is refused with the
|
|
359
|
+
valid choices, never approximated. `result` picks a named result — `normal`, `abnormal`,
|
|
360
|
+
`critical`, `missing_results` — which steers the generator so the payload keeps Junction's exact
|
|
361
|
+
shape. For an exact payload, `PUT /__admin/results/{id}` with `{ "results": [...],
|
|
362
|
+
"missing_results": null, "interpretation": "critical", "pdf_base64": "…" }`.
|
|
363
|
+
|
|
364
|
+
There is no `isTestCancelled` in Junction's contract; cancelling produces the vendor's
|
|
365
|
+
`cancelled.<method>.cancelled` event and webhook, which is what a consumer derives that flag from.
|
|
366
|
+
|
|
367
|
+
**Time.** Every timestamp reads the runtime clock. `POST /__admin/clock {"advance": "2h"}` makes
|
|
368
|
+
delayed simulations (`?delay=<seconds>`) due and moves appointment windows, with no sleeping.
|
|
369
|
+
|
|
370
|
+
## Faults and error shapes
|
|
371
|
+
|
|
372
|
+
Error bodies the mock reproduces byte for byte, checked against the sandbox by `verify` and the
|
|
373
|
+
parity suite:
|
|
374
|
+
|
|
375
|
+
| Case | Status | Body |
|
|
376
|
+
| --- | --- | --- |
|
|
377
|
+
| `GET /v2/user/{unknown id}` | 404 | `{"detail":"Not found"}` |
|
|
378
|
+
| `GET /v2/user/resolve/{unknown client_user_id}` | 404 | `{"detail":"User not found"}` |
|
|
379
|
+
| Missing `x-vital-api-key` | 401 | `{"detail":"Missing x-vital-api-key"}` |
|
|
380
|
+
| Request validation | 422 | `{"detail":[{"type":…,"loc":[…],"msg":…,"input":…}]}` (Pydantic shape) |
|
|
381
|
+
| Business rule | 400 | `{"detail":"<message>"}` or `{"detail":{"error_type":"INVALID_REQUEST","error_message":"…"}}` |
|
|
382
|
+
|
|
383
|
+
Faults reproduce the sandbox's failure modes on demand, for retry paths and error handling:
|
|
384
|
+
|
|
385
|
+
| Preset | Fault | Fidelity |
|
|
386
|
+
| --- | --- | --- |
|
|
387
|
+
| `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 |
|
|
388
|
+
| `rate_limited` | 429 `{"detail":"Too Many Requests"}`, `retry-after: 1` | shape-plausible, not verified |
|
|
389
|
+
| `server_error` / `bad_gateway` / `unavailable` | 500 / 502 / 503 | shape-plausible, not verified |
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
import { createRuntime, FAULT_PRESETS } from "@crvouga/mockingbird-service-junction"
|
|
393
|
+
|
|
394
|
+
const junction = createRuntime()
|
|
395
|
+
junction.faults.add({ ...FAULT_PRESETS.sandbox_user_quota, id: "quota", count: 1 }) // next create_user only
|
|
396
|
+
junction.faults.add({ id: "flaky-orders", operationId: "create_order_v3_order_post", status: 503, rate: 0.25 })
|
|
206
397
|
```
|
|
207
398
|
|
|
208
|
-
|
|
399
|
+
A rule matches on `operationId`, `method`, `pathPrefix` and `namespace` (all optional), fires
|
|
400
|
+
`count` times or forever, for a `rate` of matching requests (seeded, so a run replays identically),
|
|
401
|
+
after an optional `delayMs`. A rule added through the admin API defaults to the calling namespace,
|
|
402
|
+
so one worker's injected failure never lands on another's request; one added in-process, as above,
|
|
403
|
+
applies to every namespace unless given a `namespace`. Over HTTP: `POST /__admin/faults` with the same fields, or
|
|
404
|
+
`POST /__admin/faults/presets/sandbox_user_quota`.
|
|
209
405
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
406
|
+
## Webhooks
|
|
407
|
+
|
|
408
|
+
With `webhooks` / `--webhook-url`, every event is delivered signed, with retries:
|
|
409
|
+
|
|
410
|
+
- **Standard Svix signatures**: `svix-id`, `svix-timestamp`, and `svix-signature: v1,<base64
|
|
411
|
+
HMAC-SHA256 of "<svix-id>.<svix-timestamp>.<body>">`, keyed by the base64 part of `whsec_…`. The
|
|
412
|
+
official `svix` verifier, and a receiver written for real Junction, accept it unchanged. (Signing
|
|
413
|
+
`"<timestamp>.<body>"` without the message id, as this package's own server script did before,
|
|
414
|
+
is not Svix-compatible.)
|
|
415
|
+
- Timestamps are wall-clock even when the mock clock is moved, because receivers reject timestamps
|
|
416
|
+
outside a few minutes of their own clock.
|
|
417
|
+
- Any 2xx is delivered; anything else is retried on Svix's schedule (immediately, 5 s, 5 min,
|
|
418
|
+
30 min, 2 h, 5 h, 10 h, 10 h) or `retryDelaysMs`. `POST /__admin/webhooks/flush` runs pending
|
|
419
|
+
retries now; `POST /__admin/webhooks/{id}/replay` redelivers one message.
|
|
215
420
|
|
|
216
421
|
```ts
|
|
217
|
-
import {
|
|
218
|
-
import { JunctionAPI, parseSealedCorpus } from "@crvouga/mockingbird-service-junction"
|
|
422
|
+
import { createRuntime, verifySvix } from "@crvouga/mockingbird-service-junction"
|
|
219
423
|
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
junction
|
|
424
|
+
const secret = "whsec_bW9ja2luZ2JpcmQtdGVzdC1zZWNyZXQ="
|
|
425
|
+
const junction = createRuntime({
|
|
426
|
+
webhooks: { url: "http://127.0.0.1:3100/webhooks/junction", secret, retryDelaysMs: [0, 500, 2000] },
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
// In a test receiver:
|
|
430
|
+
const accept = async (request: Request) =>
|
|
431
|
+
verifySvix(
|
|
432
|
+
secret,
|
|
433
|
+
{
|
|
434
|
+
"svix-id": request.headers.get("svix-id") ?? "",
|
|
435
|
+
"svix-timestamp": request.headers.get("svix-timestamp") ?? "",
|
|
436
|
+
"svix-signature": request.headers.get("svix-signature") ?? "",
|
|
437
|
+
},
|
|
438
|
+
await request.text(),
|
|
439
|
+
)
|
|
440
|
+
console.log(typeof accept, junction.webhooks?.deliveries().length) // "function" 0
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Verify
|
|
444
|
+
|
|
445
|
+
`verify` is the fidelity guarantee: it runs the same requests against real Junction and the mock
|
|
446
|
+
and reports every divergence.
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
npx mockingbird-junction verify --real-key "$JUNCTION_SANDBOX_KEY" --corpus test/junction-corpus.json
|
|
450
|
+
npx mockingbird-junction verify --real-key "$JUNCTION_SANDBOX_KEY" --orders --sample 50 --json
|
|
223
451
|
```
|
|
224
452
|
|
|
225
|
-
|
|
226
|
-
|
|
453
|
+
- **Drift**: re-fetches each recorded observation (or `--sample n` of them) and reports any that no
|
|
454
|
+
longer match production — the signal to re-pull.
|
|
455
|
+
- **Scenario**: creates a user, reads it back, provokes the documented 404s, reads the catalog and a
|
|
456
|
+
covered ZIP's serviceability, and deletes the user; with `--orders`, also places, reads and
|
|
457
|
+
cancels an order for the corpus's first lab test. Status codes must match; bodies must match in
|
|
458
|
+
shape, and byte for byte for the documented error bodies.
|
|
459
|
+
|
|
460
|
+
It exits 1 on any divergence. The scenario always deletes its user (and cancels its order), because
|
|
461
|
+
it runs against the same capped sandbox the mock exists to relieve. `verifyAgainstReal` is the same
|
|
462
|
+
check as a function. This repo runs it in CI against the Junction sandbox.
|
|
463
|
+
|
|
464
|
+
## Determinism
|
|
465
|
+
|
|
466
|
+
With a fixed clock (`clock` / `POST /__admin/clock {"set": …, "freeze": true}`) and `seed`, a run
|
|
467
|
+
replays exactly. Stable across runs: user ids, order ids, event ids, sample ids, booking keys and
|
|
468
|
+
webhook message ids (derived from sequence numbers), timestamps (from the clock), generated
|
|
469
|
+
availability, and which requests a partial-`rate` fault hits. Wall-clock by design: webhook
|
|
470
|
+
`svix-timestamp`, `/health` uptime, and request log durations.
|
|
471
|
+
|
|
472
|
+
## What is and is not modelled
|
|
473
|
+
|
|
474
|
+
A mock that is silent about its gaps is how a green suite starts lying. Specifically:
|
|
475
|
+
|
|
476
|
+
- **Corpus-backed** (byte-exact): area and PSC serviceability for covered ZIPs; the catalog, labs,
|
|
477
|
+
expected results and lab accounts of the team the corpus was recorded from.
|
|
478
|
+
- **Synthetic** (deterministic, shape-faithful, values invented): availability slots and booking
|
|
479
|
+
keys; results (biomarker lines and PDFs) unless a fixture is installed; requisition PDFs;
|
|
480
|
+
coverage for any ZIP in `--geo synthetic` mode.
|
|
481
|
+
- **Not modelled**:
|
|
482
|
+
- `delegated_flow`-specific ordering. Accounts carry the field and orders follow the documented
|
|
483
|
+
selection and billing rules above, but any additional behaviour Junction applies to delegated
|
|
484
|
+
accounts is not reproduced. Run `verify --orders` with your team's corpus to check what you rely on.
|
|
485
|
+
- Rate limits and outages, except when injected as faults.
|
|
486
|
+
- Real 429 / 5xx bodies: the presets are plausible, not recorded.
|
|
487
|
+
- Phlebotomy availability breadth: the sandbox serves it only for `85004`; the mock generates slots
|
|
488
|
+
for any covered ZIP.
|
|
489
|
+
- Webhook retry jitter across a snapshot restore (the jitter stream is not part of a snapshot).
|
|
490
|
+
- Operations outside the vendored OpenAPI subset: they return 404 and are counted as
|
|
491
|
+
`unmatched` in `/__admin/metrics`.
|
|
227
492
|
|
|
228
493
|
## API
|
|
229
494
|
|
|
495
|
+
Main entry (`@crvouga/mockingbird-service-junction`, runtime-neutral):
|
|
496
|
+
|
|
230
497
|
| Export | Description |
|
|
231
498
|
| --- | --- |
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
499
|
+
| `createRuntime` | `(options?: JunctionRuntimeOptions) => JunctionRuntime` — the served mock (health, admin, namespaces, clock, faults, metrics, webhooks) as one `fetch`. Options: `corpus`, `geo`, `labAccounts`, `webhooks`, `onWebhook`, `sqlite`, `clock`, `seed`, `adminKey`, `onLog`. |
|
|
500
|
+
| `JunctionAPI` | Class. `new JunctionAPI(options?)`: one namespace's mock, implementing `fetch(request: Request): Promise<Response>`. |
|
|
501
|
+
| `JUNCTION_NAMESPACE` | `"junction"` — the default namespace's storage key when sharing a `sqlite` client. |
|
|
234
502
|
| `document` | The vendored Junction OpenAPI document (Mockingbird subset) that drives routing. |
|
|
235
503
|
| `operationIds` | Every `operationId` in `document`. |
|
|
236
504
|
| `supportedOperationIds` | The `operationId`s the mock implements (all of them). |
|
|
505
|
+
| `pullCorpus` | `(options: PullCorpusOptions) => Promise<SealedCorpus>` — record a corpus from a real team (GETs only). |
|
|
506
|
+
| `diffCorpus` | `(before, after) => CorpusDiff` — ZIPs, lab tests, lab accounts and observations added / removed / changed. |
|
|
507
|
+
| `fingerprintCorpus` | `(corpus) => Promise<string>` — SHA-256 of a corpus's content. |
|
|
508
|
+
| `corpusLabel` | `(corpus) => string` — the short label `/health` and logs show, e.g. `v1-2026-09-18-HqFDuEAuhkwD`. |
|
|
237
509
|
| `parseSealedCorpus` | `(value: unknown) => SealedCorpus`; throws on a non-object or unsupported `version`. |
|
|
238
|
-
| `SEALED_CORPUS_VERSION` | Current
|
|
239
|
-
| `
|
|
240
|
-
| `
|
|
510
|
+
| `SEALED_CORPUS_VERSION` | Current corpus format version (`1`). |
|
|
511
|
+
| `verifyAgainstReal` | `(options: VerifyOptions) => Promise<VerifyReport>` — the `verify` command as a function. |
|
|
512
|
+
| `DEFAULT_JUNCTION_BASE_URL` | `"https://api.sandbox.tryvital.io"`. |
|
|
513
|
+
| `isSandboxKey` / `SANDBOX_KEY_PREFIXES` | Whether a key is a sandbox team key (`sk_us_` / `sk_eu_`). |
|
|
514
|
+
| `UNKNOWN_ZIP_STATUS` / `UNKNOWN_ZIP_ERROR_TYPE` | `424` / `"MOCKINGBIRD_UNKNOWN_ZIP"` — the corpus-mode unknown-ZIP error. |
|
|
515
|
+
| `labAccountFromInput` | `(input: LabAccountInput) => LabAccountRecord` — normalize (and validate) a configured account. |
|
|
516
|
+
| `TEAM_LAB_ACCOUNTS` | The built-in lab-account fixtures. |
|
|
517
|
+
| `US_STATES` | The 50 `USState` codes Junction accepts. |
|
|
518
|
+
| `ORDER_STATUSES_BY_METHOD` | Every order status in the contract, by collection method. |
|
|
519
|
+
| `TRANSITION_ALIASES` | The transition names above, by method. |
|
|
520
|
+
| `resolveTransition` | `(method, target) => { status } \| { error }` — how `/__admin/orders/{id}/transition` reads `to`. |
|
|
521
|
+
| `RESULT_FIXTURES` | Named results (`normal`, `abnormal`, `critical`, `missing_results`). |
|
|
522
|
+
| `FAULT_PRESETS` | Named Junction faults (`sandbox_user_quota`, `rate_limited`, `server_error`, `bad_gateway`, `unavailable`). |
|
|
523
|
+
| `createWebhookDispatcher` | `(endpoint: WebhookEndpoint) => WebhookDispatcher` — signed delivery with retries, standalone. |
|
|
524
|
+
| `signSvix` / `verifySvix` | Produce / check a standard Svix `svix-signature`. |
|
|
525
|
+
| `observationCacheKey` | `(url, method = "GET", body?) => string` — the `"<METHOD> <path>?<sorted query> <json>"` key corpora use. |
|
|
526
|
+
| `prefetchCoverageObservations` | Parity helper: records coverage-ZIP reads from the sandbox into an observation cache before `seedFrom`. |
|
|
241
527
|
| `reshapeCoverageGeoCommand` | Parity-walk hook that pins geo/availability parameters onto the coverage corpus. |
|
|
242
|
-
| `COVERAGE_ZIPS` |
|
|
528
|
+
| `COVERAGE_ZIPS` | The ZIPs the shipped corpus covers. |
|
|
243
529
|
| `PSC_AVAILABILITY_ZIPS` | ZIPs that also get PSC availability observations. |
|
|
244
|
-
| `PHLEBOTOMY_AVAILABILITY_ZIPS` | ZIPs
|
|
530
|
+
| `PHLEBOTOMY_AVAILABILITY_ZIPS` | ZIPs the sandbox serves phlebotomy availability for (`["85004"]`). |
|
|
245
531
|
| `PSC_LAB_IDS` | Lab ids used for PSC reads (`[4, 6, 13, 25]`). |
|
|
246
532
|
| `AVAILABILITY_ADDRESS` | Stable Phoenix, AZ address used in availability requests so cache keys match. |
|
|
247
533
|
| `AVAILABILITY_START_DATE` | Far-future availability `start_date` (`"2099-06-15"`). |
|
|
248
534
|
|
|
535
|
+
`./server` (Node): `createServer(options?) => Promise<JunctionServer>`, `serveTarget` (what
|
|
536
|
+
`serve` and `serve --config` use), `loadCorpus(source)`, `DEFAULT_PORT` (`8787`).
|
|
537
|
+
`./corpus`: `defaultCorpus`.
|
|
538
|
+
|
|
539
|
+
`JunctionRuntime` (from `createRuntime`) members: `fetch`, `instance(namespace?)`, `namespaces()`,
|
|
540
|
+
`reset(namespace? | "*")`, `snapshot(namespace?)`, `restore(snapshot, namespace?)`, `clock`
|
|
541
|
+
(`now`, `set`, `advance`, `freeze`, `unfreeze`, `reset`, `state`), `faults` (`add`, `list`,
|
|
542
|
+
`remove`, `clear`), `metrics` (`report`, `reset`), `webhooks` (`deliveries`, `replay`, `flush`,
|
|
543
|
+
`idle`, `clear`), `sqlite`, `rng`.
|
|
544
|
+
|
|
249
545
|
`JunctionAPI` members:
|
|
250
546
|
|
|
251
547
|
| Member | Description |
|
|
252
548
|
| --- | --- |
|
|
253
549
|
| `fetch(request)` | Handle one Junction REST request. |
|
|
254
|
-
| `reset()` | `Promise<void>` — clear state; re-install the default catalog
|
|
255
|
-
| `installCorpus(corpus)` | Install a `SealedCorpus
|
|
550
|
+
| `reset()` | `Promise<void>` — clear state; re-install the corpus or default catalog, and the configured lab accounts. |
|
|
551
|
+
| `installCorpus(corpus)` | Install a `SealedCorpus`: observations (shared, read-only), catalog, labs, lab accounts, ZIP coverage. |
|
|
552
|
+
| `corpusInfo()` | `CorpusInfo \| undefined` — label, recording date, source and counts. |
|
|
553
|
+
| `geoMode` | `"corpus" \| "synthetic"`, settable. |
|
|
554
|
+
| `configureLabAccounts(accounts?)` / `labAccounts()` | Replace (or restore with `undefined`) / read the team's lab accounts. |
|
|
555
|
+
| `order(id)` / `orders()` | Read orders. |
|
|
556
|
+
| `transitionOrder(id, status, { now, flags? })` | Move an order to a full status and publish its webhook. |
|
|
557
|
+
| `installResultFixture(orderId, fixture)` / `resultFixture(orderId)` | Serve an exact result payload / PDF for an order. |
|
|
256
558
|
| `seedFrom(source, observations?)` | `Promise<SeedReport>` — copy users, orders, appointments, catalog and labs from a real Junction environment. |
|
|
257
559
|
| `ensureLabTests(source, ids)` / `ensureOrders(source, ids)` | `Promise<number>` — fetch specific lab tests / orders from `source` if missing. |
|
|
258
560
|
| `markUserDeleted(userId)` | Mark a user as deleted (subsequent reads behave like a deleted user). |
|
|
259
561
|
| `webhookEvents()` | `JunctionWebhookEvent[]`, oldest first. |
|
|
260
|
-
| `webhookDeliveryAttempts()` |
|
|
562
|
+
| `webhookDeliveryAttempts()` | The retry schedule Junction would follow per event (a record; see `webhooks` for delivery). |
|
|
261
563
|
| `app` / `sqlite` | The underlying Hono app and `SqliteClient`. |
|
|
262
564
|
|
|
263
565
|
Options and types:
|
|
@@ -265,21 +567,27 @@ Options and types:
|
|
|
265
567
|
```text
|
|
266
568
|
type JunctionAPIOptions = {
|
|
267
569
|
sqlite?: SqliteClient // share one client across services; default: fresh in-memory DB
|
|
570
|
+
namespace?: string // storage namespace; default "junction"
|
|
268
571
|
now?: () => number // clock in ms; default Date.now
|
|
572
|
+
corpus?: SealedCorpus // installed now and on every reset()
|
|
573
|
+
geo?: "corpus" | "synthetic" // default "corpus" with a corpus, else "synthetic"
|
|
574
|
+
labAccounts?: LabAccountInput[]
|
|
269
575
|
onWebhook?: WebhookPublisher // called with every recorded event
|
|
270
576
|
webhook?: JunctionWebhookOptions
|
|
271
577
|
}
|
|
272
|
-
type
|
|
578
|
+
type LabAccountInput = { id; lab; status?; delegated_flow?; account_name?; provider_account_id?; org_id?;
|
|
579
|
+
business_units?; default_clinical_notes?; allowed_billing?: Record<billingType, State[]>; states?: State[];
|
|
580
|
+
team_id_allowlist? }
|
|
581
|
+
type WebhookEndpoint = { url; secret: "whsec_…"; retryDelaysMs?: number[]; timeoutMs?; headers?; fetch? }
|
|
582
|
+
type ResultFixture = { name; results?: unknown[]; missing_results?: unknown[] | null; interpretation?; pdf_base64? }
|
|
583
|
+
type FaultRule = { id; operationId?; method?; pathPrefix?; status; body?; headers?; count?; rate?; delayMs? }
|
|
584
|
+
type JunctionWebhookOptions = { seed?: number; jitterRatio?: number /* 0..1, default 0.1 */; timeoutMs?: number }
|
|
273
585
|
type JunctionWebhookEvent = {
|
|
274
586
|
event_type: "labtest.order.created" | "labtest.order.updated" | "labtest.appointment.updated"
|
|
275
587
|
data: Record<string, unknown>; team_id: string; user_id: string; client_user_id: string
|
|
276
588
|
}
|
|
277
|
-
type
|
|
589
|
+
type SealedCorpus = { version: 1; recordedAt; source; fingerprint?; observations; catalog: { labTests; labs; expectedResults }; labAccounts }
|
|
278
590
|
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
591
|
type OperationId / SupportedOperationId // string unions of operationIds / supportedOperationIds
|
|
284
592
|
```
|
|
285
593
|
|
|
@@ -299,23 +607,19 @@ console.log(report.users, report.orders)
|
|
|
299
607
|
|
|
300
608
|
## Development
|
|
301
609
|
|
|
302
|
-
For contributors to the mockingbird repo only
|
|
303
|
-
npm package.
|
|
610
|
+
For contributors to the mockingbird repo only.
|
|
304
611
|
|
|
305
612
|
```bash
|
|
306
613
|
bun test # offline suites, incl. scheduling state space
|
|
307
|
-
bun test junction.
|
|
614
|
+
bun test junction.runtime.property.test.ts # service contract, corpus, lab accounts, faults, webhooks, verify
|
|
308
615
|
bun test junction.sdk.property.test.ts # drives the served mock through @tryvital/vital-node
|
|
309
616
|
|
|
310
|
-
bun run mock:serve # HOST
|
|
311
|
-
|
|
312
|
-
bun run corpus:record
|
|
617
|
+
bun run mock:serve # env-configured `serve`: HOST, PORT, MOCKINGBIRD_JUNCTION_CORPUS,
|
|
618
|
+
# MOCKINGBIRD_JUNCTION_WEBHOOK_URL / _SECRET / _SCOPE
|
|
619
|
+
bun run corpus:record -- --force # re-record the shipped corpus/sandbox-sealed.json (Vault key)
|
|
620
|
+
bun run verify -- --real-key "$KEY" # `mockingbird-junction verify` from source
|
|
313
621
|
```
|
|
314
622
|
|
|
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
623
|
Live parity (primary proof is seedParity: warmup N on the sandbox, `seedFrom`, then lockstep M):
|
|
320
624
|
|
|
321
625
|
```bash
|
|
@@ -326,6 +630,5 @@ bun run parity -- --mode=empty # legacy empty-start differenti
|
|
|
326
630
|
|
|
327
631
|
Webhook parity needs a public receiver; see
|
|
328
632
|
[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
633
|
|
|
331
634
|
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).
|