Expand description
cratefield-testing: the conformance kit every Factory Zero module runs
against (issue #9) — fake ports, an in-memory SQLite Database, and
request helpers over the axum router with no network.
use cratefield_testing::{conformance, TestHarness, request};
#[test]
fn my_module_conforms() {
conformance(Box::new(my_module::MyModule::new()));
}
#[pollster::test]
async fn join_returns_202() {
let kit = TestHarness::new(vec![Box::new(my_module::MyModule::new())]);
let response = request(&kit.router, http::Method::POST,
"/v1/my-module/join", Some(r#"{"email":"nick@example.com"}"#)).await;
assert_eq!(response.status, http::StatusCode::ACCEPTED);
}§cratefield-testing
The conformance kit every Cratefield module runs against — public and
private modules alike. Fake ports, an in-memory SQLite Database, and
request helpers over the axum router with no network.
§A 15-line module test
ⓘ
use cratefield_testing::{conformance, request, TestHarness};
#[test]
fn my_module_conforms() {
conformance(Box::new(my_module::MyModule::new()));
}
#[pollster::test]
async fn join_accepts_an_email() {
let kit = TestHarness::new(vec![Box::new(my_module::MyModule::new())]);
let res = request(&kit.router, http::Method::POST, "/v1/my-module/join",
Some(r#"{"email":"nick@example.com"}"#)).await;
assert_eq!(res.status, http::StatusCode::ACCEPTED);
assert_eq!(kit.mailer.sent().len(), 1); // the confirmation mail
}§What you get
TestHarness::new(vec![Box<dyn Module>])— builds the harness with every port faked, applies each module’s sqlite migrations to a fresh in-memory database, assembles the router. Exposes{ router, mailer, captcha, rate_limiter, db, clock, kv, http, defer, signer, events, modules, dialect }.- Parity (issue #20):
TestHarness::with_database(modules, Dialect::Sqlite | Dialect::Postgres { url })runs the same harness on a throwaway Postgres 16 database (per-harness, dropped on drop; needs the crate’spostgresfeature and a server atFZ_TEST_POSTGRES_URL).TestHarness::all_dialects(make_modules)/all_dialects_with_ports(make_modules, patch)build one kit per dialect available in the environment so a module suite loops over them — one test definition, every engine. The module factory runs once per dialect: modules may carry per-build state, so kits never share instances. - Fakes:
FakeMailer(recordsMessages;SendOk/NotConfigured/Failmodes, switchable mid-test),FakeCaptcha(allow-all or token list),FakeRateLimiter(scriptedDecisions + call count),FixedClock,MemoryKeyValue,FakeHttpClient(scripted responses + captured requests),EmptyDatabase(servicesSELECT 1only),FakeDefer(collects deferred futures;drain().awaitruns them), and aSignerwith the fixed dummyTEST_HARNESS_SECRET. request(&router, method, path, json?) -> TestResponse { status, headers, json() }—tower::ServiceExt::oneshot, no network.conformance(module)— the shared suite, run once per available dialect: mounts + health listing, request under the prefix, migrations apply twice on fresh databases,view_forhides undeclared ports, the two-concurrent-requests request-id test (ADR 0007), and — for modules with awell_knownrouter — that it serves at the root/.well-knownand never under/v1(#46).full_fake_ports()— aPortsbundle with everyPortwired to a fake: the one conformance’s visibility check uses.assert_wasm_safe_deps(env!("CARGO_PKG_NAME"))—cargo treecheck: noworker/wasm-bindgen/tokio/reqwestin the module’s normal dependency tree.
The in-memory Database is cratefield-adapter-sqlite; assertions on
kit.db see exactly what the module wrote. On the Postgres leg kit.db
is a pool on the kit’s own tokio runtime marshalled per call, so
pollster-driven tests, spawned threads and deferred handlers all reach
it.
MIT. Built in the open for Cratefield, a Factory Zero venture.
Modules§
- vectors
- The Web Push test vectors the workspace shares.
Structs§
- Ask
- One
Classifier::askaFakeClassifieranswered, for assertions: the state as it arrived (untruncated — this fake has no provider budget to spend) and the question ids in the set, in map order. - Commented
Call - One
Tracker::commentaFakeTrackeraccepted, for assertions. - Empty
Database - Fake
Auth - A
cratefield_core::Authanswering from a mode rather than from a key set. - Fake
Captcha - Fake
Classifier - An in-memory
Classifierfor module tests: records every ask it answered and answers according to itsClassifierMode. - Fake
Custom Hostnames - An in-memory
cratefield_core::CustomHostnamesfor module tests: records every call, walks a claim from pending to live on demand, and scripts the error the next call answers with. - Fake
Defer - Fake
Dispatcher - An in-process
Dispatcherthat answers from an axumRouter, so a module can be exercised through a sidecar mount without a network or a second Worker (ADR 0009). The conformance kit uses it to run the same assertions against both mounts (#64). - Fake
Embedder - A deterministic
cratefield_core::Embedderfor module tests: one fixed-width vector per input text, derived from the text’s own bytes, and a whitespace-word-countinput_tokens. - Fake
Http Client - Fake
Mailer - Fake
Payments - An in-memory
cratefield_core::Paymentsfor module tests: records which calls were made and answers per itsPaymentsMode.verify_webhooktreats a signature header of"invalid"as a tampered event. - Fake
Privacy Provider - An in-memory external provider. Construct with its signing secret, hold
sections per subject (
with_subject), and assert on what it saw (calls,applies,holds). - Fake
Push - An in-memory
cratefield_core::Pushfor module tests: records every(recipient, notification)and answers according to itsPushMode. - Fake
Rate Limiter - Fake
Realtime - An in-memory
cratefield_core::Realtimefor module tests: records every broadcast per room and reports a fixed member list. It exercises the port a module holds (broadcast/members from outside a socket); the socket lifecycle andRoomHandlerare the runtime adapter’s job, covered by the native adapter’s own tests. - Fake
Sidecar - A dispatcher backed by an in-process router.
- Fake
Text Model - An in-memory
TextModelfor module tests: records everyPromptit completed and answers according to itsTextModelMode. - Fake
Tracker - An in-memory
Trackerfor module tests: records every acceptedfile/status/commentcall and answers according to itsTrackerMode. A call the mode answered with an error is not recorded, asFakePushdoes not record a failed send. - Filed
Call - One
Tracker::fileaFakeTrackeraccepted, for assertions. - Fixed
Clock - Manual
Clock - A
Clocka test moves by hand (issue #583), for a fake whose behaviour depends on time —MemoryActorsfires an alarm once the clock reaches it. Every clone shares the one instant, so the host and the test that advances it never diverge. - Memory
Actors - An in-process
Actorshost for module tests (issue #583): one serialized actor per(kind, key), its values in memory, its one alarm driven by aManualClockthe test advances. - Memory
Blob - An in-memory
cratefield_core::Blobstore for module tests.MemoryBlob::newhas no presigned URLs (signed_urlreportsUnsupported, as a directory store does);MemoryBlob::with_presignsigns realSigV4URLs thatMemoryBlob::verify_presignedchecks back. - Memory
KeyValue - Provider
Call - One request a
FakePrivacyProvideraccepted, for assertions. - Statused
Call - One
Tracker::statusaFakeTrackeraccepted, for assertions. - TempDir
- A directory under the system temp directory, removed when this value is dropped — including while a panic unwinds.
- Test
Harness - A harness with every port faked and a migrated database, migrations
applied per module on creation. The same database handle is wired
into the router and exposed for assertions — SQLite in memory by
default (
TestHarness::new), or a throwaway Postgres 16 database for the parity suite (TestHarness::with_database, issue #20). - Test
Response - A fully-buffered test response.
Enums§
- Auth
Mode - What a
FakeAuthanswers. - Classifier
Mode - How a
FakeClassifieranswers, mirroringTextModelModefor the classifier port (issue #456). - Dialect
- The database engine backing a test harness.
- Fault
- A way the hop can be wrong. Used by the kit’s own tests to prove the parity checks fail when the forwarder misbehaves; never by a module.
- Mailer
Mode - How a
FakeMaileranswers. - Payments
Call - What a
FakePaymentsrecorded, for assertions. - Payments
Mode - How a
FakePaymentsresponds. - Push
Mode - How a
FakePushresponds, mirroringMailerModefor the push port. - Text
Model Mode - How a
FakeTextModelanswers, mirroringPushModefor the text model port (issue #429). - Tracker
Mode - How a
FakeTrackeranswers, mirroringMailerModeandPushModefor the tracker port (issue #431).
Constants§
- CONTRACT_
ACTOR_ KIND - The kind
contract_actor_handleranswers for andassert_actor_contractdrives. - FAKE_
EMBEDDER_ DIMENSIONS - The width of every
FakeEmbeddervector, and so of theExactVectorIndexthe kit wires infull_fake_ports— the two must agree. - FAKE_
EMBEDDER_ MODEL - The model name a
FakeEmbedderreports. - TEST_
HARNESS_ SECRET - TEXT_
MODEL_ CONFORMANCE_ CACHED_ INPUT_ TOKENS - The canonical cached-input count: the subset of
TEXT_MODEL_CONFORMANCE_INPUT_TOKENSthe scripted transport reports serving from its vendor’s prompt cache. Must stay within the input count — the suite asserts that, so the script has to satisfy it. - TEXT_
MODEL_ CONFORMANCE_ INPUT_ TOKENS - The canonical input-token count: the total prompt tokens the scripted
transport reports. Every
TextModelcompletion reports usage, so the suite asserts this count exactly — an adapter whose vendor omits a usage block fails here, not silently in a caller’s cost accounting. - TEXT_
MODEL_ CONFORMANCE_ OUTPUT_ TOKENS - The canonical output-token count the scripted transport reports.
- TEXT_
MODEL_ CONFORMANCE_ REPLY - The canonical reply the conformance suite expects, verbatim: short and fixed, so an adapter test can script its transport to answer exactly this in its vendor’s wire shape.
Functions§
- assert_
actor_ contract - Proves
actorskeeps theActorscontract, usingadvanceto move the host’s clock forward by aDurationso a pending alarm fires — an in-process host advances its own clock, an adapter’s test drives whatever its runtime exposes. - assert_
batch_ is_ atomic - Proves
db’sbatch_atomicis all-or-nothing: a committed batch is fully visible, a failing batch is invisible. Runs its own probe table, so it can be called against any adapter with a writable connection. - assert_
blob_ large_ round_ trips - Proves
blobsatisfies the large-object half of theBlobport (issue #586): streamed puts and gets for an object pastMAX_BLOB_BYTES, the declared ceiling enforced mid-stream, multipart uploads that assemble in part order, abort leaving nothing behind, the bad-key rules, and a listing that never surfaces another module’s keys. - assert_
blob_ round_ trips - Proves
dbround-trips bound bytes exactly: aVec<u8>bound asValue::Bytesreads back byte-for-byte identical, and a NULL blob reads back as no bytes. Runs its own probe table, so it can be called against any adapter with a writable connection. - assert_
wasm_ safe_ deps - Asserts the named crate’s normal dependency tree carries none of the
dependencies a wasm module must never see:
worker,wasm-bindgen,tokio,reqwest. Shell-out check (cargo tree --edges normal) so the assertion is made against the graph cargo actually resolves, not a copy of it that could drift. - classifier_
conformance - Asserts the
Classifiertrait contract against a classifier whose transport is already scripted to answerclassifier_conformance_questionsaboutclassifier_conformance_state. The suite makes exactly oneaskcall — script that one request and no other. - classifier_
conformance_ questions - The canonical question set the conformance suite asks: one
Question::Choice, oneQuestion::Score, oneQuestion::Noul, so every shape of the port is exercised once. The ids are fixed —topic,severity,urgent— and an adapter test scripts its transport to answer exactly these before running the suite. - classifier_
conformance_ state - The state the conformance suite asks about: short and fixed, so a scripted transport can match it.
- classifier_
not_ configured - Asserts an adapter with no key, binding or model behind it answers
ClassifierError::NotConfiguredrather than panicking — the unwired port is an error the caller matches, so a module can degrade on it. - classifier_
rejects_ malformed_ questions - Asserts the contract that needs no transport: a malformed question set
— empty, blank id, blank instructions, a one-criterion choice, a
one-level score — comes back as
ClassifierError::Rejectedand never panics. Safe to run against an unconfigured adapter: the set is refused before any provider is reached. - classifier_
truncates_ long_ state - Asserts that asking with a
statelonger thanprofile().max_state_charsstill succeeds — truncation is the adapter’s job, so an over-limit state must never surface as an error to the caller. - conformance
- Runs the shared conformance suite against one module, once per
dialect available in the environment (issue #20 — SQLite always,
Postgres when
FZ_TEST_POSTGRES_URLnames a server): - conformance_
in_ process_ only conformancewithout the sidecar parity axis (issue #64).reasonis recorded in code and printed by the run, so a module that opts out says why in the same place it opts out. Use it only for a module that genuinely cannot be sidecar-mounted; “it fails” is not a reason.- contract_
actor_ handler - The contract handler, for a host that takes one
Arc. - contract_
actor_ handlers - A registry holding just the contract handler under
CONTRACT_ACTOR_KIND, for a host built fromActorHandlers. - error_
json - A Colonizer-shaped error body,
{"error": message}underapplication/json. - full_
fake_ ports - A
Portsbundle with everyPortwired to a fake, for checks that need a full bundle: conformance’s visibility check (declared ports must survivePorts::view_for, undeclared ones must be hidden), and a runtime proving it carries every port through a copy of its bundle.dispatcherandtenantsare not ports and stayNone. - problem_
json - An RFC 9457
application/problem+jsonresponse — the shape Owlpost answers errors with:type,title,statusanddetail, thestatusboth the HTTP status and the body’s own field. Build one for ascriptedsequence, or take it from a preset likeFakeHttpClient::owlpost_problem. - push_
recipient_ conformance - Runs a
Pushadapter against everyRecipientvariant and asserts the port’s contract for transports it does not serve (issue #177): a cleanPushError::Rejectednaming the recipient as unsupported — never a panic, never a silent success, and never aTransientthat an outbox would retry forever. - request
- Sends a request through the router without a network.
json(whenSome) becomes a JSON body withcontent-type: application/json. - request_
as - The same, carrying
Authorization: Bearer <token>. - request_
chunks - Sends a request whose body is the given chunks, as a chunked
(no
content-length) transfer — what a client uploading a stream produces, and the only shape that exercises a route’scratefield_core::RequestStreammid-stream ceiling rather than the declared-length pre-check.content-typeis set toapplication/octet-stream. - shared_
sidecar - Wraps a
FakeSidecarso the same handle can be given toPortsand still be asked how many calls it saw. - sidecar_
parity - Asserts that a module answers identically in-process and behind a sidecar mount (issue #64, ADR 0009: a caller cannot tell which).
- sign_
provider_ scheme - The headers a configured
ProviderSchemereads forbodysigned att: the signature header carrying one entry — the scheme’sprefixthen the hex or base64 ofHMAC-SHA256(secret, payload)— and, when the scheme names a timestamp header, that header set tot. The payload is"{t}.{body}"when the scheme carries a timestamp and the rawbodyotherwise; a scheme with no timestamp header ignorest. - sign_
stripe_ style - The delivery headers Stripe sends for
bodysigned att: one header namedheader, holdingt=<t>,v1=<hex>, the hex beingHMAC-SHA256(secret, "{t}.{body}"). The exact inverse ofStripeStyle’s extraction — feed the result straight toWebhookVerifier::verifywithnow_unix == t. - text_
model_ conformance - Asserts the
TextModeltrait contract against a text model whose transport is already scripted to completetext_model_conformance_promptwithTEXT_MODEL_CONFORMANCE_REPLYand the canonical counts in its vendor’s wire shape. The suite makes exactly onecompletecall — script that one request and no other. - text_
model_ conformance_ not_ configured - Asserts an adapter with no key behind it answers
TextModelError::NotConfiguredrather than panicking — the unwired port is an error the caller matches, so a module can degrade on it. - text_
model_ conformance_ prompt - The canonical prompt the suite asks: one user turn, no system prompt,
no schema, the default token ceiling. An adapter test scripts its
transport to answer
TEXT_MODEL_CONFORMANCE_REPLYwith the canonical counts (TEXT_MODEL_CONFORMANCE_INPUT_TOKENS,TEXT_MODEL_CONFORMANCE_OUTPUT_TOKENS,TEXT_MODEL_CONFORMANCE_CACHED_INPUT_TOKENS) for this prompt. - text_
model_ image_ bounds_ conformance - Asserts a
TextModeladapter refuses an over-limit image prompt (issue #628) before any network call: too many images, one image overMAX_IMAGE_ENCODED_BYTES, or images together overMAX_PROMPT_IMAGE_ENCODED_BYTES, each refused with the matchingTextModelError::ImageLimit. - vector_
index_ conformance - Asserts the
VectorIndexcontract against an adapter, over small three-dimensional vectors (issue #561): - with_
retry_ after - Adds
Retry-After: <retry_after_secs>to a response in seconds — the header a 429 carries and the adapters read as a retry hint.