Local emulators. Real API contracts. Part of Mockingbird.
Stateful emulator of the Junction (formerly Vital) API: users
(/v2/user), the lab-testing catalog, lab orders (create, cancel, simulate, results,
requisitions), at-home phlebotomy and patient-service-center (PSC) scheduling, and the
labtest.order.* / labtest.appointment.updated webhooks. All 40 operations in the vendored
OpenAPI subset are served, and behaviour is checked by differential property tests against the
Junction sandbox.
Use it to take Junction off your suite's critical path: no sandbox key, no shared 50-user sandbox cap (the emulator enforces no sandbox limit unless a test asks for one), no cross-suite interference. Serve it as a local origin with one command, or run it in-process.
npx mockingbird-junction serve # http://127.0.0.1:8787, recorded sandbox corpus loaded- Operation coverage: SUPPORT.md
- Drop-in readiness and the sandbox quirks the emulator mirrors: docs/drop-in.md
- Behaviour notes: docs/behavior.md
- What the emulator does not model: below. Read it before trusting a green suite.
Install
npm install -D @crvouga/mockingbird-service-junctionESM only; Node >= 22 or Bun >= 1.2 (CommonJS callers on Node >= 22.12 can require() it). No
native dependencies: state lives in an in-memory SQLite engine written in TypeScript, bundled in.
| Entry point | Runtime | What it is |
|---|---|---|
@crvouga/mockingbird-service-junction |
any (Node, Bun, Workers, browsers) | JunctionAPI, createRuntime, corpus and verify tools |
@crvouga/mockingbird-service-junction/server |
Node | createServer() — a listening HTTP server |
@crvouga/mockingbird-service-junction/corpus |
any | defaultCorpus — the recording shipped in the package (a separate entry, so importing the emulator never parses it) |
mockingbird-junction (bin) |
Node | serve, corpus pull, corpus diff, verify |
Usage
mockingbird-junction serve
npx mockingbird-junction serve --port 8787
npx mockingbird-junction serve --corpus ./test/junction-corpus.json --lab-accounts ./test/lab-accounts.json
npx mockingbird-junction serve --webhook-url http://127.0.0.1:3100/webhooks/junction --webhook-secret whsec_...
npx mockingbird-junction serve --help| Flag | Default | Meaning |
|---|---|---|
--port <n> |
8787 |
Port to listen on (0 for any free port; the bound URL is printed) |
--host <host> |
127.0.0.1 |
Interface to bind |
--corpus <default|none|file> |
default |
The shipped corpus, no corpus (synthetic data), or a file from corpus pull |
--geo <corpus|synthetic|corpus+synthetic> |
corpus with a corpus |
How unknown ZIPs are answered; see Geo |
--zip-overlays <file> |
— | ZIP aliases and per-ZIP overrides |
--catalog-overlays <file> |
— | Lab tests derived from catalog ones at boot |
--marker-catalog <file> |
— | Markers GET /v3/lab_tests/markers offers, with pricing |
--add-on-source-markers |
off | Name the markers behind each add-on result line |
--lab-accounts <file> |
built-in fixtures | Lab accounts: a JSON array, or { "presets": [...], "accounts": [...] } |
--team-id <uuid> |
the corpus's team | The team the emulator answers as |
--max-users <n> |
unlimited | Enforce the sandbox's live-user cap |
--identity <strict|adopt-users> |
strict |
Unknown user_ids: 404, or create on first use |
--default-billing-type <lab=type,…> |
client_bill |
The billing_type an order for that lab gets when it omits one (billing) |
--fixtures <file> |
— | Users and orders every namespace starts with |
--journal-size <n> |
1000 |
Requests each namespace's journal keeps |
--webhook-url <url> / --webhook-secret <whsec_…> |
off | Deliver signed webhooks (secret also from MOCKINGBIRD_JUNCTION_WEBHOOK_SECRET) |
--webhook-retry-delays <ms,…> |
Svix's schedule | Delay before each delivery attempt, e.g. 0,1000,5000 in tests |
--webhook-scope <scope> |
— | Sent as x-mockingbird-scope on every delivery |
--admin-key <key> |
open | Require x-mockingbird-admin-key on /__admin/* (also MOCKINGBIRD_ADMIN_KEY) |
--seed <seed> |
0 |
Seeds fault rates and retry jitter |
--log <pretty|json|off> |
pretty |
One line per request: operation id, status, duration, namespace, fault |
--log-requests |
— | Same as --log json: one JSON line per request with the ids it touched, never bodies |
--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 |
--config <file> |
— | Serve every service in a mockingbird.json instead (below) |
At startup it prints the listen address, the loaded corpus (its version label, observation, ZIP, lab-test and lab-account counts, recording date and source), the geo mode, the team id, the lab-account count, which sandbox limits are on (normally none), the identity mode, and whether webhook delivery is on.
Many services from one config
Any Mockingbird service's CLI can boot every service in a config file, so a stack adds a config
entry per vendor instead of a wrapper process per vendor. Each service named must be installed
(junction loads @crvouga/mockingbird-service-junction).
{
"log": "json",
"services": {
"junction": {
"port": 8787,
"adminKey": "local-admin",
"options": { "corpus": "./test/junction-corpus.json", "webhook-url": "http://127.0.0.1:3100/webhooks/junction" }
},
"stripe": { "port": 12111 }
}
}npx mockingbird-junction serve --config mockingbird.jsonoptions takes the service's own serve flags by long name.
createServer (Node)
import { VitalClient } from "@tryvital/vital-node"
import { createServer } from "@crvouga/mockingbird-service-junction/server"
const server = await createServer() // shipped corpus, any free port
// The Vital SDK your application already uses. Only `environment` changes.
const junction = new VitalClient({ apiKey: "sk_us_mockingbird", environment: server.url })
const user = await junction.user.create({ clientUserId: "app-user-1" })
console.log(user.userId) // "<uuid>"
await server.close()createServer(options) takes every createRuntime option plus
port (default 0), host (default 127.0.0.1) and corpus ("default", "none", a file path
or a loaded corpus). The result has url, port, runtime and close().
createRuntime (any Fetch server)
createRuntime is the whole served emulator — health, admin, namespaces, clock, faults, metrics — as a
single runtime-neutral fetch(request). Hand it to Bun.serve, a Worker, or any other Fetch server:
import { VitalClient } from "@tryvital/vital-node"
import { createRuntime } from "@crvouga/mockingbird-service-junction"
import { defaultCorpus } from "@crvouga/mockingbird-service-junction/corpus"
const junction = createRuntime({ corpus: defaultCorpus })
const server = Bun.serve({ port: 0, fetch: (request) => junction.fetch(request) })
// One SDK client per worker. The URL prefix isolates that worker's data.
const as = (worker: string) =>
new VitalClient({
apiKey: "sk_us_mockingbird",
environment: `${server.url.origin}/__admin/ns/${worker}`,
})
await as("worker-1").user.create({ clientUserId: "patient-1" })
const other = await as("worker-2").user.getAll()
console.log(other.users) // [] (worker-2 sees none of worker-1's users)
await junction.reset("worker-1") // or junction.reset("*") for every namespace
await server.stop()Pointing @tryvital/vital-node at it
The official SDK takes the base URL as environment (this is how the package's SDK drop-in test
constructs it). It has no option for a custom fetch, so it needs a served URL; a client that
accepts a fetch can call createRuntime().fetch in-process instead.
import { VitalClient } from "@tryvital/vital-node"
import { createServer } from "@crvouga/mockingbird-service-junction/server"
// Test setup: the emulator, with its clock set.
const server = await createServer()
await fetch(`${server.url}/__admin/clock`, {
method: "POST",
body: JSON.stringify({ set: "2030-01-01T00:00:00Z" }),
})
// The application's calls, through the SDK.
const junction = new VitalClient({ apiKey: "sk_us_mockingbird", environment: server.url })
const user = await junction.user.create({ clientUserId: "app-user-1" })
const labTests = await junction.labTests.get()
const testkit = labTests.find((test) => test.method === "testkit")
if (testkit === undefined) throw new Error("default catalog has a testkit test")
const { order } = await junction.labTests.createOrder({
userId: user.userId,
patientDetails: {
firstName: "Ada",
lastName: "Lovelace",
dob: "1990-01-01",
gender: "female",
phoneNumber: "+14155551234",
email: "ada@example.com",
},
patientAddress: {
firstLine: "1 N Central Ave",
city: "Phoenix",
state: "AZ",
zip: "85004",
country: "US",
},
orderSet: { labTestIds: [testkit.id] },
})
console.log(order.id, order.status) // "<uuid>" "received"
// Test assertion: every event the emulator published, oldest first.
const events = server.runtime.instance().webhookEvents()
console.log(events.map((event) => event.event_type)) // ["labtest.order.created"]
await server.close()The clock (POST /__admin/clock, or now in milliseconds on JunctionAPI) drives
created_on/updated_at fields, webhook timestamps, and when delayed simulations
(POST /v3/order/{id}/test?final_status=completed&delay=<seconds>) become due; the delayed
transition is applied on the next GET /v3/order/{id} whose clock is past the due time.
If your app guards Junction hosts with an allowlist, allow 127.0.0.1 / localhost for tests
while keeping the production and sandbox guards intact.
The service contract
Every Mockingbird service answers the same control surface, outside the vendor's auth gate:
GET /__admin/health— unauthenticated readiness probe:{ "status": "ok", "service": "junction", "corpus": "<label>", "geo": "corpus", … }./__admin/*— the control plane. Open by default; with--admin-key/adminKeyit requiresx-mockingbird-admin-key, which is separate from any vendor key. Admin errors have one shape,{ "error": { "type": "mockingbird_admin", "message": "…" } }, so they can never be confused with a Junction error.GET /__adminlists every route.x-mockingbird-namespace: <name>— isolates a request's data ([A-Za-z0-9_.-]{1,64}). Each namespace is a separate team over one shared database, so parallel workers share one process without seeing each other. The corpus is shared read-only; creating a namespace is cheap. Admin routes take the namespace from?namespace=, then the header, thendefault. Configuration set through the admin API (lab accounts, limits, identity, geo) belongs to one namespace and survives that namespace'sPOST /__admin/reset; data does not.x-mockingbird: junction@<version>; ns=<namespace>— on every response: vendor answers, errors, faults, admin and health. Junction never sends it, so it tells the emulator from the vendor (see Is this the emulator?).
| Route | Does |
|---|---|
POST /__admin/reset |
Reset the request's namespace; ?all=1 resets every namespace |
GET /__admin/namespaces |
Namespaces created so far |
POST /__admin/snapshots |
Snapshot the namespace; returns { id } |
POST /__admin/snapshots/{id}/restore |
Restore it (an assignment: later writes are discarded) |
DELETE /__admin/snapshots/{id} |
Forget a snapshot |
GET / POST /__admin/clock |
Read or move the clock: { "set": "2030-01-01T00:00:00Z" }, { "advance": "2h" }, { "freeze": true }, { "reset": true } |
GET / POST / DELETE /__admin/faults |
List, add or clear fault rules (DELETE ?id= removes one) |
POST /__admin/faults/presets/{name} |
Add a named Junction fault; body may override count, rate, operationId… |
GET /__admin/metrics |
Requests by operation and status, fault count, and unmatched routes |
GET /__admin/orders |
Orders in the namespace with their current status, the lab_account_id each was placed with (null: Junction's platform account) and lab_account_request (below) |
POST /__admin/orders/{id}/transition |
Move an order: { "to": "completed", "result": "abnormal" } (order control) |
GET / PUT /__admin/results/{id} |
Read or install an exact result payload / PDF for an order |
GET /__admin/result-fixtures |
The named results |
GET / PUT /__admin/quirks |
Read or replace the namespace's opt-in sandbox quirks ({ "quirks": { "sameSecondCancelStatus": true } }) |
GET / PUT /__admin/lab-accounts |
Read or replace the namespace's lab accounts ({ "accounts": [...], "presets": [...] }; { "accounts": null } restores the default) |
POST /__admin/lab-accounts |
Add one account; 409 if the id exists |
PATCH / DELETE /__admin/lab-accounts/{id} |
Merge fields into an account ({ "status": "suspended" }, { "states": [...] }) / remove it; 404 if absent |
GET /__admin/lab-accounts/presets |
Every preset, with its full record, plus aliases (deprecated names) and synthetic (presets that are not a recorded account) |
POST /__admin/lab-accounts/presets/{name} |
Add a preset account; body { "id": "…" } overrides its id |
GET /__admin/team |
{ "teamId" } — the team the namespace answers as |
GET / PUT /__admin/limits |
Read or change the namespace's sandbox limits |
GET / PUT /__admin/default-billing-types |
Read or replace { "defaultBillingTypes": { "<lab slug>": "<billing_type>" } } (billing); {} restores client_bill |
GET / PUT /__admin/identity |
Read or set { "mode": "strict" | "adopt-users" } (identity) |
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 |
DELETE /__admin/users/{id} |
Remove a user outright, leaving no deletion tombstone |
POST /__admin/orders |
Insert an order, optionally already in a status, through create_order's record builder |
POST /__admin/import |
{ "users": [...], "orders": [...] } in one call |
GET / DELETE /__admin/requests |
The namespace's request journal (?operationId=, ?status=, ?since=, ?limit=, ?all=1) / clear it |
GET /__admin/corpus, PUT /__admin/geo |
The loaded corpus; switch geo mode |
GET /__admin/webhooks |
Deliveries with every attempt |
POST /__admin/webhooks/{id}/replay, POST /__admin/webhooks/flush |
Redeliver one message; run pending retries now |
GET /__admin/webhooks/events |
Every event recorded, delivered or not |
GET /__admin/metrics → unmatched counts requests to paths the contract does not have. It is how
a new SDK call shows up — as a count — before it fails a suite as a 404.
Corpus
Provider-owned inventory — area serviceability, PSC site lists, the lab catalog, labs, lab accounts — cannot be synthesized faithfully. A corpus is an exact recording of those parameter-stable reads, and the emulator answers them byte for byte.
The shipped corpus (defaultCorpus, and what serve loads unless told otherwise) was recorded
from the Junction sandbox. It covers area and PSC serviceability for 57 ZIPs across the US, the PSC
labs those ZIPs reach, and the recording team's catalog. The catalog and lab accounts belong to the
recording team: lab-test ids are per team, so your product's ids will not be in it. For your
catalog and your lab accounts, record your own team and commit the file:
npx mockingbird-junction corpus pull --real-key "$JUNCTION_SANDBOX_KEY" --out test/junction-corpus.json
npx mockingbird-junction corpus pull --real-key "$JUNCTION_SANDBOX_KEY" --base test/junction-corpus.json \
--zip 10001,94105 --coverage-only --out test/junction-corpus.json # add ZIPs to it
npx mockingbird-junction corpus diff test/junction-corpus.json /tmp/fresh.json # exit 1 when they differcorpus pull only issues GETs, refuses non-sandbox keys (sk_us_… / sk_eu_…) unless given
--allow-any-key, and stamps the file with a SHA-256 fingerprint of its content (not its
recording time), so re-pulling unchanged data yields the same fingerprint. Availability is never
recorded: its slot dates and single-use booking_keys cannot be replayed, so it always comes from
the deterministic generator.
Geo: corpus or synthetic
With a corpus loaded, serviceability reads (area/info, psc/info, phlebotomy and PSC
availability) answer only for ZIPs the corpus covers. Any other well-formed ZIP gets:
{
"detail": {
"error_type": "MOCKINGBIRD_UNKNOWN_ZIP",
"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.",
"zip_code": "00501",
"corpus": "v1-2026-09-18-HqFDuEAuhkwD"
}
}with status 424, which Junction never sends and HTTP clients do not retry: a fixture gap fails
loudly instead of silently disagreeing with production. A malformed ZIP still gets Junction's own
422. --geo synthetic (the default without a corpus) invents plausible, deterministic coverage
instead — right for property tests that walk random ZIPs, wrong for a suite standing in for
production.
A covered ZIP is answered from its recordings, never next to them. The corpus records area/info
and psc/info at the default radius (25 miles, so radius=25 and no radius are the same read)
and at 100 miles. A psc/info read at another radius is the smallest wider recording filtered to
distance < radius: the sandbox lists the nearest sites (at most 30), and distance is rounded, so
this reproduces every recorded 25-mile answer from its 100-mile one exactly. An area/info read at
a radius with no recording, or any read with parameters the corpus did not record (for example
capabilities), gets the same 424, because within_radius counts cannot be derived. In
--geo synthetic the derivation still applies, and only reads with nothing to derive from are
invented.
Hybrid geo and ZIP overlays
--geo corpus+synthetic (geo: "corpus+synthetic") replays the recording for the ZIPs the corpus
covers and synthesizes every other ZIP, so a suite needs no second, synthetic runtime as a
fallback. A covered ZIP is still never answered next to its recording: a read the recording cannot
answer (see above) refuses with the same 424 as --geo corpus. Switch it per namespace with
PUT /__admin/geo {"mode": "corpus+synthetic"}.
zipOverlays (--zip-overlays <file>, or PUT /__admin/geo {"overlays": {...}}) edits answers
for individual ZIPs, in any geo mode:
{
"aliases": { "07105": "85004" },
"overrides": { "07105": { "phlebotomy_served": true }, "10118": { "no_pscs": true } }
}An alias makes a ZIP answer as another one (coverage, recordings and the synthetic model all use
the target; the response echoes the requested ZIP). phlebotomy_served sets
phlebotomy.is_served (and providers) on area/info; no_pscs empties the psc/info list and
zeroes the within_radius counts on area/info. An override is looked up by the requested ZIP,
then by its alias target. Overlays are configuration: they survive reset().
The sealed sandbox recording agrees with the report behind this feature that NJ 07030 is not
served for at-home phlebotomy, and the synthetic model reports 10118, 11201, 12207, 07030
and 07601 as not served. It does not know that NJ 07105 is served (the synthetic model only
serves a handful of ZIP prefixes), so set that one with an override.
Catalog overlays and a-la-carte markers
Catalog overlays (catalogOverlays, --catalog-overlays <file>) add lab tests derived from
ones the catalog already holds, with no network access, to every namespace at boot and again on
every reset():
import { createRuntime } from "@crvouga/mockingbird-service-junction"
createRuntime({
catalogOverlays: [
{
from: "lipid_panel_athome", // id or slug of a catalog lab test (or an earlier overlay)
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
name: "Lipid Panel: Quest",
slug: "lipid_panel_quest", // default: derived from name
lab: "sonora_quest", // a lab slug the catalog lists, or a lab record { id, slug, name, ... }
stripAoe: true, // markers lose their ask-on-order questions
autoGenerated: true, // lists with generation_method=auto, not by default
},
],
})method, price (dollars) and status can be overridden too. Moving the clone to a lab gives its
markers and expected results that lab's id. A bad overlay (unknown from, non-UUID or duplicate
id, unknown lab) fails at boot naming catalogOverlays[n].<field>.
A-la-carte markers. GET /v3/lab_tests/markers lists the markers a team can order one by one:
those embedded in active catalog lab tests, plus markerCatalog entries (--marker-catalog <file>) with their own price, pricing, aoe, expected_results and a_la_carte_enabled. It
takes lab_id, lab_slug (wins over lab_id), name (a name substring, or a provider test code),
a_la_carte_enabled, lab_account_id (narrows to the account's lab), include_pricing, page and
size, and answers { markers, total, page, size, pages } (plus pricing: { currency, data } keyed
by marker id for the page's markers with include_pricing=true). A marker price is read as dollars
("12.50" is base_amount_minor: 1250); "N/A" is { "unspecified": "price_unavailable" }.
POST /v3/lab_test/estimate_order_set_pricing prices each order set (lab_test_ids and/or
add_on.marker_ids / provider_ids) as one lab_charge component: the sum of the lab tests'
price and the add-on markers' prices, with a marker_breakdown for add-ons. If any add-on marker
has no stated price the whole estimate is price_unavailable. modality, us_state and billing
are validated but do not change the price, and no other charge components are modelled.
Both routes follow the published contract
(markers,
estimate); neither
has been compared with the live sandbox (prices are contract-owned; best guess, not oracle-verified), so their parity is disabled and
the 400 messages for unknown tests/markers are borrowed from create_order.
addOnSourceMarkers: true (--add-on-source-markers) makes each biomarker line of an add-on order
carry source_markers: [{ marker_id, name, slug, provider_id }]. It is off by default, because the
default result lines carry no source_markers, and was reported from a sandbox probe that has not
been re-verified.
GET /v3/lab_tests lists every lab test unless filtered; pass status=active to exclude inactive
ones. The documented default of that filter is not stated, so the emulator does not change it:
default-excludes-inactive (as #470 reports) is an unverified claim and is not implemented
(best guess, not oracle-verified; junction.markers-overlays.test.ts pins the current contract).
Lab accounts
create_order selects explicit linked accounts in either active or ready_to_launch
status. With no id, exactly one orderable linked account is selected. Multiple orderable
accounts use the per-lab labAccountOmissionPolicies control (platform, the default, or
reject). Platform selection uses an orderable linked account with org_id: null, or an
opaque platform account for a supported platform lab. It never chooses a customer account
by listing order. With no linked accounts, supported labs use the platform; with linked
accounts but none orderable, ordering fails. Billing type and patient state are checked
against the selected account. This follows the
Junction guide; the choice between
platform fallback and rejection varies by lab/team, so configure reject where appropriate.
Set { labAccountOmissionPolicies: { bioreference: "platform" } } at runtime creation, or
PUT /__admin/lab-account-omission-policies with { "policies": { "bioreference": "reject" } }.
GET reads the effective overrides. Overrides are namespace isolated and kept across reset.
Explicit ids naming another lab's account remain accepted, matching the sandbox probe in
corpus/lab-account-probes.json and #138.
Delegation alone does not change account selection.
Requisition PDFs are synthetic and contain labelled Account number: evidence copied from
the selected account's provider_account_id when the order is created. Later account edits,
reordering or routing changes cannot change that evidence. Opaque platform accounts show
unknown. Use GET /__admin/requisitions/:id to inspect it, or PUT with
{ "provider_account_id": "MOCK-CUSTOMER" } (or null for explicitly unknown evidence) to
install a namespace-isolated fixture. Reset removes the orders and their requisition fixtures.
This models account evidence, not a vendor's exact PDF layout or proof of a real association.
Junction's order (ClientFacingOrder) has no lab_account_id, so neither does the emulator's: not on
the create response, GET /v3/order/{id}, GET /v3/orders or webhooks. The account an order was
placed with is reported only by GET /__admin/orders and instance().orderLabAccount(orderId).
The accounts come from, in order: labAccounts / --lab-accounts, else the corpus (a pulled
corpus carries your team's real accounts), else built-in fixtures. Configure them as
ClientFacingLabAccounts, with states as shorthand for client-bill states:
import { createRuntime, US_STATES } from "@crvouga/mockingbird-service-junction"
const junction = createRuntime({
labAccounts: [
{ id: "acct-bioref-nynj", lab: "bioreference", states: ["NY", "NJ"] },
{
id: "acct-labcorp-47",
lab: "labcorp",
states: US_STATES.filter((state) => !["NY", "NJ", "RI"].includes(state)),
},
{ id: "acct-quest", lab: "quest", delegated_flow: "order_delegated" },
],
})
console.log(junction.instance().labAccounts().length) // 3Rejections keep Junction's body shape, 400 {"detail": "Lab account is not linked to your team"}.
delegated_flow is stored and listed, but see not modelled.
Availability reads that take a lab_account_id (GET /v3/order/area/info) check it against the
same live list, so an id create_order accepts is never a 404 there.
Default billing type
An order that omits billing_type is evaluated as client_bill, Junction's documented default.
A sandbox team has been observed evaluating BioReference orders as patient_bill_passthrough
instead (whether that is a team, lab or account setting is not visible from outside), so the
default is configurable per lab slug:
import { createRuntime } from "@crvouga/mockingbird-service-junction"
const junction = createRuntime({ defaultBillingTypes: { bioreference: "patient_bill_passthrough" } })
console.log(junction.instance().defaultBillingTypes) // { bioreference: "patient_bill_passthrough" }--default-billing-type bioreference=patient_bill_passthrough and
PUT /__admin/default-billing-types (per namespace, kept across its reset) do the same. When the
default resolves to a billing type the account lacks, the rejection is worded as the sandbox words
it, 400 {"detail": "Lab 13 does not support billing type patient_bill_passthrough"}; an explicit
billing_type the account lacks keeps Billing type … is not supported by the lab account used for this order. verify --orders orders through each active account with billing_type omitted, so a
run against your team shows which default it uses.
Changing accounts at runtime
Each namespace has its own layout, kept across its own reset. Change one account at a time, or replace them all:
curl -X POST localhost:8787/__admin/lab-accounts/presets/bioreference_ny_nj_delegated
curl -X PATCH localhost:8787/__admin/lab-accounts/$ID -d '{"status": "suspended"}'
curl -X POST localhost:8787/__admin/lab-accounts -d '{"id": "…", "lab": "quest", "states": ["AZ"]}'
curl -X DELETE localhost:8787/__admin/lab-accounts/$ID
curl -X PUT localhost:8787/__admin/lab-accounts -d '{"presets": ["quest_platform"], "accounts": []}'A bad field is a 400 in the admin shape naming it, e.g. lab account x: states: ZZ is not a US state code. --lab-accounts and the labAccounts option take the same layout, so a stack can
declare it at boot with no admin call:
{ "presets": ["bioreference_ny_nj_delegated", { "name": "quest_platform", "id": "…" }], "accounts": [] }Lab-account presets
Named after what they model; ids are deterministicUuid("junction:lab-account:<name>") unless
given. *_platform presets are Junction's own accounts (org_id: null, team_id_allowlist: []);
the other active presets are customer-owned (an Org's account linked to the team). Where a preset copies an account recorded from a real BioReference-linked sandbox team
(2026-09-21), its billing states are the recording's. PLATFORM_ACCOUNT_STATES is every state
but NJ, NY and RI (47).
| Preset | lab |
delegated_flow |
allowed_billing |
team_id_allowlist |
Source |
|---|---|---|---|---|---|
bioreference_ny_nj_delegated |
bioreference |
order_delegated |
client_bill: NY, NJ |
the team | requested shape, not a recorded account |
bioreference_delegated_multi_state |
bioreference |
order_delegated |
client_bill: every state but NY, NJ (48) |
the team | recorded |
bioreference_platform |
bioreference |
not_delegated |
client_bill: PLATFORM_ACCOUNT_STATES |
[] |
recorded ("Junction BioReference Account") |
bioreference_patient_bill_passthrough |
bioreference |
not_delegated |
patient_bill_passthrough: NJ, NY |
the team | recorded |
quest_platform / labcorp_platform |
quest / labcorp |
not_delegated |
client_bill: PLATFORM_ACCOUNT_STATES |
[] |
recorded ("Junction Quest/Labcorp Account") |
bioreference_current_platform |
bioreference |
not_delegated |
patient_bill_passthrough: NJ, NY |
[] |
team listing reported 2026-10-18 (#475) |
quest_sandbox_platform |
quest |
not_delegated |
client_bill: PLATFORM_ACCOUNT_STATES; commercial_insurance: all 50 |
[] |
sandbox listing reported 2026-10-18 (#475) |
sonora_quest_production_platform |
sonora_quest |
not_delegated |
client_bill: PLATFORM_ACCOUNT_STATES |
[] |
production listing reported 2026-10-18 (#475) |
suspended_bioreference / suspended_quest / suspended_labcorp |
as above | not_delegated |
as the platform accounts | [] |
the platform shapes with status: "suspended" |
The September recording's bioreference_platform is illustrative, not a current listing: the
house BioReference account now lists only patient_bill_passthrough for NJ and NY (use
bioreference_current_platform, and see
omitted house-account billing for how production still
accepts client_bill when no account is named). No account at any lab lists DC, so no preset does
either. bioreference_current_platform, quest_sandbox_platform and sonora_quest_production_platform
follow listings an agent
reported from GET /v3/lab_test/lab_account (a consumer-audit report with synthetic ids, not a
repository probe, so best guess, not oracle-verified); the older presets keep their shapes so
pinned suites do not change.
Every preset is status: "active" unless named suspended_*. GET /__admin/lab-accounts/presets
returns the full records, the deprecated aliases, and the synthetic presets (not a recorded
account). bioreference_customer_multi_state is a deprecated alias of bioreference_platform: it
was misnamed, since the record is Junction's platform account, not a customer-owned one. It still
works and keeps its own default id (presetAccountId("bioreference_customer_multi_state")).
Team identity
The emulator answers as one team: team_id on users, orders and webhooks, and the team that
team_id_allowlist is checked against. It is, in order: teamId / --team-id, the team a
version-2 corpus recorded (corpus pull reads it from a listed user, else from the accounts'
allowlists), else the fixed MOCK_TEAM_ID. GET /__admin/team reports it.
A version-2 corpus keeps each account's allowlist verbatim, so an account the real team is not
linked to answers 400 {"detail": "Lab account is not linked to your team"} exactly as it does
live. A version-1 corpus (no teamId) still loads and behaves as 0.2.0 did: every recorded account
is also linked to the emulator team.
Empty allowlists. Junction's own Quest, Labcorp and BioReference accounts carry
team_id_allowlist: [], and a real team's listing returns them, so the emulator lists them and treats
them as linked. Ordering through one by explicit id, and with the id omitted when it is the lab's
only active account, places the order on the sandbox (corpus/lab-account-probes.json, recorded
by the parity run), as the emulator does. verify --orders re-checks it whenever the corpus has such
an account.
Sandbox limits
The emulator exists to escape the sandbox's restrictions, so it enforces none of them unless a test asks — this is a tested guarantee (500 users in a loop all succeed), not an accident.
| Sandbox limit | Modelled | Default | Turn it on |
|---|---|---|---|
| 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} |
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 |
| Rate limits | as a per-namespace budget: 429 {"detail":"Too Many Requests"}, retry-after: 1 (shape not recorded) |
off | rateLimitPerSecond: n |
PUT /__admin/limits merges (null lifts a numeric limit) and is per namespace;
GET /__admin/limits returns the effective values. For a one-off failure instead of a standing
limit, the sandbox_user_quota fault preset is still there.
Fixtures and identity
A suite often needs state the public API cannot build: database fixtures that already carry real
Junction user_ids, or an order already in some status. Insert them directly:
curl -X POST localhost:8787/__admin/users -d '{"user_id": "3f0c…", "client_user_id": "patient-1"}'
curl -X POST localhost:8787/__admin/orders -d '{"user_id": "3f0c…", "lab_test_id": "…", "status": "completed"}'
curl -X POST localhost:8787/__admin/import -d @test/junction-fixtures.json- Users take
user_id?,client_user_id,created_on?,fallback_time_zone?,fallback_birth_date?,ingestion_start?,ingestion_end?. A duplicateuser_idorclient_user_idis a 409;POST /__admin/users/bulkinserts all or none. - Orders take
order_id?,user_id,lab_test_id,lab_account_id?,status?(any transition target),collection_method?,patient_details?,patient_address?,billing_type?,created_at?andresult_fixture?(asPUT /__admin/results/{id}takes it). Request validation is skipped; references are not — the user and lab test must exist (404 otherwise). - Inserted records go through the same builders as
create_user/create_order, so every read path (GET /v2/user/{id}, resolve byclient_user_id,GET /v3/order/{id},GET /v3/orders?user_id=) sees them as API-created. Default timestamps read the emulator clock. - No webhook fires unless the body has
"emitWebhooks": true: fixtures should not trigger backend side effects. fixtures/--fixtures <file>loads{ "users": [...], "orders": [...] }into every namespace at creation and again on each reset.
For flow suites that do not care about Junction identity, identity: "adopt-users" (or
--identity adopt-users, or PUT /__admin/identity {"mode": "adopt-users"} per namespace) creates
an unknown, well-formed user_id on first use — in create_order and GET /v3/orders?user_id= —
and the request proceeds. The journal marks the request adopted: true. Orders are never
adopted: an unknown order id always 404s, because inventing an order hides real bugs. The default,
strict, is Junction's own behaviour.
Is this the emulator?
An emulator 404 and a sandbox 404 are byte-identical, which is how a backend half-pointed at the sandbox goes unnoticed. Three things make it visible:
x-mockingbirdon every response. Assert on it in a test helper:TypeScript import { expect } from "vitest" /** Wrap the fetch your Junction client uses; fails any call that did not reach the emulator. */ export const mockOnlyFetch = (inner: typeof fetch = fetch): typeof fetch => async (input, init) => { const response = await inner(input, init) expect(response.headers.get("x-mockingbird"), `${String(input)} did not reach the Junction emulator`).toMatch( /^junction@/, ) return response }The request journal. Each namespace keeps its last 1000 requests (
--journal-size,journalSize): operation id, method, path, status, duration, fault id, the user / order / lab-account ids it touched, and when (emulator clock).GET /__admin/requests?operationId=create_order_v3_order_postproves an order was placed here; an empty answer proves it was not. Never bodies.Miss headers on 404s. A missing user or order keeps Junction's body byte for byte and adds
x-mockingbird-miss: order <id>(oruser <id>,user client:<client_user_id>) andx-mockingbird-known: users=<n> orders=<n>.
--log-requests prints the same fields as one JSON line per request.
Order control
simulate_order (POST /v3/order/{id}/test) behaves like the sandbox's, including its stepwise
rules. For direct control, the admin API moves an order straight to a status, recording the same
event, dates and appointment cascade, and publishing the same webhook, a real transition does:
curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "collected"}'
curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "completed", "result": "critical"}'
curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "cancelled.testkit.do_not_process"}'to is a full status (completed.testkit.completed), a <phase>.<event> pair, or a name that maps
onto the contract's own status for the order's collection method:
| Name | at_home_phlebotomy |
on_site_collection |
testkit |
walk_in_test |
|---|---|---|---|---|
requisition_created |
received.…requisition_created |
same | same | same |
scheduled |
collecting_sample.…appointment_scheduled |
— | — | collecting_sample.…appointment_scheduled |
shipped / delivered |
— | — | collecting_sample.…transit_customer / …with_customer |
— |
collected |
collecting_sample.…draw_completed |
sample_with_lab.…draw_completed |
collecting_sample.…transit_lab |
— |
at_lab |
— | sample_with_lab.…draw_completed |
sample_with_lab.…delivered_to_lab |
— |
partial_results |
sample_with_lab.…partial_results |
same | — | same |
completed / corrected / cancelled / failed |
completed.…completed / completed.…corrected / cancelled.…cancelled / failed.…sample_error |
same | same | same |
A name with no real status for the method (collected for a walk-in test) is refused with the
valid choices, never approximated. result picks a named result — normal, abnormal,
critical, missing_results — which steers the generator so the payload keeps Junction's exact
shape. For an exact payload, PUT /__admin/results/{id} with { "results": [...], "missing_results": null, "interpretation": "critical", "pdf_base64": "…" }.
Same-second cancel. See the opt-in sandbox quirks.
There is no isTestCancelled in Junction's contract; cancelling produces the vendor's
cancelled.<method>.cancelled event and webhook, which is what a consumer derives that flag from.
Time. Every timestamp reads the runtime clock. POST /__admin/clock {"advance": "2h"} makes
delayed simulations (?delay=<seconds>) due and moves appointment windows, with no sleeping.
Faults and error shapes
Error bodies the emulator reproduces byte for byte, checked against the sandbox by verify and the
parity suite (ORDER_NOT_FOUND holds the unknown-order text per operation id):
| Case | Status | Body |
|---|---|---|
GET /v2/user/{unknown id} |
404 | {"detail":"Not found"} |
GET /v2/user/resolve/{unknown client_user_id} |
404 | {"detail":"User not found"} |
| Unknown order: get order, requisition PDF | 404 | {"detail":"This order doesn't exist"} |
| Unknown order: cancel, simulate | 404 | {"detail":"Order doesn't exist"} |
| Unknown order: result, result metadata, result PDF | 404 | {"detail":"Order not found"} |
| Unknown order: every phlebotomy / PSC appointment route | 404 | {"detail":"This order doesn't exist."} (trailing period included) |
Missing x-vital-api-key |
401 | {"detail":"Missing x-vital-api-key"} |
| Request validation | 422 | {"detail":[{"type":…,"loc":[…],"msg":…,"input":…}]} (Pydantic shape) |
| Business rule | 400 | {"detail":"<message>"} or {"detail":{"error_type":"INVALID_REQUEST","error_message":"…"}} |
Faults reproduce the sandbox's failure modes on demand, for retry paths and error handling:
| Preset | Fault | Fidelity |
|---|---|---|
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 |
rate_limited |
429 {"detail":"Too Many Requests"}, retry-after: 1 |
shape-plausible, not verified |
server_error / bad_gateway / unavailable |
500 / 502 / 503 | shape-plausible, not verified |
import { createRuntime, FAULT_PRESETS } from "@crvouga/mockingbird-service-junction"
const junction = createRuntime()
junction.faults.add({ ...FAULT_PRESETS.sandbox_user_quota, id: "quota", count: 1 }) // next create_user only
junction.faults.add({ id: "flaky-orders", operationId: "create_order_v3_order_post", status: 503, rate: 0.25 })A rule matches on operationId, method, pathPrefix and namespace (all optional), fires
count times or forever, for a rate of matching requests (seeded, so a run replays identically),
after an optional delayMs. A rule added through the admin API defaults to the calling namespace,
so one worker's injected failure never lands on another's request; one added in-process, as above,
applies to every namespace unless given a namespace. Over HTTP: POST /__admin/faults with the same fields, or
POST /__admin/faults/presets/sandbox_user_quota.
A rule can also match on the request itself: path, pathPattern, requestHeaders, query, and
requestBody, a map from a dotted path in the JSON body to the value it must equal. That targets
one member when the backend sends no namespace header, with an optional body to answer with:
# Refuse one member's order
curl -X POST localhost:8787/__admin/faults -d '{"method":"POST","path":"/v3/order",
"requestBody":{"patient_details.email":"member-a@example.test"},"status":503}'
# No phlebotomy slots for one ZIP code
curl -X POST localhost:8787/__admin/faults -d '{"method":"POST",
"pathPrefix":"/v3/order/phlebotomy/appointment/availability",
"requestBody":{"zip_code":"00000"},"status":200,"body":{"slots":[],"timezone":"America/Phoenix"}}'A body that is missing, not JSON, or not a JSON object never matches. See the fault rule reference.
Webhooks
With webhooks / --webhook-url, every event is delivered signed, with retries:
- Standard Svix signatures:
svix-id,svix-timestamp, andsvix-signature: v1,<base64 HMAC-SHA256 of "<svix-id>.<svix-timestamp>.<body>">, keyed by the base64 part ofwhsec_…. The officialsvixverifier, and a receiver written for real Junction, accept it unchanged. (Signing"<timestamp>.<body>"without the message id, as this package's own server script did before, is not Svix-compatible.) - Timestamps are wall-clock even when the emulator clock is moved, because receivers reject timestamps outside a few minutes of their own clock.
- Any 2xx is delivered; anything else is retried on Svix's schedule (immediately, 5 s, 5 min,
30 min, 2 h, 5 h, 10 h, 10 h) or
retryDelaysMs.POST /__admin/webhooks/flushruns pending retries now;POST /__admin/webhooks/{id}/replayredelivers one message.
import { Webhook } from "svix"
import { createRuntime } from "@crvouga/mockingbird-service-junction"
const secret = "whsec_bW9ja2luZ2JpcmQtdGVzdC1zZWNyZXQ="
const junction = createRuntime({
webhooks: { url: "http://127.0.0.1:3100/webhooks/junction", secret, retryDelaysMs: [0, 500, 2000] },
})
// The application's receiver, verifying with the official svix package as it does for real Junction:
const accept = async (request: Request) => {
const body = await request.text()
new Webhook(secret).verify(body, {
"svix-id": request.headers.get("svix-id") ?? "",
"svix-timestamp": request.headers.get("svix-timestamp") ?? "",
"svix-signature": request.headers.get("svix-signature") ?? "",
}) // throws unless the signature is valid
return JSON.parse(body) as { event_type: string }
}
console.log(typeof accept, junction.webhooks?.deliveries().length) // "function" 0Verify
verify is the fidelity guarantee: it runs the same requests against real Junction and the emulator
and reports every divergence.
npx mockingbird-junction verify --real-key "$JUNCTION_SANDBOX_KEY" --corpus test/junction-corpus.json
npx mockingbird-junction verify --real-key "$JUNCTION_SANDBOX_KEY" --orders --sample 50 --json- Drift: re-fetches each recorded observation (or
--sample nof them) and reports any that no longer match production — the signal to re-pull. - Scenario: creates a user, reads it back, provokes the documented 404s (including every
order-scoped operation against a random unknown order), reads the catalog (paged and bare) and a
covered ZIP's serviceability, and deletes the user; with
--orders, also places, reads and cancels an order for the corpus's first lab test, and — when the corpus has an account with an emptyteam_id_allowlist— orders through it with and without its id. Status codes must match; bodies must match in shape, and byte for byte for the documented error bodies.
It exits 1 on any divergence. The scenario always deletes its user (and cancels its order), because
it runs against the same capped sandbox the emulator exists to relieve. verifyAgainstReal is the same
check as a function. This repo runs it in CI against the Junction sandbox.
Determinism
With a fixed clock (clock / POST /__admin/clock {"set": …, "freeze": true}) and seed, a run
replays exactly. Stable across runs: user ids, order ids, event ids, sample ids, booking keys and
webhook message ids (derived from sequence numbers), timestamps (from the clock), generated
availability, and which requests a partial-rate fault hits. Wall-clock by design: webhook
svix-timestamp, /__admin/health uptime, and request log durations.
What is and is not modelled
An emulator that is silent about its gaps is how a green suite starts lying. Specifically:
- Corpus-backed (byte-exact): area and PSC serviceability for covered ZIPs; the catalog, labs, expected results and lab accounts of the team the corpus was recorded from.
- Synthetic (deterministic, shape-faithful, values invented): availability slots and booking
keys; results (biomarker lines and PDFs) unless a fixture is installed; requisition PDFs;
coverage for any ZIP in
--geo syntheticmode. - Not modelled:
delegated_flow-specific ordering. Accounts carry the field and orders follow the documented selection and billing rules above, but any additional behaviour Junction applies to delegated accounts is not reproduced. Runverify --orderswith your team's corpus to check what you rely on.- Rate limits and outages, except when injected as faults or switched on as limits.
- Lab/team-specific omission defaults must be configured through
labAccountOmissionPolicies(see Lab accounts); exact vendor requisition layout is not modelled. - Adopted users (
adopt-users) haveclient_user_idequal to theiruser_id; nothing in Junction corresponds to adoption. - Real 429 / 5xx bodies: the presets are plausible, not recorded.
- Phlebotomy availability breadth: the sandbox serves it only for
85004; the emulator generates slots for any covered ZIP. - Webhook retry jitter across a snapshot restore (the jitter stream is not part of a snapshot).
- Operations outside the vendored OpenAPI subset: they return 404 and are counted as
unmatchedin/__admin/metrics.
API
Main entry (@crvouga/mockingbird-service-junction, runtime-neutral):
| Export | Description |
|---|---|
createRuntime |
(options?: JunctionRuntimeOptions) => JunctionRuntime — the served emulator (health, admin, namespaces, clock, faults, metrics, journal, webhooks) as one fetch. Options: corpus, geo, zipOverlays, catalogOverlays, markerCatalog, addOnSourceMarkers, labAccounts, teamId, limits, identity, defaultBillingTypes, labAccountBehavior, quirks, fixtures, journalSize, webhooks, onWebhook, sqlite, clock, seed, adminKey, onLog. |
JunctionAPI |
Class. new JunctionAPI(options?): one namespace's emulator, implementing fetch(request: Request): Promise<Response>. |
JUNCTION_NAMESPACE |
"junction" — the default namespace's storage key when sharing a sqlite client. |
document |
The vendored Junction OpenAPI document (Mockingbird subset) that drives routing. |
operationIds |
Every operationId in document. |
supportedOperationIds |
The operationIds the emulator implements (all of them). |
pullCorpus |
(options: PullCorpusOptions) => Promise<SealedCorpus> — record a corpus from a real team (GETs only). |
diffCorpus |
(before, after) => CorpusDiff — ZIPs, lab tests, lab accounts and observations added / removed / changed. |
fingerprintCorpus |
(corpus) => Promise<string> — SHA-256 of a corpus's content. |
corpusLabel |
(corpus) => string — the short label /__admin/health and logs show, e.g. v1-2026-09-18-HqFDuEAuhkwD. |
parseSealedCorpus |
(value: unknown) => SealedCorpus; throws on a non-object or unsupported version. |
SEALED_CORPUS_VERSION |
Current corpus format version (2: adds teamId). |
SUPPORTED_SEALED_CORPUS_VERSIONS |
Versions parseSealedCorpus loads ([1, 2]). |
MOCK_TEAM_ID |
The team id used when neither teamId nor the corpus gives one. |
OTHER_TEAM_ID |
A team that is not the emulator's, for fixtures modelling another team's account. |
isLinkedToTeam |
(account, teamId) => boolean — the allowlist rule ([] counts as linked). |
LAB_ACCOUNT_PRESETS |
The lab-account presets, without ids. |
LAB_ACCOUNT_PRESET_ALIASES |
Deprecated preset names → the preset each stands for (bioreference_customer_multi_state → bioreference_platform). |
SYNTHETIC_LAB_ACCOUNT_PRESETS |
Presets that model a requested shape, not a recorded account. |
BILLING_TYPES |
The billing_type values create_order accepts. |
presetAccountId |
(name) => string — a preset's default id. |
PLATFORM_ACCOUNT_STATES / DELEGATED_ACCOUNT_STATES |
The recorded 47- and 48-state client-bill lists. |
LabAccountConflict |
Thrown by addLabAccount for an id that exists. |
DEFAULT_LIMITS |
Every sandbox limit, off. |
sandboxUserQuotaBody |
(max) => body — the sandbox's user-cap 400 body. |
IDENTITY_MODES |
["strict", "adopt-users"]. |
FixtureError |
Thrown by the fixture inserts; status is 400, 404 or 409. |
ORDER_NOT_FOUND |
Junction's unknown-order detail, by operation id. |
MISS_HEADER / KNOWN_HEADER |
"x-mockingbird-miss" / "x-mockingbird-known". |
verifyAgainstReal |
(options: VerifyOptions) => Promise<VerifyReport> — the verify command as a function. |
DEFAULT_JUNCTION_BASE_URL |
"https://api.sandbox.tryvital.io". |
isSandboxKey / SANDBOX_KEY_PREFIXES |
Whether a key is a sandbox team key (sk_us_ / sk_eu_). |
UNKNOWN_ZIP_STATUS / UNKNOWN_ZIP_ERROR_TYPE |
424 / "MOCKINGBIRD_UNKNOWN_ZIP" — the corpus-mode unknown-ZIP error. |
labAccountFromInput |
(input: LabAccountInput) => LabAccountRecord — normalize (and validate) a configured account. |
TEAM_LAB_ACCOUNTS |
The built-in lab-account fixtures. |
US_STATES |
The 50 USState codes Junction accepts. |
ORDER_STATUSES_BY_METHOD |
Every order status in the contract, by collection method. |
TRANSITION_ALIASES |
The transition names above, by method. |
resolveTransition |
(method, target) => { status } | { error } — how /__admin/orders/{id}/transition reads to. |
RESULT_FIXTURES |
Named results (normal, abnormal, critical, missing_results). |
FAULT_PRESETS |
Named Junction faults (sandbox_user_quota, rate_limited, server_error, bad_gateway, unavailable). |
createWebhookDispatcher |
(endpoint: WebhookEndpoint) => WebhookDispatcher — signed delivery with retries, standalone. |
signSvix / verifySvix |
Produce / check a standard Svix svix-signature. |
observationCacheKey |
(url, method = "GET", body?) => string — the "<METHOD> <path>?<sorted query> <json>" key corpora use. |
prefetchCoverageObservations |
Parity helper: records coverage-ZIP reads from the sandbox into an observation cache before seedFrom. |
reshapeCoverageGeoCommand |
Parity-walk hook that pins geo/availability parameters onto the coverage corpus. |
COVERAGE_ZIPS |
The ZIPs the shipped corpus covers. |
PSC_AVAILABILITY_ZIPS |
ZIPs that also get PSC availability observations. |
PHLEBOTOMY_AVAILABILITY_ZIPS |
ZIPs the sandbox serves phlebotomy availability for (["85004"]). |
PSC_LAB_IDS |
Lab ids used for PSC reads ([4, 6, 13, 25]). |
AVAILABILITY_ADDRESS |
Stable Phoenix, AZ address used in availability requests so cache keys match. |
AVAILABILITY_START_DATE |
Far-future availability start_date ("2099-06-15"). |
./server (Node): createServer(options?) => Promise<JunctionServer>, serveTarget (what
serve and serve --config use), loadCorpus(source), DEFAULT_PORT (8787).
./corpus: defaultCorpus.
JunctionRuntime (from createRuntime) members: fetch, instance(namespace?), namespaces(),
reset(namespace? | "*"), snapshot(namespace?), restore(snapshot, namespace?), clock
(now, set, advance, freeze, unfreeze, reset, state), faults (add, list,
remove, clear), metrics (report, reset), journal (list, clear, size), webhooks
(deliveries, replay, flush, idle, clear), sqlite, rng.
JunctionAPI members:
| Member | Description |
|---|---|
fetch(request) |
Handle one Junction REST request. |
reset() |
Promise<void> — clear state; re-install the corpus or default catalog, and the configured lab accounts. |
installCorpus(corpus) |
Install a SealedCorpus: observations (shared, read-only), catalog, labs, lab accounts, ZIP coverage. |
corpusInfo() |
CorpusInfo | undefined — label, recording date, source and counts. |
geoMode |
"corpus" | "synthetic" | "corpus+synthetic", settable. |
zipOverlays |
ZIP aliases and overrides for this namespace; settable, kept across reset(). |
configureLabAccounts(layout?) / labAccounts() |
Replace (a list, or { presets, accounts }; undefined restores the default) / read the team's lab accounts. |
addLabAccount(input) / addLabAccountPreset(name, id?) / patchLabAccount(id, patch) / removeLabAccount(id) |
Change one account; kept across reset(). |
teamId |
The team this namespace answers as. |
limits / configureLimits(input) |
Read / change the sandbox limits; kept across reset(). |
identity |
"strict" | "adopt-users", settable. |
defaultBillingTypes |
Lab slug → the billing_type an order gets when it omits one (default billing type); settable, kept across reset(). |
insertUsers(users) / insertOrders(orders, { emitWebhooks? }) / importFixtures(fixtures, options?) / hardDeleteUser(id) |
The fixture backdoor. |
order(id) / orders() |
Read orders. |
orderLabAccount(id) |
The lab account id an order was placed with (null: Junction's platform account; undefined: unknown order). |
transitionOrder(id, status, { now, flags? }) |
Move an order to a full status and publish its webhook. |
installResultFixture(orderId, fixture) / resultFixture(orderId) |
Serve an exact result payload / PDF for an order. |
seedFrom(source, observations?) |
Promise<SeedReport> — copy users, orders, appointments, catalog and labs from a real Junction environment. |
ensureLabTests(source, ids) / ensureOrders(source, ids) |
Promise<number> — fetch specific lab tests / orders from source if missing. |
markUserDeleted(userId) |
Mark a user as deleted (subsequent reads behave like a deleted user). |
webhookEvents() |
JunctionWebhookEvent[], oldest first. |
webhookDeliveryAttempts() |
The retry schedule Junction would follow per event (a record; see webhooks for delivery). |
app / sqlite |
The underlying Hono app and SqliteClient. |
Options and types:
type JunctionAPIOptions = {
sqlite?: SqliteClient // share one client across services; default: fresh in-memory DB
namespace?: string // storage namespace; default "junction"
now?: () => number // clock in ms; default Date.now
corpus?: SealedCorpus // installed now and on every reset()
geo?: "corpus" | "synthetic" | "corpus+synthetic" // default "corpus" with a corpus, else "synthetic"
zipOverlays?: { aliases?: Record<string, string>; overrides?: Record<string, { phlebotomy_served?: boolean; no_pscs?: boolean }> }
catalogOverlays?: CatalogOverlay[] // lab tests cloned from catalog ones, now and on every reset()
markerCatalog?: MarkerCatalogEntry[] // markers GET /v3/lab_tests/markers offers on top of the catalog's
addOnSourceMarkers?: boolean // add-on result lines carry source_markers; default false
labAccounts?: LabAccountInput[] | { presets?: (string | { name; id? })[]; accounts?: LabAccountInput[] }
teamId?: string // default: the corpus's recorded team, else MOCK_TEAM_ID
limits?: { maxUsers?: number | null; simulateRequiresSandbox?: boolean; rateLimitPerSecond?: number | null }
identity?: "strict" | "adopt-users" // default "strict"
defaultBillingTypes?: Record<string, BillingType> // lab slug → billing_type when omitted; default client_bill
fixtures?: { users?: UserFixture[]; orders?: OrderFixture[] } // loaded now and on every reset()
onWebhook?: WebhookPublisher // called with every recorded event
webhook?: JunctionWebhookOptions
}
type LabAccountInput = { id; lab; status?; delegated_flow?; account_name?; provider_account_id?; org_id?;
business_units?; default_clinical_notes?; allowed_billing?: Record<billingType, State[]>; states?: State[];
team_id_allowlist? }
type WebhookEndpoint = { url; secret: "whsec_…"; retryDelaysMs?: number[]; timeoutMs?; headers?; fetch? }
type ResultFixture = { name; results?: unknown[]; missing_results?: unknown[] | null; interpretation?; pdf_base64? }
type FaultRule = { id; operationId?; method?; pathPrefix?; status; body?; headers?; count?; rate?; delayMs? }
type JunctionWebhookOptions = { seed?: number; jitterRatio?: number /* 0..1, default 0.1 */; timeoutMs?: number }
type JunctionWebhookEvent = {
event_type: "labtest.order.created" | "labtest.order.updated" | "labtest.appointment.updated"
data: Record<string, unknown>; team_id: string; user_id: string; client_user_id: string
}
type SealedCorpus = { version: 1 | 2; recordedAt; source; teamId?; fingerprint?; observations; catalog: { labTests; labs; expectedResults }; labAccounts }
type SeedSource = { fetch: (request: Request) => Promise<Response>; baseUrl: string; headers: Record<string, string> }
type OperationId / SupportedOperationId // string unions of operationIds / supportedOperationIdsSeeding from the real sandbox (needs network and a real key):
import { JunctionAPI } from "@crvouga/mockingbird-service-junction"
const junction = new JunctionAPI()
const report = await junction.seedFrom({
fetch: (request) => globalThis.fetch(request),
baseUrl: "https://api.sandbox.tryvital.io",
headers: { "x-vital-api-key": process.env.JUNCTION_API_KEY ?? "" },
})
console.log(report.users, report.orders)Development
For contributors to the mockingbird repo only.
bun test # offline suites, incl. scheduling state space
bun test junction.runtime.property.test.ts # service contract, corpus, lab accounts, faults, webhooks, verify
bun test junction.sdk.property.test.ts # drives the served emulator through @tryvital/vital-node
bun run emulator:serve # env-configured `serve`: HOST, PORT, MOCKINGBIRD_JUNCTION_CORPUS,
# MOCKINGBIRD_JUNCTION_WEBHOOK_URL / _SECRET / _SCOPE
bun run corpus:record -- --force # re-record the shipped corpus/sandbox-sealed.json (JUNCTION_API_KEY)
bun run verify -- --real-key "$KEY" # `mockingbird-junction verify` from sourceLive parity (primary proof is seedParity: warmup N on the sandbox, seedFrom, then lockstep M):
bun run parity # default mode=seed against api.sandbox.tryvital.io
bun run parity -- --warmup 15 --compare 30 --runs 25
bun run parity -- --mode=empty # legacy empty-start differentialWebhook parity needs a public receiver; see docs/webhook-parity.md.
Asynchronous testkit simulation
A completion-target simulation on an active testkit advances through delivery and queues
completion for the next millisecond of the injected now clock. This is a deterministic
emulator scheduling boundary, not a claim about Junction's processing latency. Order, result,
transaction, and order-list reads apply due work to its owning order. A second simulation
is not required. Results remain unavailable while the testkit is delivered_to_lab.
Cancelled orders never complete from queued simulation work.
The parity harness polls order reads after successful immediate completion simulations on
both targets. Testkit completion must settle within 15 seconds or verification fails;
explicitly delayed simulations and unsuccessful requests are not treated as settled.
The USSL testkit result metadata follows the observed sandbox fixture (null specimen dates
and CLIA 05D2130115); other laboratories retain their existing metadata behavior.
Part of mockingbird — agent integration guide: README · llms.txt · report an issue or request a feature.
Lab-account behavior profiles
createRuntime({ labAccountBehavior: { profile, accounts } }) selects a named policy.
native is the backward-compatible default. expected-contract enforces the
documented lab/account association and billing rules,
including rejecting a selected account belonging to another lab. This is an assumption
from the public contract, not measured production parity. Both expected-contract
and sandbox-observed return the persisted account association in POST/GET; a synthetic
requisition uses that same stored provider account number.
For #440, the scoped account-owned
billing control is a best guess, not oracle-verified. The reporter observed an explicit
client_bill refusal for a root-selected delegated BioReference account even though its listing
allowed that billing in AZ. They reported 400 {"detail":"Lab 13 does not support billing type patient_bill_passthrough"}
in the sandbox and the same error with Lab 14 in production, including sandbox panel/add-on,
home/walk-in and physician-present controls. These are report-derived errors; neither their
cause nor a general vendor billing rule has been established. The repository's
2026-10-20 sanitized probe
could not find matching account/test inventory, recorded no order observations and created no
user or orders. This evidence limit does not prevent a suite from testing the explicit fallback.
Configure the reported refusal against the ID of a synthetic account loaded by labAccounts
or your corpus. For example:
import { createRuntime } from "@crvouga/mockingbird-service-junction"
const delegatedAccountId = "11111111-1111-4111-8111-111111111111"
const runtime = createRuntime({
labAccounts: [{
id: delegatedAccountId,
lab: "bioreference",
org_id: "55555555-5555-4555-8555-555555555555",
delegated_flow: "order_delegated",
allowed_billing: { client_bill: ["AZ"] },
}],
labAccountBehavior: {
profile: "observed",
oracle: "sandbox",
accounts: {
[delegatedAccountId]: {
rejectBilling: { client_bill: "Lab 13 does not support billing type patient_bill_passthrough" },
},
},
},
})Load a BioReference panel or marker catalog in the same runtime's corpus, then submit your
normal valid order with root lab_account_id: delegatedAccountId and
billing_type: "client_bill". This configuration returns the exact configured 400 detail before
creating an order or webhook, including when physician is present. It applies to both panel
and add-on orders and home/walk-in collection. To model the production report, choose
oracle: "production" and change the configured detail to Lab 14 ...; the oracle label
describes the intended environment, not proof of verification. sandbox-observed remains an
alias for observed with oracle: "sandbox".
The fallback is deliberately narrow: only that listed account's explicit root ID and explicit
billing activate rejectBilling. Other delegated accounts keep their own policies. Omitting
billing_type still uses defaultBillingTypes (or client_bill when no default is configured)
and checks the account's listed billing. Thus this fixture accepts an omitted default
client_bill even though it refuses explicit client_bill; configuring an omitted
patient_bill_passthrough default rejects because the account lacks that billing, independently
of rejectBilling. This distinction is the chosen emulator contract, not a verified production
omission behavior. Nested-only IDs do not select the customer account. In the sandbox profile
they follow the platform fallback with null POST/GET association; a successful root-selected
account retains its persisted ID on both responses. Never treat nested-only acceptance as a
successful delegated-account selection or retry a rejected customer order by dropping its ID.
These controls do not model why Junction overrides the requested billing, validate physician
selection, or establish any production omission/returned-identity rule. Native behavior remains
the default. The separately reported NY/NJ acceptance uses the
allowBilling positive control below.
GET/PUT /__admin/lab-account-behavior inspect/change one
namespace's policy; changes survive its reset and never affect another namespace.
Unknown profile names are rejected. CLI/server equivalents are --lab-account-profile
and --lab-account-sandbox-policies <json-file> (the account-policy object).
sandbox-observed also encodes the owner-recorded October 2026 sandbox omission controls
(#439), which are recorded observations, best guess, not oracle-verified by this repository
(its sandbox probe still finds no matching account inventory; no live POST/GET identity
comparison has been run here): an order whose
root lab_account_id is omitted, nested only in order_set, or an unknown UUID is
placed on the lab's platform account (lab_account_id: null on POST and GET, no listed
allowed_billing check), while a root ID naming a listed account keeps its identity and
policy. expected-contract keeps the strict behavior (unknown ID → 400, omission → the
house account). The native profile keeps its historical response shape (no lab_account_id
on the order); the explicit profiles return the stored association, never the requested id, so a
test that asserts identity on both POST and an independent GET cannot pass on an ignored id.
junction.account-profiles.test.ts holds the positive control and the six negative controls.
Explicit Junction-owned account in an uncovered state (#494)
Under sandbox-observed (or observed with oracle: "sandbox"), a root lab_account_id
naming a Junction-owned account (org_id: null) whose listing has no billing type for the
patient's state is refused with 400 {"detail":"Lab <lab name> does not support state <ST>"}
(for example Lab BioReference does not support state AZ) before any billing-type check, and no
order or webhook is created. This follows the reported sandbox refusal (2026-10-19); it is a
best guess, not oracle-verified here, and the reporter did not re-probe a covered state. It is
kept apart from the billing-type message (Lab 13 does not support billing type ...) that a
customer-owned account returns. A state that the account lists for another billing type keeps the
billing message, an account-owned allowBilling exception counts as coverage, and
customer-owned accounts, native, expected-contract and the production oracle keep their
existing messages (the state rule is a sandbox observation only).
The explicit profiles also accept order_set.add_on with provider_ids or marker_ids
from one lab's configured marker catalog. The selected markers determine the synthetic
custom panel; unresolved or cross-lab combinations refuse without placing an order.
Combining lab_test_ids and add_on in one request is explicitly refused by these profiles
until the combined-panel semantics are verified. Malformed add-ons also refuse without placing
an order; requested add-on markers are never silently discarded.
Panel and custom orders share account/billing checks for home and walk-in collection.
The native profile keeps its existing panel-only contract.
Explicit NY/NJ customer-account billing (#441)
The positive control for #441 is a
best guess, not oracle-verified. The reporter observed a non-delegated customer-owned
BioReference account accepting explicit client_bill in NY despite advertising only
patient_bill_passthrough for NY/NJ. The reported POST and independent GET both retained
the requested account ID and client_bill. The
public billing rules
require the requested billing type and patient state to be listed; they do not establish this
account-specific exception. The repository's
2026-10-20 sandbox probe
returned complete: false with matching account inventory unavailable, no observations and
no resources created. No vendor acceptance was verified by that run.
Load a synthetic active BioReference customer account with delegated_flow: "not_delegated"
and allowed_billing: { patient_bill_passthrough: ["NY", "NJ"] }, plus an orderable
BioReference panel. Set labAccountBehavior to the following, replacing the synthetic UUID
with that fixture account's ID:
{
"profile": "observed",
"oracle": "sandbox",
"accounts": {
"22222222-2222-4222-8222-222222222222": {
"allowBilling": { "client_bill": ["NY", "NJ"] }
}
}
}Submit a valid order with that ID at the root lab_account_id, explicit
billing_type: "client_bill" and a NY or NJ patient address. POST and independent GET
preserve both the selected account ID and client_bill; the account listing still advertises
only its original patient_bill_passthrough states. This models an ordering exception without
rewriting inventory or changing the requested billing. Including NJ is an explicit fixture
assumption from the reported NY/NJ account, not an observed NJ oracle response.
The exception applies only to the configured account, explicit billing type and configured
states under observed (or its sandbox-observed alias). An unconfigured account or a state
outside NY/NJ still fails the listed billing/state checks. Omitting billing_type evaluates
defaultBillingTypes against the listed account billing: the default client_bill rejects,
while an explicitly configured patient_bill_passthrough default accepts in NY/NJ and is
returned as such. Native and expected-contract profiles retain their existing validation.
The separate delegated refusal in #440 still requires its own rejectBilling entry; neither
control establishes a universal rule based on delegation or lab. See
junction.account-profiles.test.ts for the acceptance cases and strict negative controls.
Observed profile and oracle label
profile: "observed" with oracle: "sandbox" | "production" is the general form of
sandbox-observed (an alias that always reports oracle: "sandbox"), so
GET /__admin/lab-account-behavior says which vendor environment a configuration reproduces.
Account-owned rejectBilling / allowBilling observations apply to either oracle, per
account and never as a blanket delegated-account rule: a delegated customer account refuses
explicit client_bill only when its own entry says so. The October 2026 sandbox omission
recordings (#439) are sandbox observations, so they apply only to oracle: "sandbox"; with
oracle: "production" an omitted account selects the house account as expected-contract
does, except that the production oracle defaults omittedAccountBilling to
{ bioreference: "unchecked" } (#495, below). Whether production also refuses a delegated account
when billing_type is omitted is unverified, so it is not modelled. oracle on any other profile, or production on the
sandbox-observed alias, is refused.
Omitted house-account billing (#467)
A reporter observed production accepting an AZ client_bill order with no lab_account_id on
a team whose house BioReference account lists only patient_bill_passthrough for NY and NJ,
placing it on the house account (its requisition prints the house account number). That is a
single consumer report, not a repository probe, so the default is unchanged (the listing is
enforced). profile: "observed", oracle: "production" is the named profile for this
production recording (#495) and turns it on for BioReference by default, best guess, not
oracle-verified (sandbox, by contrast, places an omitted-id order on the platform account with a
null association, see #439 above). With it, an omitted lab_account_id places the order on the
Junction-owned account, the requisition names that account's number, and the account listing is
unchanged (it still lists client_bill for no state). Set omittedAccountBilling: { bioreference: "listed" } to turn it off. For any other profile, opt in per lab:
import { createRuntime } from "@crvouga/mockingbird-service-junction"
createRuntime({
labAccountBehavior: {
profile: "expected-contract",
omittedAccountBilling: { bioreference: "unchecked" },
},
})With unchecked, an order whose root lab_account_id is omitted (a nested-only id is
ignored) and that resolves to a Junction-owned account (org_id: null) skips that account's
listed billing and state check. The order keeps the house account's identity on POST, GET and
the requisition. Explicit selection of the house account keeps its listed policy (or an
account-owned rejectBilling), and a customer-owned account chosen by routing keeps its own
listing. GET/PUT /__admin/lab-account-behavior read and change omittedAccountBilling.
Where an order's account id was sent (#473)
GET /__admin/orders adds lab_account_request: { labAccountId, nestedLabAccountId } per order
(instance().orderLabAccountRequest(id)): the id carried at the request root, which selects
the account, and the one nested in order_set, which Junction ignores. Each is null when
absent. The request body itself is still not stored; orders seeded by a fixture have
lab_account_request: null.
Sandbox quirks
Behaviors the sandbox shows that clients must tolerate, every one off by default
(createRuntime({ quirks }), or per namespace GET/PUT /__admin/quirks; they survive reset()).
| Quirk | Effect |
|---|---|
sameSecondCancelStatus (#474) |
POST /v3/order/{id}/cancel in the same wall-clock second as the order's creation records a cancelled.<method>.cancelled event with the same timestamp, but status, last_event and updated_at stay received; a repeat cancel is a no-op. A cancel at least one second later reads back cancelled. |
This was reported from the sandbox (not re-measured here, production unverified), so it is a best guess, not oracle-verified. It is off by default; a client that tests it should read the event log and wait at least a second between creating and cancelling. The quirk does not publish a webhook or cascade-cancel appointments for the stale cancel; the report does not say what the vendor does there.
Requisition text layout (#464)
The synthetic requisition PDF prints two text lines: Requisition <order id> and
Account number: <provider number> (unknown when the order has no frozen number), so an
extractor that accepts a field label only at the start of a line finds it. The layout is for
interoperability, not vendor pixel fidelity.