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 - 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§
- 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_
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_ 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.- 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. - 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.