Vending
Vending is how packages share test generators with each other. Instead of duplicating arbitraries in every package that needs them, declare them once and the registry auto-discovers them.
The Package.json Key
Each package declares where its test generators live:
{ "name": "@my/auth", "version": "1.0.0", "corpus": { "testing": "./dist/testing/register.js" }}The "corpus": { "testing": ... } path points to a module — the registrar. The auto-loader imports it and calls its exported register() function, which populates the registry via testing.arbitrary() and testing.failure().
Writing a Registrar
A registrar is import-safe: nothing registers at module load. All registration happens inside an exported register() function, so downstream code can import your brand symbols for identity without touching the registry.
import { arbitrary, failure, compose, fc } from "@f0rbit/corpus/testing";import type { ArbBrand } from "@f0rbit/corpus/testing";import type { AuthToken, AuthError } from "../types.js";
// Brands for your domain types — export them so downstream tests can look them upexport const AUTH_TOKEN_BRAND = Symbol("AuthToken") as ArbBrand<AuthToken>;export const AUTH_ERROR_BRAND = Symbol("AuthError") as ArbBrand<AuthError>;
// Called by the auto-loader (or explicitly via testing.load_from)export function register(): void { arbitrary( AUTH_TOKEN_BRAND, compose((draw) => ({ id: draw(fc.uuid()), secret: draw(fc.stringMatching(/^[a-f0-9]{32}$/)), })) );
failure( AUTH_ERROR_BRAND, "invalid_credential", compose((draw) => ({ kind: "invalid_credential" as const, reason: draw(fc.string()), })) );}Then compile to JavaScript:
bun run build # produces dist/testing/register.jsAnd your package.json knows where to find it:
{ "corpus": { "testing": "./dist/testing/register.js" }}Auto-Loading
The first time a test calls testing.lookup() or testing.lookup_failure(), the registry auto-discovers and loads all registrars from your dependencies’ package.json files:
import { testing } from "@f0rbit/corpus";
const result = await testing.lookup(USER_ID_BRAND);// ↓ First call triggers auto-load// ↓ Discovers "corpus" keys in all direct dependencies' package.json// ↓ Imports each registrar module// ↓ Calls each registrar's exported register()// ↓ Returns the registered arbitraryThis is automatic — no manual wiring required. Every test that calls lookup() or lookup_failure() for the first time triggers the walk.
Explicit Loading with load_from
When auto-discovery isn’t enough (testing a fixture package, monorepo sibling, or offline scenario), use load_from() for explicit control:
import { testing } from "@f0rbit/corpus";
// Load from a directoryconst result = await testing.load_from("./fixtures/my-package");if (!result.ok) { console.error("Failed to load:", result.error.message);}
// Or by npm package name (resolves via import.meta.resolve)const result = await testing.load_from("@my/auth");Explicit loads are additive — they don’t stop auto-discovery. A later lookup() will still walk the full dependency graph as usual.
Registrar Scope
The auto-loader discovers:
- Direct dependencies — packages in your
package.jsondependencies,devDependencies, andpeerDependencies - The current package — your own
"corpus": { "testing": ... }entry, if present
It does NOT discover transitive dependencies (your dependencies’ dependencies). Instead, each package’s registrar can register on behalf of its own deps, so arbitraries propagate upward naturally.
In a monorepo with hoisting (npm v7+, pnpm, bun), all packages resolve via the same node_modules, so transitives are discovered anyway. The restriction is purely for non-hoisted setups.
Testing: Reset and Reload
In test setup, you may want explicit control over which registrars are loaded. Use testing.__reset_registry_for_tests() to clear the cache and registries:
import { testing } from "@f0rbit/corpus";
beforeEach(async () => { // Clear the registry and auto-load promise testing.__reset_registry_for_tests();
// Now the next lookup() will walk fresh const arb = await testing.lookup(MY_BRAND);});Then use load_from() to load a specific registrar:
beforeEach(async () => { testing.__reset_registry_for_tests();
// Load only the fixture registrar, not the full dependency graph const result = await testing.load_from("./fixtures/test-package"); if (!result.ok) { throw new Error(`Failed to load fixture: ${result.error.message}`); }});Dev Checkout Safety
The walker loads exactly the path the "corpus": { "testing": ... } key declares — there’s no automatic src/ vs dist/ switching. Two deliberate carve-outs keep dev checkouts sane:
-
Missing self-registrar file → silently skipped. Manifests usually point at build output (
./dist/...). In a dev checkout of your own package that hasn’t been built, the declared file doesn’t exist — the walker skips it rather than warning on every test run. Register from source explicitly in your own suite (import yourregister.tsdirectly, or usetesting.load_from). A dependency with a missing registrar file is NOT skipped — that’s a packaging bug, and it surfaces as a load failure. -
The corpus checkout itself is always skipped. When the cwd package IS
@f0rbit/corpus, loading./dist/testing/register.jswould double-register through a parallel module graph (dist has its own registry instance). Corpus’s own suite importstesting/register.tsfrom source instead.
Troubleshooting
Registrar fails to load
If a registrar throws an error:
[corpus/testing] auto-load: registrar for '@my/auth' failed to load: [error message]Fix that package's "corpus": { "testing": ... } entry, or load a corrected registrar via testing.load_from(...).Check:
- The path exists — Does the file at
"corpus": { "testing": ... }exist? - It exports a
register()function — the walker calls it when present; a module without one is assumed to register via import side-effects (supported, but not the convention) - It doesn’t throw on import — module-level code runs before
register()is called - No circular imports — The registrar shouldn’t import the test suite
As a workaround, use testing.__reset_registry_for_tests() + testing.load_from() to load a corrected version manually.
Registrar not discovered
If a registrar never loads:
- Check it’s a direct dependency — Auto-load only walks direct deps, not transitives
- Check the
package.jsonkey — It must be"corpus": { "testing": ... }, not"test_corpus"or similar - Use
load_from()explicitly — If the package isn’t a direct dep, load it by path
Performance: Auto-load is slow
The first lookup() call walks the dependency graph and imports each registrar. On a large monorepo, this can take a second or two. Options:
- Move to devDependencies — Only test packages need registrars; trim unnecessary deps
- Pre-load in your test setup — Call
await testing.lookup()once in a top-level test hook - Use
load_from()selectively — Load only the registrars you need for a specific test
Best Practices
- One registrar per package — All arbitraries for a package go in one
register.tsfile - Keep imports side-effect-free — Do all registration inside the exported
register(); the module itself should be safe to import for its brand symbols alone - Export the brands — Let downstream packages import your
AUTH_TOKEN_BRANDand use it withlookup() - Avoid transitive dependencies — If your registrar imports your domain code, don’t import test utilities that aren’t in the published package
- Test the registrar — Add a smoke test that calls
testing.load_from()and verifies a known brand is registered
Example: Complete Registrar
Here’s a full example from a hypothetical @my/auth package:
import { arbitrary, failure, compose, fc } from "@f0rbit/corpus/testing";import type { ArbBrand } from "@f0rbit/corpus/testing";import type { AuthToken, Session, AuthError } from "../types.js";
export const AUTH_TOKEN_BRAND = Symbol("AuthToken") as ArbBrand<AuthToken>;export const SESSION_BRAND = Symbol("Session") as ArbBrand<Session>;export const AUTH_ERROR_BRAND = Symbol("AuthError") as ArbBrand<AuthError>;
// Arbitraries defined at module level are fine — REGISTRATION is what// must wait for register(). Reuse token_arb inside session_arb directly// (lookup() is async and can't be drawn inside compose).const token_arb = compose((draw): AuthToken => ({ id: draw(fc.uuid()), secret: draw(fc.stringMatching(/^[a-f0-9]{32}$/)), expires_at: draw(fc.date({ min: new Date("2020-01-01"), max: new Date("2030-01-01") })),}));
const session_arb = compose((draw): Session => ({ token: draw(token_arb), user_id: draw(fc.uuid()), created_at: draw(fc.date({ min: new Date("2020-01-01"), max: new Date("2030-01-01") })),}));
export function register(): void { arbitrary(AUTH_TOKEN_BRAND, token_arb); arbitrary(SESSION_BRAND, session_arb);
// One failure generator per error variant your package's error union carries failure( AUTH_ERROR_BRAND, "invalid_credential", compose((draw) => ({ kind: "invalid_credential" as const, field: draw(fc.constantFrom("email", "password")), reason: draw(fc.string()), })) );}Then in package.json:
{ "name": "@my/auth", "version": "1.0.0", "corpus": { "testing": "./dist/testing/register.js" }}Build with bun run build (or your build tool), and downstream packages automatically get these arbitraries on the first lookup() call.