Declaring a Local Emulator
A lexicon that can be exercised without a cloud account declares an
emulator capability: enough for chant to boot the container, wait for it to
be healthy, and tell tooling where it is. chant emulator up|down|status and
behold serve --local both drive it.
The declaration
Section titled “The declaration”import { emulatorLifecycle, type EmulatorCapability, type EmulatorSpec } from "@intentius/chant/op";
export const MY_EMULATOR_SPEC: EmulatorSpec = { name: "chant-myfake", // default container name image: "org/myfake:1.2.0", // pinned, not :latest containerPort: 4599, healthPath: "/_myfake/health", upstream: { repo: "org/myfake" }, // where releases are published};
export const MY_EMULATOR: EmulatorCapability = { spec: MY_EMULATOR_SPEC, env: (endpoint) => ({ MYFAKE_ENDPOINT_URL: endpoint }),};
const myfake = emulatorLifecycle(MY_EMULATOR_SPEC);export const myfakeUp = (args = {}, signal?: AbortSignal) => myfake.up(args, signal);export const myfakeDown = (args = {}, signal?: AbortSignal) => myfake.down(args, signal);
// src/plugin.tsemulator: MY_EMULATOR,EmulatorSpec (packages/core/src/op/emulator-lifecycle.ts:12) has four
required fields and three optional ones:
| Field | Required | Meaning |
|---|---|---|
name | yes | Default container name |
image | yes | Docker image, with a version tag |
containerPort | yes | Port the emulator listens on inside the container |
healthPath | yes | Health path on the host port |
ready | no | (healthBody: string) => boolean. Default: any 200 is ready |
runArgs | no | Extra docker run args inserted before the image, e.g. a socket mount |
upstream | no | { repo }, the owner/repo whose latest release names the current version |
lexicons/fly/src/op/activities/flaps.ts:25 is the reference declaration.
The same spec drives emulatorLifecycle(), so the wrapper your Ops already use
and the capability chant boots are one object rather than two that can drift.
Having one is not declaring one
Section titled “Having one is not declaring one”This distinction once cost the repo three of its four emulators. azure and gcp
both built the exact spec above in src/op/activities/ and used it from their
Ops without setting plugin.emulator, so chant emulator up --all booted Floci
and nothing else while the local-testing docs presented all three clouds as
equal local targets (chant #1345). All five capabilities are declared now. The
lesson holds anyway. The wrapper is not the declaration.
More than one
Section titled “More than one”emulator is typed EmulatorDeclaration, which is one capability or a readonly
array of them. fly declares two (lexicons/fly/src/plugin.ts:33):
emulator: [MUDFLAPS_EMULATOR, SPRITZER_EMULATOR],chant emulator reports one line per emulator rather than per lexicon, and
--lexicon fly acts on both.
env names the endpoint variable
Section titled “env names the endpoint variable”env(endpoint) returns what points tooling at the running emulator. Two things
depend on getting it right:
chant emulator up --jsonhands it to a consumer verbatim, so credentials and a region belong here too. Floci returnsAWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEYandAWS_REGIONalongsideAWS_ENDPOINT_URL(lexicons/aws/src/op/activities/floci.ts:70).--live --env <name>endpoint injection picks out only the variables whose value is the endpoint and sets those, leaving the credentials alone. Core derives that by callingenv()with a sentinel and keeping the keys that carry it (endpointEnvVarsinpackages/core/src/op/emulator-lifecycle.ts:82). That replaced a hand-maintained map which listed two of the four lexicons with an endpoint variable and asserted azure had none, while azure’s owndescribe-resources.tsreadAZURE_ENDPOINT_URLon every call.
Each of the five declared capabilities names exactly one endpoint variable:
AWS_ENDPOINT_URL, AZURE_ENDPOINT_URL, GCP_ENDPOINT_URL,
FLY_FLAPS_BASE_URL (mudflaps) and SPRITES_BASE_URL (spritzer). Return {}
if your emulator is reached only through an explicit apply argument. That is a real answer, and a better one than naming a
variable nothing reads.
Pin the image
Section titled “Pin the image”image should carry a version tag. An emulator on :latest cannot be behind,
which sounds fine until a local suite that passed yesterday fails today and
nothing in the repo records what moved.
Declaring upstream.repo puts the pin in the weekly freshness check
(scripts/check-emulator-freshness.ts), which compares each pin against the
latest GitHub release and opens a single advisory notice. It never bumps
anything: per the chant #808 policy a pin moves when a consuming test needs the
newer emulator, not because a release happened.
See also
Section titled “See also”chant emulator— the command this drives- Local Testing — the per-cloud loops