testing/cross_backend
10 modules
testing/cross_backend/backend_config.ts
Cross-process backend configuration.
BackendConfig describes a spawnable test binary — argv, mount paths, env vars, bootstrap credentials, daemon-token discovery path, declared capabilities. Consumer projects ship per-backend factories (
deno_backend_config(),rust_backend_config(),spine_stub_backend_config()) that produce this shape; spawn_backend consumes it.fuz_app ships
spine_stub_backend_config()as a convenience preset (operational dep ontesting_spine_stub— path-based discovery, nopackage.jsoncoupling to private_fuz). Otherwise backend-specific knowledge (binary paths, port choices, env vars) is a consumer concern; fuz_app's testing library knows nothing about Deno, Cargo, or any specific runtime beyond that preset.testing/cross_backend/bootstrap_backend.ts
One-call spawn + bootstrap helper.
Composes
spawn_backend(config)andbootstrap({transport, config})so a consumer's vitestglobalSetupreduces to a single await:import {bootstrap_backend} from '@fuzdev/fuz_app/testing/cross_backend/bootstrap_backend.js'; export default async function ({provide}) { const bootstrapped = await bootstrap_backend(deno_backend_config()); provide('backend_handle', bootstrapped); return async () => { await bootstrapped.teardown(); }; }If
bootstrap()throws — typically a bad token, port collision, or keeper-username mismatch — the spawned binary is torn down before the error propagates so vitest doesn't strand the port.testing/cross_backend/build_test_backend_paths.ts
Per-backend filesystem layout under
os.tmpdir()for cross-process tests.Isolation matters because vitest projects can run in parallel — a shared
rootwould mix daemon tokens across concurrently-running backends. Each backend gets its own subtree via theprefixarg (typically theBackendConfig.name).Consumers compose: take the generic paths from
build_test_backend_paths(name), add domain-specific dirs (e.g.zzz_dir,scoped_dir) under the returnedroot.testing/cross_backend/capabilities.ts
Capability vocabulary for cross-backend integration testing.
Backends declare which optional behaviors they support; suite bodies call
test_if(capabilities.X, ...)to skip cases the backend doesn't implement. Noif (config.name === 'rust')branches anywhere — name- checking is a code smell that says capability vocabulary is missing.In-process Hono via default_in_process_setup declares every capability
true(see in_process_capabilities). Cross-process backends opt in per-flag on their BackendConfig.testing/cross_backend/default_backend_configs.ts
Family-shared BackendConfig builders for cross-process test backends.
Two consumer-facing factories — {@link make_default_ts_backend_config} and {@link make_default_rust_backend_config} — own the common shape for the JS-runtime (Deno/Node on V8) and Rust families respectively. Per-backend factories in consumer projects compose a small declaration against one of these and add consumer-specific env vars via
extra_env.Defaults baked in by family:
- TS —
'memory://'PGlite, 30s startup window, capabilities without trusted_proxy/login_rate_limit. The TS canonical path leaves these limiters null in test mode. - Rust — caller-supplied real Postgres URL (PGlite isn't reachable fromtokio-postgres), 120s startup window (cargo first-build cost), capabilities including trusted_proxy + login_rate_limit + theFUZ_TESTING_RESET_DB_ON_STARTUP=trueself-wipe gate.Both builders default
port_env_varto'PORT'. Consumers whose binary reads a different name (e.g. zzz'sZZZ_PORT) override.Common across both families:
/api/rpc,/api/ws,/health,/api/account/bootstrap,cookie_name: 'fuz_session', the standard bootstrap block keyed offdefault_test_*constants. Builders callbuild_test_backend_paths(name)internally when the optionalpathsis omitted.testing/cross_backend/default_secrets.ts
Default test secrets for cross-process backend bootstrap.
Every cross-backend consumer needs the same shape: a bootstrap token the harness writes to disk before spawn, a keeper username/password the harness POSTs to
/api/account/bootstrap, and cookie keys the binary uses to construct its Keyring. The literals here are dev-only and protected byassert_dev_env; the binaries themselves throw on production load.Each constant is exported individually so consumers can override one without re-deriving the rest. Builders in
default_backend_configs.tsthread these defaults into theBackendConfig.bootstrapblock and theSECRET_FUZ_COOKIE_KEYSenv entry; callers compose thebootstrap_overridesknob when they need a non-default keeper.testing/cross_backend/setup.ts
Per-test fixture protocol shared by in-process and cross-process transports.
Each standard suite body takes a required
setup_test: () => Promise<TestFixture>callback and invokes it once per test. The fixture carries everything a test needs to fire requests and assert on a single bootstrapped keeper account — transport, account / actor identity, three header builders, a multi-account mint factory, and (in-process only) the in-memory keyring + raw backend.default_in_process_setup(options)wraps create_test_app into the SetupTest contract. The cross-process sibling (default_cross_process_setup) lands alongside the spawn-a-backend transport plumbing — it implements the same contract by spawning a binary and bootstrapping over real HTTP.testing/cross_backend/spawn_backend.ts
Spawn a test backend binary, wait for it to come up, and return a handle the test harness drives.
Lifecycle:
1. Write the bootstrap token (
config.bootstrap.token) toconfig.bootstrap.token_pathso the binary picks it up at startup. 2.child_process.spawn(...)the binary withdetached: true— creates a new process group so aSIGTERMto the negative PID tears down any descendants the binary spawned (PTYs, child workers). vitest worker death + Ctrl+C handlers also fire the group teardown so ports never strand. 3. Poll{base_url}{health_path}until it returns 2xx orstartup_timeout_mselapses. 4. Readconfig.bootstrap.daemon_token_pathto load the binary's deterministic daemon token; thread it onto BackendHandle so_testing_resetand other keeper-credential calls can authenticate.Bootstrapping (
POST /api/account/bootstrap) is a separate concern — the caller composesbootstrap()from../transports/bootstrap.tsagainst a FetchTransport built aroundhandle.config.base_url. Splitting the two keeps spawn_backend consumer-agnostic — fuz_app knows nothing about specific binary contents.testing/cross_backend/standard.ts
Cross-process counterpart to describe_standard_tests.
Wires the cross-process-safe subset of the standard bundle — the five suites whose option shape is
{setup_test, surface_source, capabilities, ...}and whose bodies fire requests throughfixture.transportrather than touching the in-processBackend. Consumers wire one call against a spawned binary instead of repeating the five sibling calls per file.Suites included — always run:
- describe_standard_integration_tests - describe_round_trip_validation - describe_rpc_round_trip_tests - describe_data_exposure_tests
Gated on
roles— included when the consumer supplies a RoleSchemaResult:- describe_standard_admin_integration_tests
Suites omitted — the three that don't survive a process boundary, documented here so per-consumer files don't have to repeat the bookkeeping:
- describe_rate_limiting_tests — builds a fresh TestApp per test to inject tight per-test rate-limiter overrides. That path requires in-process construction of
Backend+ rate limiter; the spawned binary has neither knob nor restart-per-test budget. - describe_audit_completeness_tests — reaches into FK-structural introspection that only the in-process backend exposes. Wire-level audit observability lives in the consumer's own audit.cross.test.tsdrivingaudit_log_list/audit_log_role_grant_history. - describe_bootstrap_success_tests — bootstrap is one-shot per backend lifecycle, and the consumer'sglobalSetupalready consumed it before the suite file loads. Re-running would 409.testing/cross_backend/testing_reset_actions.ts
Test-binary RPC actions for cross-process integration tests.
Single daemon-token-authed action:
_testing_reset— full DB wipe + keeper re-seed + optional secondary-account seeding. The handler wipes every auth-namespace row (no keeper-preserve filter), flipsbootstrap_lockback to its post-bootstrap shape, seeds a fresh keeper account inline (reusing create_test_account_with_credentials so cross-process matches in-process write semantics), seeds any caller-requestedextra_accounts(also direct-inserted at this setup step), refreshes the daemon-token cache to point at the new keeper, and fires the consumer-supplied domain-state callback. The new keeper + secondary credentials return as the action output so the per-test fixture closes over them.The redesign converges in-process and cross-process keeper lifetimes: both modes now run against a freshly bootstrapped keeper per test. Mutation-cascade tests (password change, revoke-all, hardcoded-username signup uniqueness) and direct keeper-vs-admin probes work uniformly cross-process.
Keeper ≠admin. The
keeperandadminroles are independent. Keeper authorizes daemon-token / bootstrap paths; admin authorizes the user-facing admin RPC surface._testing_resetseeds the keeper account with[ROLE_KEEPER, ROLE_ADMIN]by default — matching the production bootstrap_account flow — plus any roles passed viaextra_keeper_roles. Tests probing the keeper-vs-admin separation (a keeper-only account must 403 on admin RPCs) declare a secondary viaextra_accounts: [{username, roles: [ROLE_KEEPER]}]so the account is seeded at this same bootstrap-equivalent step.No free-form runtime bypass. Earlier drafts considered a separate
_testing_seed_role_grantaction for arbitrary direct grants; that was rejected because a runtime bypass would let tests skip the production consent flow's side-effects (audit emit, WS fan-out) and silently mask bugs in those paths. The bypass that does exist —extra_accounts— is framed as bootstrap-time seeding, the same shape bootstrap_account itself uses to grant the initialKEEPER+ADMINpair. Tests that want a role on a *post-bootstrap* account must route throughrole_grant_offer_create+role_grant_offer_accept(the production path); they observe the full event chain.Production safety: this module lives under
cross_backend/and starts withimport '../assert_dev_env.js';— production bundles either tree-shake the module out or throw at startup. The Rust mirror (fuz_testingcrate) ships a parallel action; `cargo xtask check-releaseblocksfuz_testing` from entering production dep graphs.