Provider overrides and snapshot testing conventions.
MOA_PROVIDERS_OVERRIDE lets a dev or CI orchestrator replace normal LLM
providers at process startup. Build the orchestrator with
--features provider-overrides before setting this variable; default production
builds do not compile the scripted provider.
Supported values:
- unset: use providers configured from normal API keys;
scripted:<path>: load deterministic responses from a JSON fixture;mock:<seed>: use a built-in deterministic mock response.
The override is blocked when the orchestrator detects a production environment
(prod or production via MOA_OBSERVABILITY_ENVIRONMENT).
The fixture supports a fallback response and an optional queue of one-shot responses:
{
"default": {
"completion": {
"content": "OK",
"duration_ms": 1,
"input_tokens": 64,
"cached_input_tokens": 0,
"cache_write_input_tokens": 0,
"tool_calls": []
}
},
"responses": [
{
"completion": {
"content": "first response",
"tool_calls": []
}
}
]
}responses are consumed in order. After they are exhausted, default is used
for every later request. tool_calls entries have name, input, and optional
id fields.
The load-test smoke fixture is checked in at
crates/moa-loadtest/scripts/perf-gate.json.
Use cargo-nextest for the default local suite. It schedules individual tests
across test binaries. The default developer target uses the fast-pr nextest
profile, which excludes tests that need Postgres, Docker, Restate, OpenFGA, the
PII sidecar, cloud auth, or live/billed providers.
cargo install cargo-nextest --locked
make test-fastFor the inner loop, make test-affected narrows the run further: it maps the
change set (versus the merge base with main, plus uncommitted files) to
workspace crates via cargo metadata, expands to reverse dependents, and runs
only those crates' tests. Workspace-level files such as Cargo.lock fall back
to the full fast-pr lane.
cargo-nextest does not run doctests. make test-fast intentionally skips
them (the workspace currently has no runnable doc examples and the rustdoc
pass costs ~90s); make test-ci keeps the doc-test pass as the safety net.
The CI-equivalent local target also keeps running after failures and writes
nextest's JUnit report under target/nextest/ci/:
make test-ciThe suite is split by runtime requirements rather than by crate. Keep the fast lane free of hidden service dependencies; move mixed test files into clearer suffixes as they are touched.
| Lane | Command | Runtime requirements |
|---|---|---|
| Fast PR | make test-fast |
none beyond local mock servers and tempdirs |
| DB session | make test-db-session |
Postgres only; schema isolation |
| DB memory | make test-db-memory |
Postgres with relational graph/vector state; per-test template-cloned databases, runs 4-wide |
| Authz pentest | make test-authz-pentest |
Postgres with graph/vector state; writes the pentest report |
| Service E2E | make test-service-e2e |
clean Postgres/OpenFGA/Restate/PII harness with deterministic providers |
| Behavior Lab | make test-behavior-lab |
service E2E harness; candidate-evaluation surface, unbilled |
| Behavior Lab live | make test-behavior-lab-live |
Behavior Lab harness plus billed provider credentials and an approved budget |
| Provider E2E | make test-provider-e2e |
service E2E harness plus live/billed provider credentials |
Pull requests also run the bounded restate-recovery-pr nextest profile. Tests
selected by that profile use the recovery_matrix_ prefix, run one at a time,
and own a dedicated fixture whose orchestrator is killed with SIGKILL and
reaped while Restate, Postgres, Valkey, OpenFGA, provider JSONL, and fixture
capability state remain alive. Recovery assertions must stop at exact provider,
capability, or database barriers; elapsed sleeps are never crash gates.
The 500-task and chaos matrices remain in their nightly/heavy profiles rather than expanding the pull-request lane. Destructive Restate member and PVC replacement remains an operator-run procedure, not repository test automation.
Coordination tests assert persisted transitions and Restate acknowledgements, not elapsed quiet periods:
worker_coordination_service_e2eproves that successful children produce no early parent resume and oneFanInSettledresume when the current child-registration generation settles, failed children produce one failure resume, explicit result waiters resolve once, and restart/replay preserves the same counts.- The worker liveness case proves one outstanding deadline, heartbeat-driven deadline movement, no stale signal while terminal or awaiting input, one joined stale signal after the exact deadline, and no periodic Session calls while a healthy child remains active.
- Long-horizon execution cases prove that every controller activation stays within its scheduler-step and dispatch-batch bounds, every task or compensation activation executes one generation, and restart after partial completion is exactly once.
- Public execution-mutation cases stop after the Postgres transition but before immediate dispatch. Recovery must redeliver the same outbox/trigger identity, and replay must neither advance the generation nor apply the transition twice.
- Input/review/signal/timer/external/pause cases assert that parked rows have no active attempt reservation, Restate handler invocation, or hand. Resume must provision fresh compute and restore the verified checkpoint when filesystem state is required.
Use the restate-recovery-pr profile for the conversational restart matrix and
the long-horizon-execution profile for accelerated timer, burst, deployment
drain, and Postgres/outbox reconstruction scenarios. The latter runs one thread,
uses deterministic time/provider fixtures, and must fail when its selector
matches no tests. Twenty-four-hour and seven-day canaries remain ignored behind
MOA_RUN_LONG_HORIZON_CANARY=1; provider-live execution additionally requires
its own opt-in and positive cost budget.
Both use deterministic local Postgres, Restate, OpenFGA, Valkey, MCP, and
fixture-capability dependencies; neither requires a billed provider.
The nextest profiles in .config/nextest.toml are mostly suffix-based. Keep
new out-of-line test targets on one of these suffixes so the filters stay short:
*_unit.rs, *_offline.rs, *_component.rs, *_db.rs, *_db_memory.rs,
*_service_e2e.rs, *_provider_e2e.rs, *_eval.rs, *_live.rs, and
*_docker.rs. When a file starts mixing runtime requirements, split it into
lane-specific binaries before adding profile selectors.
Offline, _db, and _db_memory behavior files are consolidated into one
harness binary per crate per lane (for example
crates/moa-orchestrator/tests/orchestrator_db.rs declaring
#[path = "orchestrator_db/session_store_db.rs"] mod session_store_db;).
Each file under tests/ otherwise links as its own binary, and the binary
count dominates link and nextest-listing time. When adding a behavior file to
one of these lanes, place it under the harness directory and add a mod line
to the harness; run it with
cargo test -p <crate> --test <harness> <module_name>. Binaries that nextest
profiles, scripts, or workflows reference by name (the _service_e2e,
_provider_e2e, _live, _eval lanes and pinned names like
cross_tenant_pentest_db_memory) stay standalone. For example, memory
eval corpus and metric tests belong in _offline or _eval targets, graph
gold-resolution and tenant knowledge graph/vector tests belong in
_db_memory, local hand-tool filesystem tests belong in _offline,
session-search tests belong in _db, Docker hardening belongs in _docker,
and Restate service/provider E2E coverage should use surface-specific
*_service_e2e.rs or *_provider_e2e.rs binaries. If an existing mixed
integration-test binary cannot be split immediately, give the resource-backed
test function the same lane suffix so nextest can keep it out of fast-pr.
The consolidation is concrete: the seven former moa-session DB roots now
run through the single session_db harness. Knowledge and eval behavior files
use the same pattern under their lane directories. Behavior modules belong
under a lane root and are declared by that lane's harness; adding another root
binary for the same runtime requirement is a regression unless a pinned script
or profile requires the standalone target.
If a crate-private inline unit test needs a slow resource and cannot move to an
integration test without exposing internals, put the lane marker in the test
function name, for example *_db_*. Keep these exceptions rare; file suffixes
are the preferred boundary.
DB-backed lineage and auth recovery checks are explicit integration lanes. Run them directly when touching those surfaces:
cargo test -p moa-auth-providers --features auth0 --test auth_providers_db auth0_ciba_db --locked -- --test-threads=1
cargo test -p moa-authz --test authz_db authz_poller_db --locked -- --test-threads=1
cargo test -p moa-lineage-audit --test merkle_publisher_db --locked -- --test-threads=1These commands require MOA_DATABASE_URL or the local compose Postgres default.
When the database is absent, the lane fails with a Postgres reachability error
instead of silently skipping inside a library unit test.
Before changing build profiles, linker settings, or crate structure for compile speed, capture a Cargo timings report:
make build-timingsThe report is written under target/cargo-timings/.
Run the boundary scanner after touching Restate handlers, workflows, runtime
dependency wiring, domain repository seams, workspace crate dependencies,
moa-core top-level re-exports, or central hotspot files such as
crates/moa-edge/src/routes.rs,
crates/moa-config/src/env_overlay/mod.rs, and
crates/moa-orchestrator/src/workflows/turn_execution/mod.rs:
cargo run -p xtask -- check-architecture-boundariesThe check fails on new direct SQL in
crates/moa-orchestrator/src/services/** or
crates/moa-orchestrator/src/workflows/**, and on attempts by orchestrator
objects, services, or workflows to recover runtime dependencies from a global
or static accessor. Runtime dependencies must originate in
moa_orchestrator::runtime::deps::RuntimeDeps and travel through constructors
or handler structs. If a handler truly needs a temporary direct-SQL exception,
record it in the scanner allowlist with a concrete reason and exact expected
count. The scanner fails when a counted source allowance becomes unused, so
deleting debt also requires deleting its allowance; forbidden dependency
directions and global dependency access have no allowance list. Prefer moving
SQL to a repository or domain crate instead of expanding the allowlist.
The same command also reports and enforces architecture budgets from Cargo
metadata and current source files: workspace package/default-member counts,
moa-core direct and transitive reverse dependencies, configured LOC budgets,
reduced-module LOC ratchets, forbidden dependency directions from
docs/15-architecture-policy.md, and the
exact semantic moa-core root allowlist (MoaError, Result, and
WORKSPACE_ID) with wildcard rejection. Dependency rules distinguish normal,
build, and dev edges so a test-only allowance cannot silently become a
production dependency. If one of these numbers grows
intentionally, update the scanner budget in the same change with the measured
count and the reason for accepting the growth.
Use the clean runner for certification instead of the persistent compose
Restate and moa database. It creates a temporary Postgres database, starts an
ephemeral restate-server with random ports, bootstraps OpenFGA into a temp env
file, and cleans those resources up on exit.
make e2e-cleanIgnored/live Restate E2E requires an explicit opt-in:
MOA_RUN_LIVE_E2E=1 make e2e-clean-liveThe live Restate lane keeps provider-backed cases out of the default --live
path. It uses moa-orchestrator/provider-overrides with
MOA_PROVIDERS_OVERRIDE=mock:<run-id> for deterministic lifecycle and
skill-learning smoke tests (skill learning is always compiled in). The clean
runner also executes the focused skill_learning orchestrator tests before the
ignored live profiles, and it unsets provider API-key environment variables
around provider-override smoke tests. Billed provider coverage remains in the separate
provider lane below.
Optional provider and long-eval lanes remain explicit because they can be billed or slow:
MOA_RUN_LIVE_E2E=1 MOA_RUN_LIVE_PROVIDER_TESTS=1 make test-provider-e2e
MOA_RUN_LIVE_E2E=1 ./scripts/run-clean-e2e.sh --live --long-evalThe Behavior Lab is the contained candidate-evaluation surface: an
ExperimentRun/ExperimentTrialRun executes a release candidate against a
scripted environment and produces the scorecard that release gating consumes. Its
binaries are scheduled by two named profiles, both invoked from
scripts/run-clean-e2e.sh --live (so make test-behavior-lab is an alias for
make e2e-clean-live). They are unbilled.
| Profile | Binaries | Restate stack |
|---|---|---|
behavior-lab-service-e2e |
experiment_trial_run_e2e, skill_learning_gate_e2e |
external: reads MOA_RESTATE_INGRESS_URL/RESTATE_ADMIN_URL, spawns its own orchestrator on reserved ports; the Admin URL belongs to test infrastructure, not the runtime process |
behavior-lab-fixture-service-e2e |
artifact_release_service_e2e |
self-contained: OrchestratorTestFixture starts its own containers and fails if MOA_RESTATE_INGRESS_URL is set |
That split is why there are two profiles rather than one: the two halves need
opposite environments, so the runner invokes the first with the ephemeral
server's URLs exported and the second through
run_without_external_orchestrator. Both run test-threads = 1 and take the
service-e2e group slot; every case holds RESTATE_E2E_LOCK or a dedicated
fixture, and they share one Postgres database, OpenFGA store, and Valkey.
skill_learning_gate_e2e needs Postgres and provider-overrides but no Restate.
It sits in this lane because it evaluates a release candidate through the real
eval engine before a skill can be accepted, which is the same gating surface.
The billed trial-to-score smoke is excluded from behavior-lab-service-e2e and
selected only by the behavior-lab-live profile, which requires authorization
twice over plus an approved budget:
MOA_RUN_LIVE_E2E=1 MOA_RUN_LIVE_PROVIDER_TESTS=1 MOA_BEHAVIOR_LAB_BUDGET_USD=5 \
make test-behavior-lab-liveThe runner refuses that flag before creating any container or database when the
authorization flags are missing, when MOA_BEHAVIOR_LAB_BUDGET_USD is absent or
not a positive USD amount with at most six decimal places, or when no provider
credential is present. The test itself stays #[ignore]d, re-checks its own
flags, and binds that approved amount to the run and trial's exact micro-USD
resource ceiling.
Postmark messaging e2e coverage is ignored by default and reads local .env
values directly. Use POSTMARK_SERVER_API_TOKEN=POSTMARK_API_TEST for
non-delivery validation, or provide POSTMARK_TEST_FROM and POSTMARK_TEST_TO
with a real server token:
MOA_RUN_LIVE_POSTMARK_TESTS=1 \
cargo test -p moa-messaging --test postmark_provider_e2e --all-features -- --ignored --nocaptureThe Postmark offline suite covers payload shape, provider status errors,
bounded HTTP 429 retries, exhausted rate-limit failures, and nonzero
ErrorCode classification. Contact OTP delivery additionally requires
MOA_MESSAGING_EMAIL_FROM to contain a verified sender address. Live Postmark
coverage should remain a single happy-path acceptance check because reproducing
account, suppression, or rate limit failures against the real service is
brittle.
Twilio SMS e2e coverage is also ignored by default and reads local .env
values directly. It requires TWILIO_ACCOUNT_SID, either TWILIO_AUTH_TOKEN
or TWILIO_API_KEY_SID plus TWILIO_API_KEY_SECRET, and either
TWILIO_FROM_NUMBER or TWILIO_MESSAGING_SERVICE_SID. Set
TWILIO_TEST_TO to the recipient number for a live send; the test skips when
that value is absent:
MOA_RUN_LIVE_TWILIO_TESTS=1 \
cargo test -p moa-messaging --test twilio_provider_e2e --all-features -- --ignored --nocaptureThe Twilio live test polls the accepted Message SID until the message reaches
sent, delivered, or a terminal failure state. Terminal failures include the
Twilio status and error code in the assertion so delivery regressions do not
look like successful provider acceptance.
Slack messaging tests stay offline by default. Unit and integration tests cover Events API normalization, approval controls, edit fallbacks, per-channel send pacing, exhausted rate limits, and Slack API error classification; live Slack coverage should use a separate ignored provider lane once a test workspace and channel are configured.
Remote loadtest checks are also ignored by default. The step-latency check requires a running orchestrator with Prometheus metrics enabled:
MOA_RUN_LOADTEST_REMOTE_SMOKE=1 \
MOA_RESTATE_INGRESS_URL=http://localhost:10010 \
MOA_LOADTEST_METRICS_ENDPOINT=http://localhost:9090/metrics \
cargo test -p moa-loadtest --test mock_loadtest_service_e2e mock_short_profile_reports_runtime_step_latency -- --ignoredThe remote smoke test expects the orchestrator, Restate ingress, and metrics endpoint to already be running. It does not bring up or tear down the compose stack for you.
Use snapshots when exact rendered output is the contract:
- provider wire-format request bodies;
- byte-stable prompt prefixes;
- rendered UI strings;
- public error message formatting.
Do not snapshot internal struct layouts, incidental debug output, or data that is easier to assert with targeted semantic checks.
Keep fixture inputs fixed, small, and named with constants. Route through the
same formatter or provider request builder used in production, then snapshot
with a stable name in the form <area>__<scenario>.
const SYSTEM_PROMPT: &str = "You are MOA.";
#[test]
fn provider_request_body__minimal_request_serializes_with_stable_byte_layout() {
let body = build_provider_request_body(SYSTEM_PROMPT)
.expect("request body should build");
insta::assert_json_snapshot!("provider_request_body__minimal_request", body, {
".metadata.request_id" => "[redacted]",
".timestamp" => "[redacted]"
});
}Use redactions only for genuinely nondeterministic fields such as IDs, timestamps, or provider-generated cache names. If a test can avoid nondeterminism with fixed inputs, prefer that over redacting.
Run the failing test locally, then inspect pending updates with:
cargo insta reviewA good diff has an intentional code change next to an expected wire-format or rendering change. A suspicious diff is only key reordering, whitespace churn, a changed cache marker location, or an unexpected new ID/timestamp; fix the determinism issue instead of accepting that snapshot.