@crvouga/mockingbird-service-junction
Stateful, in-process mock of the Junction (formerly Vital) API for
test suites: users (/v2/user), the lab-testing catalog, lab orders (create, cancel, simulate,
results, requisitions), and at-home phlebotomy / patient-service-center (PSC) scheduling
(availability, booking, reschedule, cancel), plus the labtest.order.* and
labtest.appointment.updated webhook events. All 39 operations in the vendored OpenAPI subset are
served; behaviour is verified by differential property tests against the Junction sandbox.
Use it when your backend calls Junction via fetch or @tryvital/vital-node and you want tests to
run offline, deterministically, with no sandbox key.
- Operation coverage: SUPPORT.md
- Drop-in readiness, sealed corpus and sandbox quirks the mock mirrors: docs/drop-in.md
- Consumer wiring checklist: docs/qa-followon.md
- Behaviour notes: docs/behavior.md
Install
npm install -D @crvouga/mockingbird-service-junction
ESM only. Requires Node >= 22 or Bun >= 1.2. No native dependencies: state lives in an in-memory
SQLite engine (@crvouga/mockingbird-service-sqlite, pure TypeScript). To serve it over HTTP also
install @crvouga/mockingbird-adapter-node (works under Node and Bun) or
@crvouga/mockingbird-adapter-bun.
Usage
Behaviour the examples rely on (all from the source):
- Any host works. Routing uses only the path (
/v2/...,/v3/...). - Auth: every request needs an
x-vital-api-keyheader; any value is accepted (sandbox keys look likesk_us_*/sk_eu_*). Without it the response is401 {"detail":"Missing x-vital-api-key"}. State is not partitioned by key: all requests share one team (team_id11111111-1111-4111-8111-111111111111). - Bodies are JSON with the API's snake_case field names.
- A default synthetic catalog is installed at construction (lab tests with methods
walk_in_test,testkitandat_home_phlebotomy);installCorpusreplaces it with a recording.
In-process (inject fetch)
import { JunctionAPI } from "@crvouga/mockingbird-service-junction"
const junction = new JunctionAPI({ now: () => Date.UTC(2030, 0, 1) })
const headers = { "x-vital-api-key": "sk_us_mockingbird", "content-type": "application/json" }
const call = async (method: string, path: string, body?: unknown) => {
const response = await junction.fetch(
new Request(`https://api.sandbox.tryvital.io${path}`, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
}),
)
return { status: response.status, json: (await response.json()) as Record<string, unknown> }
}
const user = await call("POST", "/v2/user", { client_user_id: "app-user-1" })
const userId = String(user.json.user_id)
const catalog = await call("GET", "/v3/lab_test")
const labTests = catalog.json.data as { id: string; method: string }[]
const testkit = labTests.find((test) => test.method === "testkit")
if (testkit === undefined) throw new Error("default catalog has a testkit test")
const order = await call("POST", "/v3/order", {
user_id: userId,
patient_details: {
first_name: "Ada",
last_name: "Lovelace",
dob: "1990-01-01",
gender: "female",
phone_number: "+14155551234",
email: "ada@example.com",
},
patient_address: {
first_line: "1 N Central Ave",
city: "Phoenix",
state: "AZ",
zip: "85004",
country: "US",
},
order_set: { lab_test_ids: [testkit.id] },
})
console.log(order.status, (order.json.order as { id: string }).id) // 200 "<uuid>"
// Every event the mock published, oldest first:
console.log(junction.webhookEvents().map((event) => event.event_type)) // ["labtest.order.created"]
now (milliseconds) 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 now is past the due time.
Over HTTP
import { serve } from "@crvouga/mockingbird-adapter-node"
import { JunctionAPI } from "@crvouga/mockingbird-service-junction"
const junction = new JunctionAPI()
const server = await serve(junction, { port: 0, host: "127.0.0.1" }) // port 0 = ephemeral
const address = server.address()
if (address === null || typeof address === "string") throw new Error("not listening")
const baseUrl = `http://127.0.0.1:${address.port}`
const response = await fetch(`${baseUrl}/v2/user`, {
headers: { "x-vital-api-key": "sk_us_mockingbird" },
})
console.log(response.status) // 200
server.close()
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):
import { VitalClient } from "@tryvital/vital-node"
const client = new VitalClient({ apiKey: "sk_us_mockingbird", environment: baseUrl })
const user = await client.user.create({ clientUserId: "app-user-1" })
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.
Webhooks
onWebhook is called synchronously with every event the mock records
(labtest.order.created, labtest.order.updated, labtest.appointment.updated). Delivery and
signing are up to you. Junction delivers through Svix; the contributor server script signs with
svix-id, svix-timestamp and svix-signature: v1,<base64 HMAC-SHA256 of "<timestamp>.<body>">,
keyed by the base64-decoded secret after stripping a whsec_ prefix:
import { createHmac, randomUUID } from "node:crypto"
import { JunctionAPI } from "@crvouga/mockingbird-service-junction"
const WEBHOOK_URL = "http://127.0.0.1:3100/webhooks/junction"
const WEBHOOK_SECRET = "whsec_bW9ja2luZ2JpcmQtdGVzdC1zZWNyZXQ="
const junction = new JunctionAPI({
onWebhook: (event) => {
const timestamp = Math.floor(Date.now() / 1000).toString()
const body = JSON.stringify(event)
const key = Buffer.from(WEBHOOK_SECRET.replace(/^whsec_/, ""), "base64")
const signature = createHmac("sha256", key).update(`${timestamp}.${body}`).digest("base64")
void fetch(WEBHOOK_URL, {
method: "POST",
headers: {
"content-type": "application/json",
"svix-id": randomUUID(),
"svix-timestamp": timestamp,
"svix-signature": `v1,${signature}`,
},
body,
}).catch(() => undefined)
},
})
console.log(junction.webhookEvents().length) // 0
webhookDeliveryAttempts() returns the retry schedule Junction would follow for each event
(8 attempts: immediately, then 5s, 5m, 30m, 2h, 5h, 10h, 10h, with jitter controlled by the
webhook option). It is a record only; nothing is re-sent.
Resetting between tests
reset() clears users, orders, appointments and webhook events, then re-installs the default
catalog, or the corpus passed to installCorpus if there was one:
import { beforeEach, expect, test } from "bun:test"
import { JunctionAPI } from "@crvouga/mockingbird-service-junction"
const junction = new JunctionAPI()
beforeEach(() => junction.reset())
test("starts with no users", async () => {
const response = await junction.fetch(
new Request("https://api.sandbox.tryvital.io/v2/user", {
headers: { "x-vital-api-key": "sk_us_mockingbird" },
}),
)
expect(await response.json()).toMatchObject({ users: [], total: 0 })
})
Sealed corpus (optional, higher fidelity)
Provider-owned inventory (area serviceability, PSC site lists, the team's lab catalog, labs, lab
accounts) cannot be synthesized faithfully. A sealed corpus is an exact sandbox recording of those
parameter-stable GETs; installing it makes the mock answer them byte-for-byte. The recording in the
repo (corpus/sandbox-sealed.json) is not shipped in the npm package; copy it or record your own
(see Development).
import { readFileSync } from "node:fs"
import { JunctionAPI, parseSealedCorpus } from "@crvouga/mockingbird-service-junction"
const junction = new JunctionAPI()
const recording: unknown = JSON.parse(readFileSync("test/junction-sandbox-sealed.json", "utf8"))
junction.installCorpus(parseSealedCorpus(recording)) // re-applied on every reset()
Availability is never recorded (its slot dates and single-use booking_keys cannot be replayed);
it always comes from the deterministic generator.
API
| Export | Description |
|---|---|
JunctionAPI |
Class. new JunctionAPI(options?); implements FetchAPI (fetch(request: Request): Promise<Response>). |
JUNCTION_NAMESPACE |
"junction" — SQLite namespace holding the mock's state 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 mock implements (all of them). |
parseSealedCorpus |
(value: unknown) => SealedCorpus; throws on a non-object or unsupported version. |
SEALED_CORPUS_VERSION |
Current sealed-corpus format version (1). |
observationCacheKey |
(url, method = "GET", body?) => string — the "<METHOD> <path>?<sorted query> <json>" key used by the observation cache and corpus. |
prefetchCoverageObservations |
Parity helper: records coverage-ZIP area/PSC/availability reads from the real sandbox into an observation cache before seedFrom. |
reshapeCoverageGeoCommand |
Parity-walk hook that pins geo/availability parameters onto the coverage corpus. |
COVERAGE_ZIPS |
ZIP codes the coverage corpus seals for area/PSC reads. |
PSC_AVAILABILITY_ZIPS |
ZIPs that also get PSC availability observations. |
PHLEBOTOMY_AVAILABILITY_ZIPS |
ZIPs served for phlebotomy availability (["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"). |
JunctionAPI members:
| Member | Description |
|---|---|
fetch(request) |
Handle one Junction REST request. |
reset() |
Promise<void> — clear state; re-install the default catalog or the installed corpus. |
installCorpus(corpus) |
Install a SealedCorpus (observation cache, catalog, labs, lab accounts). |
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() |
Recorded retry schedule per event. |
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
now?: () => number // clock in ms; default Date.now
onWebhook?: WebhookPublisher // called with every recorded event
webhook?: JunctionWebhookOptions
}
type JunctionWebhookOptions = { seed?: number; jitterRatio?: number /* 0..1, default 0.1 */; timeoutMs?: number /* default 15000 */ }
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 WebhookPublisher = (event: JunctionWebhookEvent) => void
type SeedSource = { fetch: (request: Request) => Promise<Response>; baseUrl: string; headers: Record<string, string> }
type SeedObservations = { getCache?: ReadonlyMap<string, GetCacheEntry> }
type SeedReport = { users; orders; appointments; catalogTests; labs; cacheEntries: number }
type GetCacheEntry = { status: number; headers: Record<string, string>; body: unknown }
type SealedCorpus = { version: 1; recordedAt; source; observations; catalog: { labTests; labs; expectedResults }; labAccounts }
type OperationId / SupportedOperationId // string unions of operationIds / supportedOperationIds
Seeding 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; these scripts and the corpus are not shipped in the npm package.
bun test # offline suites, incl. scheduling state space
bun test junction.seed.property.test.ts # offline monkey (no network)
bun test junction.sdk.property.test.ts # drives the served mock through @tryvital/vital-node
bun run mock:serve # HOST=127.0.0.1 PORT=8787; GET /health, POST /__admin/reset
MOCKINGBIRD_JUNCTION_CORPUS=corpus/sandbox-sealed.json bun run mock:serve
bun run corpus:record # record corpus/sandbox-sealed.json (needs a sandbox key)
mock:serve installs corpus/sandbox-sealed.json when present, and signs webhooks to
MOCKINGBIRD_JUNCTION_WEBHOOK_URL with MOCKINGBIRD_JUNCTION_WEBHOOK_SECRET (delivery is disabled
without a secret). /health and /__admin/reset bypass the x-vital-api-key gate.
Live 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 differential
Webhook parity needs a public receiver; see
docs/webhook-parity.md.
The client-parity*.ts SDK scenarios are deprecated as proof and kept only as manual probes.
Part of mockingbird — agent integration guide: @crvouga/mockingbird.