testing/cross_backend
59 modules
testing/cross_backend/account_lifecycle.ts
Cross-backend parity suite for the account-lifecycle admin verbs:
account_delete(soft),account_undelete(reactivation), andaccount_purge(keeper hard-delete), plus the keeper guard.Like the cell suites, these verbs can't ride the generic describe_rpc_round_trip_tests: they're stateful and destructive (a generic round-trip would tombstone the bootstrapped keeper). They live-mount on every spine's RPC path but stay off the declared surface, so this dedicated suite is their cross-impl validator. Every success
resultis parsed against the verb's declared Zod output schema, so a TS↔Rust envelope drift fails the assertion.$lib-free by contract (relative specifiers only) so it can be imported from the spawnable cross-process test files.testing/cross_backend/action_manifest_parity.ts
Cross-impl action-manifest parity — structural diff + assertion over two ActionManifests captured via the
_testing_action_manifestRPC action.The action-surface twin of
schema_parity.ts: two live impls (the TS fuz_app spine and the Rusttesting_spine_stub) are each other's parity reference. After both bootstrap, dump each one's live RPC method set, diff, fail loudly on drift. The diff entries name the specific divergence (a method only one impl mounts, a per-method auth-axis or side-effect mismatch) so the error message points at the source.const manifest_a = await capture_action_manifest(ts_handle); const manifest_b = await capture_action_manifest(rust_handle); assert_action_manifests_equal(manifest_a, manifest_b, {a: 'ts', b: 'rust'});Non-coverage: the manifest captures `{method, side_effects, account, actor, roles, credential_types}` — the wire-relevant auth shape. It does not capture input/output schemas (the declared-surface wire shape is gated by
rpc_round_trip) nor the protocol actionsheartbeat/cancel(excluded by construction — seeaction_manifest.ts).testing/cross_backend/action_manifest.ts
Cross-impl RPC action-manifest introspection — a normalized, JSON-serializable dump of a backend's live RPC method set, one entry per method carrying its auth shape + side-effect flag.
The sibling of
schema_introspect.ts's SchemaSnapshot: where that captures the live *database* shape for the schema-parity gate, this captures the live *RPC registry* shape for the action-manifest parity gate. Both are dumped over a daemon-token_testing_*introspection action (_testing_action_manifesthere,_testing_schema_snapshotthere) and diffed across the TS spine and the Rusttesting_spine_stubso a method-set or per-method auth-shape divergence fails loud. This complements the in-repospine_method_coveragegate: that proves *mounted ⟹ covered*; this proves *TS-mount-set ≡ Rust-mount-set* (method set + auth shape).Scope — domain + testing surface, not wire protocol. Protocol actions (
heartbeat/cancel/peer/ping) are excluded: on the TS spine create_testing_action_manifest_action filters them via the protocol_action_specs method set; the Rust stub dropsPROTOCOL_ACTION_SPECS. The two impls organize protocol actions differently (peer/pingis on the TS spine's WS and HTTP-RPC endpoints — it must answerpeer_no_transportover HTTP — while heartbeat/cancel stay WS-only; the Rust stub compiles one shared registry serving both transports), so including them would be a spurious cross-impl diff. This matches the scope of the in-repospine_method_coveragegate (also over build_full_spine_rpc_actions).Paired with
action_manifest_parity.tsfor the diff + assertion helpers.$lib-free by contract — reached by the spawned TS binary (viatesting_reset_actions.ts→full_spine_mount.ts), so every import is relative.testing/cross_backend/actor_lookup.ts
Cross-backend parity suite for
actor_lookup.actor_lookupis an opt-in batched id → label resolver ({ids} → {actors: [{id, username, display_name?}]}), not folded into the standard bundle. It's live-mounted on the spine RPC path but kept off the declared surface (create_spine_surface_spec) — like cells / ws / sse — so the standard cross suite's generic round-trip never drives it; this dedicated suite is its validator. Three cases over raw transport calls:- anonymous → 401 — the account-grain auth gate refuses an unauthenticated caller before the handler runs.
- keeper resolves own actor → 200 — the populated round trip: the
returned row carries the keeper's
id+username, and noaccount_id/email/ timestamp / role field (the wire shape's deliberate info-leak posture). This is the assertion that exercises the Rust row→JSON mapping against the TS canonical shape. - empty
ids→ 400 — themin(1)input bound is enforced on both spines (TS Zod, Rustparse_ids).
Runs both legs via the shared
{setup_test}protocol: the in-process leg (auth/actor_lookup_parity.db.test.ts, plaingro test) and the cross-process leg (cross_backend/actor_lookup.cross.test.ts, the TS spine binaries + Rusttesting_spine_stubover real HTTP).actor_lookupis mounted on every spine, so the suite is ungated.$lib-free by contract (relative specifiers only), like the sibling cross-backend suites.testing/cross_backend/actor_search.ts
Cross-backend parity suite for
actor_search.actor_searchis an opt-in case-insensitive prefix search overactor.name(`{query, scope_ids?, limit?} → {actors: [{id, username, display_name?}]}`), not folded into the standard bundle. Likeactor_lookup/ cells, it's live-mounted on the spine RPC path but kept off the declared surface, so this dedicated suite is its validator. The security property under test is the empty-scope_idsadmin gate:- anonymous → 401 — the account-grain auth gate refuses an unauthenticated caller before the handler runs.
- non-admin + no
scope_ids→ 400actor_search_scope_required— an unbounded global search is admin-only; a non-admin must scope the query. This is the core security assertion, exercised against each impl's real auth resolution. - non-admin +
scope_ids→ 200 — passing a scope bypasses the admin requirement (results are filtered to actors holding active role_grants on those scopes); an unheld scope simply yields an empty result, not a rejection — proving the gate keys onscope_idspresence, not identity. - admin + no
scope_ids→ 200 — the admin path reaches the unbounded search.
Cites
security.md§Authorization (theactor_searchscope gate).Runs both legs via the shared
{setup_test}protocol: in-process (auth/actor_search_parity.db.test.ts) + cross-process (cross_backend/actor_search.cross.test.ts, TS spine binaries + Rusttesting_spine_stub). Mounted on every spine, so the suite is ungated.$lib-free by contract (relative specifiers only).testing/cross_backend/app_settings.ts
Cross-backend effect suite for the
open_signupapp setting.The declarative conformance table pins the admin gate on
app_settings_get/app_settings_update(401 / 403 / 200). This suite pins the behavioral effect of the toggle end to end: an admin flipsopen_signupviaapp_settings_update, and a subsequent anonymousPOST /signupobserves the new value.- toggle on → anonymous signup without an invite succeeds (200) — with
open_signup: true, the invite gate is skipped. - toggle off → anonymous signup is refused (403
no_matching_invite) — flipping it back restores the invite requirement, proving the gate keys on the live value rather than a one-time read.
The signup handler reads the toggle fresh from the database on every request, so the admin's write is visible to the next signup. This suite runs in a single process, so it validates the read-through *mechanism* — not multi-process consistency (which the fresh-read shape provides by construction but no single-binary test can observe).
Cites
security.md§Signup. Runs both legs via the shared{setup_test}protocol: in-process (auth/app_settings_parity.db.test.ts) + cross-process (cross_backend/app_settings.cross.test.ts, TS spine binaries + Rusttesting_spine_stub). Mounted on every spine, so the suite is ungated.$lib-free by contract (relative specifiers only).- toggle on → anonymous signup without an invite succeeds (200) — with
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(),rust_spine_stub_backend_config()) that produce this shape; spawn_backend consumes it.fuz_app ships
rust_spine_stub_backend_config()as a convenience preset (operational dep ontesting_spine_stub— path-based discovery, nopackage.jsoncoupling to the stub's source package). 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/bench/bench_report.ts
Reporting adapters over a CrossImplBenchResult — all built on fuz_util's formatters + Welch comparison. Markdown for human eyeballs, a per-scenario TS-vs-reference significance verdict, and a self-describing JSON artifact.
testing/cross_backend/bench/run_cross_impl_bench.ts
Drive identical wire scenarios across several spawned backends and time each round trip, so a TS impl and a Rust impl can be compared apples-to-apples (both cross-process over real HTTP). The reusable cross-impl measurement primitive.
fuz_util's benchmark library is the engine —
Benchmarkruns each scenario as a task andBenchmarkResult.statscarries the percentiles; this module is the thin scenario→task→tagged-result adapter. Reporting (markdown, TS-vs-Rust verdict, JSON artifact) lives in testing/cross_backend/bench/bench_report.ts.testing/cross_backend/bench/scenario.ts
Context handed to a
BenchScenario.run. Carries a ready, pre-authed transport (the bootstrapped keeper's, by default) plus the resolved RPC path and the backend's declared capabilities. A scenario fires one round trip (or a small fixed *idempotent* sequence) against it — no per-call_testing_reset, which is the correctness-test model and would dominate the timing.testing/cross_backend/body_size_smuggling.ts
Cross-backend request-smuggling probe for the body-size limit's connection handling — the security sibling of
body_size.ts.When the server caps the request body it answers
413on theContent-Lengthheader. The strong (defense-in-depth) posture is to close the connection *without reading the oversized body*: HTTP/1.1 forbids reusing a keep-alive connection whose request body wasn't consumed, because unread body bytes would be parsed as the start of the next request — a classic request-smuggling vector. This suite probes the boundary by pipelining: it opens a raw TCP socket and sends, in one write, an oversizedPOSTimmediately followed by a secondGET. The assertion forks on the backend's declaredoversized_reject_closes_connectioncapability:- Closes (Node / Deno / hyper) — the reject closes the socket with the
GET bytes unconsumed, so at most one response comes back.
<= 1rather than "exactly the 413" because the impls close differently at the TCP level (node-server graceful close delivers the 413 first; hyper's RST can drop the in-flight 413 before the client reads it), so demanding a cleanly-read 413 would be flaky. - Drains + keepalives (Bun) —
Bun.servereads the full declaredContent-Lengthbody and answers the *correctly-framed* pipelined GET, so two responses come back. This is not a smuggle: the GET is delimited by the body'sContent-Length, not the unread body reinterpreted as a request — Bun answers it with a clean400(missing method), not thexbody bytes reparsed. The security property asserted here is no desync (<= 2): a real desync would reframe the 1 MiB ofxinto bogus request lines and push the count past two.
Either way the oversized body is rejected *with* a 413 — pinned reliably over
fetchby describe_body_size_cross_tests; this test owns only the connection-handling half. A positive control (two pipelined requests →>= 2responses) proves a second response *would* be seen if a trailing request were processed — without it the close-posture<= 1would be vacuous on a server that never reuses connections — and that the counter isn't undercounting.Raw-socket by necessity (the FetchTransport can't pipeline two requests on one connection), so — unlike
body_size.ts— this is cross-process only (no in-process leg; there is no socket in-process). The connection-close half is capability-gated; the no-desync half holds on every spine.Cited property:
docs/security.md§"Body Size Limiting" (connection handling on oversized reject).$lib-free by contract (relative +node:specifiers only).- Closes (Node / Deno / hyper) — the reject closes the socket with the
GET bytes unconsumed, so at most one response comes back.
testing/cross_backend/body_size.ts
Cross-backend parity suite for the request body-size limit.
create_app_server (TS) and the Rust spine both cap the request body at a 1 MiB default (DEFAULT_MAX_BODY_SIZE /
fuz_http'sDEFAULT_BODY_LIMIT_BYTES) and reject oversized payloads with413and the canonical flat REST body{error: 'payload_too_large'}— *before* auth, origin, or dispatch run (middleware step 4). Each impl unit-tests this in isolation, but nothing fires an oversized POST over the wire, so the cross-impl agreement on the status + body shape (and on the exact>cap boundary, not an off-by-one divergence) was unpinned. Three cases:- over-limit POST (cap + 1 byte) → 413
payload_too_large, refused before any handler runs (the limit fires ahead of origin verification + the dispatcher, so an over-cap body is rejected regardless of how well-formed it is). Exactly one byte over — both impls reject on a strict>, so this is the tight upper boundary, and staying just over keeps it clear of any larger framework-default limit that would answer with a different body. - at-limit POST (exactly the cap) → not 413 — one byte under the rejection threshold passes the size gate and reaches the dispatcher (the downstream status is irrelevant; only "not size-rejected" is asserted). The boundary sibling of the case above.
- under-limit POST (small) → 200 — a small, well-formed authenticated
account_verifyenvelope sails through to a successful handler response, the positive control that the route works for normal traffic.
Real-socket connection hazard (cross-process only). When the server caps the body it answers 413 and closes the connection *before* the client finishes uploading — correct HTTP, since an unread request body can't share a keep-alive socket. The client's pool can then hand that now-dead socket to the very next request (observed as
other side closed). So every request here goes throughfetch_retrying_once: a request that inherits the poisoned socket retries onto a fresh connection, which both keeps the suite deterministic *and* evicts the dead socket so it can't strand a later cross suite in the same process. In-process (app.request) has no socket, so the hazard is cross-process-only and the retry never fires there.Like origin/payload rejection, this is middleware-level flat REST — not the JSON-RPC envelope the conformance-table runner expects — so it's an imperative suite, not a
conformance_tablerow. Runs both legs via the shared{setup_test, capabilities}protocol: the in-process leg (auth/body_size_parity.db.test.ts, plaingro test) and the cross-process leg (cross_backend/body_size.cross.test.ts, the TS spine binaries + Rusttesting_spine_stubover real HTTP). The body-size limit is on every spine, so the suite is ungated.Cited property:
docs/security.md§"Body Size Limiting".$lib-free by contract (relative specifiers only), like the sibling cross-backend suites.- over-limit POST (cap + 1 byte) → 413
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.ts'; 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.Where the per-backend declarations live (this file owns only the vocabulary + the in-process preset):
- in_process_capabilities — here; every flag
true. - ts_default_capabilities / rust_default_capabilities — consumer-facing
family defaults, in
default_backend_configs.ts(full literals, so adding a capability is a compile error until each family declares it). ts_spine_capabilities/ts_spine_bun_capabilities— fuz_app's own TS spine presets, ints_spine_backend_config.ts(deltas off the family default; Bun flipsoversized_reject_closes_connection).rust_spine_stub_capabilities— fuz_app's Rust spine-stub preset, inrust_spine_stub_backend_config.ts(delta off the rust family default).
Gating flags vs shape notes. BackendCapabilities holds only flags a suite actually gates on (each has a
test_if(capabilities.X, ...)reader). Wiring facts that gate nothing —bearer_auth/trusted_proxy/login_rate_limit— live in the parallel BackendShapeNotes record (in_process_shape_notes here, ts_default_shape_notes / rust_default_shape_notes indefault_backend_configs.ts) so the capability type never claims gating power it doesn't have.- in_process_capabilities — here; every flag
testing/cross_backend/cell_cross_helpers.ts
Shared call-site primitives for the cell cross-backend parity suites (testing/cross_backend/cell_crud.ts + testing/cross_backend/cell_relations.ts).
The cell verbs are stateful and authz-shaped, so both suites POST raw JSON-RPC envelopes (threading ids + auth headers across calls) and parse every success
resultagainst the verb's declared Zod output schema — the wire-shape parity gate. A TS↔Rust envelope drift, not just a payload field drift, fails the assertion.$lib-free by contract (relative specifiers only) so the suites can be imported from the spawnable cross-process test files.testing/cross_backend/cell_crud.ts
Dedicated stateful cell-CRUD parity suite for the cross-backend harness.
The generic describe_rpc_round_trip_tests can't cover cells: the verbs are stateful (update / delete / get-by-id need a real cell id threaded from a prior create) and
cell_get's input has a top-level.refine(). So cells stay off the standard declared surface (create_spine_surface_spec) — exactly like ws / sse — and this suite plus its sibling describe_cell_relations_cross_tests (grant / field / item / clone / audit) are the cell validators. This one gates oncapabilities.cell_crud; it runs against any backend that live-mounts the cell surface (the TS spine binary, the in-process Hono app, and the Rusttesting_spine_stub).Drives the full lifecycle (create → get → update → delete → list, threading the created id) plus the authz matrix the wire contract guarantees. Every success response is parsed against the verb's declared Zod output schema (CellCreateOutput / CellGetOutput / …), so a TS↔Rust envelope drift — not just a CellJson field drift — fails the suite:
- owner does full CRUD; responses match the output schemas exactly;
- anon sees
publiccells only —privateis 404 (existence not leaked); - a non-owner non-admin editing / reading / deleting another's private cell gets 404 (IDOR mask), never 403;
- admin reaches any cell;
- duplicate active
path→ 409 (cell_path_taken); pathwrite by a non-admin → 403 (cell_path_admin_only), on both create and update (even by the owner);cell_getwith neitheridnorpath→invalid_params;- null-auth
cell_listwithcreated_by→invalid_params; - a denied caller (non-editor update / non-admin path create) carrying a
kindinsidedatastill gets the authz error (404 / 403), never the kind error — authz runs before the input-shape checks, on both spines; - an empty-string
kindis rejected on create (cell_kind_empty) and is accepted as a list filter that matches nothing.
The visibility-manage-tier 403 (
cell_visibility_manage_only) needs a non-owner editor, which only acell_grantcan produce, so it lives in describe_cell_relations_cross_tests alongside the grant verbs rather than here.$lib-free by contract (relative specifiers only) so the suite can be imported from the spawnable cross-process test files.testing/cross_backend/cell_gated_create.ts
Cross-backend parity suite for the parent-aware cell-creation authorizer (CellCreateAuthorize) — the directory model.
The authorizer adds no method, column, or wire shape, so the schema-snapshot and action-manifest parity gates are blind to a TS↔Rust authorizer divergence (the authorizer in the wrong phase, a different deny shape, the 404/403 split, the moderation outcome). This behavioral cross case is the only gate that catches one — proven *here in fuz_app*, against both reference spines.
Both spines mount the same directory-model policy (the TS spine binary via
create_test_cell_gated_create_authorize, the Rusttesting_spine_stubviaTestCellGatedCreateAuthorize): admin bypasses; a non-admin creating akind: 'space'root is denied (admin-only); a contribution under a space is gated by the space'sdata.policy[kind] = {min_role, moderation_required}, with the moderation outcome folded into the verdict; plain parentless creates stay open. The suite asserts all spines agree on:- root-create admin-only — a non-admin
spaceroot → 403cell_create_forbidden; admin → succeeds. - contribution gated by the root's policy — under a public space, an
unauthorized stranger → 403; a
participant→ succeeds. - moderation per
moderation_required— aparticipant'spost(moderation_required: true) is bornmoderation: 'pending'+ private; areact(moderation_required: false) is bornmoderation: 'approved'at the author's visibility. - 404 on a hidden parent vs 403 on a visible one — a contribution under a
private space the caller can't view → 404
cell_not_found(the parent is masked); under a public space → 403 (you see it, you can't contribute).
Gated on
capabilities.cell_gated_create—trueonly on the reference spine binaries that mount the policy, so it skips for generic consumers and the in-process default app (the authorizer hook's in-process coverage is the standaloneauth/cell_create_authorize.db.test.ts).$lib-free by contract (relative specifiers only).- root-create admin-only — a non-admin
testing/cross_backend/cell_grant_role.ts
Cross-backend parity suite for role-shaped
cell_grants.The cell CRUD / relations suites exercise only actor-shaped grant principals. This suite covers the role-shaped path and its closed-registry gate — the security-correctness property that the Rust spine previously lacked (it created inert grant rows for any role string). Both spines now validate the role against a closed registry at create.
- role grant admits a holder; excludes a non-holder — an owner grants
{role}on a private cell; an account holding that role cancell_getit (200), an account without it gets the IDOR-mask 404. - unknown role rejected at create (security-correctness) — granting a
role outside the registry is
invalid_params/cell_grant_unknown_role, not a silent inert row. - editor-level role grant admits edit — a holder of an
editor-level role grant cancell_updatethe cell's content.
The holder is seeded via
extra_accountsunder CELL_ROLE_HOLDER_USERNAME holding CELL_EDITOR_ROLE (the role has no grant path, so it can't be offered — the bootstrap-cradle seed is the only path). Both legs configure that seed and register CELL_EDITOR_ROLE in their role registry; the Rusttesting_spine_stubmirrors the same membership in itsknown_roles.Cites
security.md§Authorization (role-shaped cell-grant validation). Runs both legs via the shared{setup_test}protocol: in-process (auth/cell_grant_role_parity.db.test.ts) + cross-process (cross_backend/cell_grant_role.cross.test.ts). Gated oncapabilities.cell_relations(true on every spine, so it never skips).$lib-free by contract (relative specifiers only).- role grant admits a holder; excludes a non-holder — an owner grants
testing/cross_backend/cell_relations.ts
Dedicated cell relation / ACL / audit parity suite for the cross-backend harness — the sibling of describe_cell_crud_cross_tests covering every cell verb beyond plain CRUD:
cell_grant_*,cell_field_*,cell_item_*,cell_clone, andcell_audit_list.Like the CRUD suite (and ws / sse), these verbs are live-mounted on the spine RPC path but stay off the standard declared surface, so the generic describe_rpc_round_trip_tests never drives them. Every success response is parsed against the verb's declared Zod output schema, so a TS↔Rust envelope drift fails the suite — not just a payload-field drift.
Coverage (gated on
capabilities.cell_relations):- grant lifecycle — owner grants an actor-shaped editor; the grantee
gains edit (was 404 before the grant);
cell_grant_listis manage-tier (owner sees it, the editor gets the IDOR 404); revoke drops the edit path. cell_visibility_manage_only— the editor-grant holder can edit content but flippingvisibilityis 403 (the case the 5-verb CRUD cut couldn't reach without a grant principal).- fields —
cell_field_setUPSERT, forward + reversecell_field_list, idempotentcell_field_delete. - items —
cell_item_insertat fractional-index positions, forward (lex-ordered) + reversecell_item_list,cell_item_move, idempotentcell_item_delete. - clone — shallow copies item / field *edges* (shared
child_id/target_id); deep clones each viewable child into a fresh cell at the same position. Both nullpathand stamp the caller as owner. - audit —
cell_audit_listis manage-tier: the owner reads the cell's timeline; a viewer-grant holder who cancell_getthe cell still gets the IDOR 404 on the timeline (D14). - relation-read visibility (D8) — listing edges toward a cell the caller
can't view filters them out (no-existence-leak-via-edge): an anonymous
viewer of a public parent — and a viewer-grant holder of a private parent —
sees only independently-viewable children in the
cell_getbundle and the forwardcell_item_list/cell_field_list. The cross twin of the in-processauth/cell_relation_visibility.db.test.ts. - clone D8 — a cloner who can view a public parent but not a private child
silently drops that child: an admin (who *can* view it) reading the clone
still sees only the viewable edge/child, and the
cell_cloneaudit row records no skipped-child count — so the source's hidden-child count can't leak to the cloner. The cross twin ofauth/cell_actions.clone.db.test.ts.
Only actor-shaped grants are exercised — role-shaped principals need a closed role registry the Rust spine deliberately lacks, so role-grant parity is out of scope here (the TS impl covers it in-process).
$lib-free by contract (relative specifiers only) so the suite can be imported from the spawnable cross-process test files.- grant lifecycle — owner grants an actor-shaped editor; the grantee
gains edit (was 404 before the grant);
testing/cross_backend/conformance_case.ts
Declarative conformance-case schema for the cross-backend behavioral + security suite.
A conformance case is a single request → expected-response assertion, carried as data. The case references a
method(an RPC method name or a REST auth-route suffix); the runner (describe_conformance_table_tests) resolves theinput/outputZod schemas from the live action-spec registry / RouteSpec — the case never carries a schema. This is the opinionated behavioral/security layer on top of the spec-derived auto-enumeration (describe_rpc_round_trip_tests / describe_rpc_attack_surface_tests): the same case definition runs in-process (fast, everygro test) and cross-process (the conformance gate) against each impl's real auth resolution.The table is for single-request matrices (credential-type ceiling, privilege gates, IDOR masks, enumeration-equivalence, validation). Multi-step flows stay imperative in their own
describe_*suites, sharing assertion primitives — there is deliberately no declarative setup DSL.testing/cross_backend/conformance_table.ts
Table runner for the declarative cross-backend conformance suite.
describe_conformance_table_tests takes a list of ConformanceCase rows plus the standard
{setup_test, surface_source, capabilities}fixture protocol every Tier 1 suite uses — so one runner drives both transports: in-process via default_in_process_setup (fast, everygro test) and cross-process via default_cross_process_setup (the conformance gate, exercising each impl's real auth resolution over real HTTP). Same case definition, transport-parameterized.Each row references a
method; the runner resolves itsinput/outputschema from the live spec registry (RPC) or RouteSpec (the 6 REST auth routes) — the row never carries a schema. The principal the row runsasresolves to a TestFixture accessor viaresolve_principal— no inline credential minting.Every response also passes an always-on no-fingerprint invariant (assert_no_fingerprint_headers over FINGERPRINT_HEADERS —
Server/X-Powered-By/WWW-Authenticatemust stay absent on both spines), and a row may pin further header expectations viaexpect.headers. Headers are deliberately kept out of the equivalence-group{status, body}comparison.testing/cross_backend/cookie_attributes.ts
Cross-backend parity suite for session cookie attributes over real HTTP.
The session cookie's
Set-Cookieattributes are the load-bearing browser security boundary:HttpOnlykeeps the token out of JS (XSS can't read it),Securekeeps it off plaintext HTTP,SameSite=Strictis the primary CSRF defense (the Origin allowlist is only defense-in-depth), andPath=/scopes it to the whole app. A regression dropping any one of these is a real downgrade — and it's wire-observable, so both spines must emit the same hardened set. The cross-backend conformance table deliberately keepsSet-Cookie*out* of its byte-identity comparison (the signed value legitimately differs per request and per impl), so the attribute contract had no cross-backend pin: the TS spine's attributes were covered bysession_cookie.ts's own tests, but the Rust spine'ssign_session_cookie/ clear_session_cookie strings were asserted nowhere. This suite closes that gap on both impls by parsing the rawSet-Cookieand asserting the attributes directly. Three properties:- successful login sets a hardened cookie — `HttpOnly; Secure;
SameSite=Strict; Path=/
plus a positive integerMax-Age` (the session lifetime). The signed value is opaque here; only the attributes are pinned. - a failed login sets no session cookie — a denial mints no credential,
so there is no
Set-Cookiefor the session name at all. A spine that leaked a (signed-but-unauthenticated) cookie on the 401 would fail here. - logout clears the cookie with
Max-Age=0and the same hardened flags — the clear must not silently dropSecure/HttpOnly/SameSite=Strict(a cleared-but-unhardenedSet-Cookieis a downgrade window).
Both surfaces are flat REST (
POST /api/account/{login,logout}) on every spine, so this is an imperative suite (not aconformance_tablerow) — the sibling oforigin.ts/login_security.ts. Cross-process only: reading the rawSet-Cookieattributes needs the wire response (the in-process parse_session /session_cookie.rsunit tests cover each impl's cookie codec directly). Cited property:docs/security.md§"Session Security" (the "Cookie attributes" bullet —HttpOnly; Secure; SameSite=Strict; Path=/).$lib-free by contract (relative specifiers only), like the sibling cross-backend suites.- successful login sets a hardened cookie — `HttpOnly; Secure;
SameSite=Strict; Path=/
testing/cross_backend/create_cross_backend_global_setup.ts
Generic vitest
globalSetupfactory for cross-backend integration suites.Pairs with make_cross_backend_project: each cross-backend vitest project sets its own
test.name, and this factory derives the backend name from that project name (vitest 4 passes theTestProjectto globalSetup), picks the matching BackendConfig, spawns + bootstraps it via bootstrap_backend, andprovides a serializable handle that*.cross.test.tsfilesinjectand rebuild with reconstruct_bootstrapped_handle.A consumer's
global_setup.tscollapses to:import {create_cross_backend_global_setup} from '@fuzdev/fuz_app/testing/cross_backend/create_cross_backend_global_setup.ts'; import {deno_backend_config, rust_backend_config} from './my_backend_config.js'; import './cross_test_types.js'; // augments inject('backend_handle') export default create_cross_backend_global_setup({ configs: {deno: deno_backend_config, rust: rust_backend_config}, });vitest 4's
providehard-rejects non-serializable values, so the livechild/teardown/keeper_transportare stripped via serialize_bootstrapped_handle; the teardown closure stays in the globalSetup process and is returned for vitest to fire after the suite.testing/cross_backend/create_dual_spawn_global_setup.ts
Generic dual-spawn vitest
globalSetupfactory — spawns + bootstraps *two* backends at once andprovides both serialized handles, for any cross-impl gate that needs both alive together. (The per-backend create_cross_backend_global_setup derives one backend from the project name; this brings up a pair.)Its primary use is the schema- / action-manifest-parity gates (capture each backend over a
_testing_*introspection RPC and diff with assert_schema_snapshots_equal / assert_action_manifests_equal) — which is why the defaultprovide_keysareparity_handle_*. The login-security gate (global_setup_login_security.ts) reuses it with its own keys; any future two-backend gate can too.A consumer's dual-spawn
global_setup.tscollapses to:import {create_dual_spawn_global_setup} from '@fuzdev/fuz_app/testing/cross_backend/create_dual_spawn_global_setup.ts'; import {deno_backend_config, rust_backend_config} from './my_backend_config.js'; import './cross_test_types.js'; // augments the two provide keys export default create_dual_spawn_global_setup({ configs: {a: deno_backend_config, b: rust_backend_config}, });The
.cross.test.tsinjects both keys, rebuilds each with reconstruct_bootstrapped_handle, and asserts. Run this project in a latergroupOrderthan the single-backend projects (or with distinct ports) — it reuses both configs' ports, so it must not run concurrently with the per-backend projects.testing/cross_backend/credential_header_robustness.ts
Cross-backend credential-header robustness probe — the auth sibling of
body_size_smuggling.ts.The auth middleware reads a single credential per header (
Authorization,X-Daemon-Token). What a *duplicated*, *oversized*, or *control-char-injected* credential header does is framework-territory — Hono reads its headers via the WebHeadersAPI, axum viahttp::HeaderMap— and the two could resolve a duplicate differently (first-vs-last), cap header size differently, or parse a malformed value differently. None of that is exercised overfetch(the FetchTransport can't emit a duplicate or malformed header), so it had no cross-impl pin. This raw-socket suite sends hand-framed requests and asserts the security invariants that must hold regardless of how each framework resolves the ambiguity — so it pins a real property without blessing one resolution over the other:- a duplicated credential header never escalates — a request carrying a
valid keeper
X-Daemon-Token*and* a second, conflicting one must not perform a keeper operation. The target omits confirm, so the documented confirm guard (purge_not_confirmed) makes the honored-token branch a guaranteed non-2xx with zero side effect, and the discarded-token branch is a non-2xx auth/credential refusal — so whichever copy the framework picks, the outcome is non-2xx. The same shape with two garbageAuthorizationheaders pins the bearer path can't be escalated either. - a control-char-injected credential header can't smuggle or authenticate
— embedded
CRLF/ bareCRin the value never frames a second request (no smuggle) and never authenticates (the truncated / rejected value is no token). - no response desync — each crafted request yields exactly one HTTP response (a duplicate header that reframed the request would surface as a second status line, like the body-size smuggling probe).
- an oversized credential header can't wedge the server — after a request
with a 64 KiB
X-Daemon-Token(well past both frameworks' header caps), a subsequent normal request on a fresh connection still gets a response. The oversized request's own outcome (4xx / 431 / connection close) is don't-care; surviving it is the property.
Raw-socket by necessity (
fetchcan't emit duplicate or malformed headers), so — likebody_size_smuggling.ts— this is cross-process only and fixture-free: it needs only the base URL, the RPC path, and a valid daemon token (handle.daemon_token, kept current for the run by the same rotation the_testing_resetchannel relies on). Cited property:docs/security.md§"Credential Type Hierarchy" (a leaked / conflicting credential header can't escalate the credential ceiling) + §"API Token Security" (bearer handling).$lib-free by contract (relative +node:specifiers only).- a duplicated credential header never escalates — a request carrying a
valid keeper
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. Its shape notes (ts_default_shape_notes) recordtrusted_proxy: false/login_rate_limit: false— the TS canonical path leaves those limiters null in test mode. - Rust — caller-supplied real Postgres URL (PGlite isn't reachable
from
tokio-postgres), 120s startup window (cargo first-build cost), theFUZ_TESTING_RESET_DB_ON_STARTUP=trueself-wipe gate, and shape notes (rust_default_shape_notes) recordingtrusted_proxy: true+login_rate_limit: true.
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.- TS —
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 testing/cross_backend/default_backend_configs.ts thread these defaults into the
BackendConfig.bootstrapblock and theSECRET_FUZ_COOKIE_KEYSenv entry; callers compose thebootstrap_overridesknob when they need a non-default keeper.testing/cross_backend/default_spine_surface.ts
Canonical no-domain spine surface — the standard fuz_app auth/account/admin/audit surface with no consumer domain layer on top.
This is the single source of truth for "the standard spine surface", shared by:
- the Rust
testing_spine_stubcross-process self-tests (which build the AppSurfaceSpec via create_spine_surface_spec and drive the binary's wire shape), - the TS
testing_spine_servercross-process binary (which feeds create_spine_route_specs + spine_rpc_endpoints into a live create_app_server), and - the
cross_backend_ts_*self-test projects.
$lib-free by contract. This module and everything it imports use relative specifiers (no$libSvelteKit alias) so the spawned TS test binary — run under Gro's loader, which resolves.js→.tsand package imports but not the$libalias — can import it transitively. Keep it that way: a$libimport anywhere in this graph breaks the binary spawn while still typechecking under vitest.- the Rust
testing/cross_backend/expected_schema.json
testing/cross_backend/fact_serving.ts
Cross-backend fact-serving parity suite — the per-reference (cell-scoped) read model over real HTTP.
Re-proves the D1 fact-access cases (
docs/security.md§ Fact Access Control) against each backend's real auth resolution, twinning fuz_app's server/serve_fact_route.ts and the Rustfuz_fact_servingrouters:- cell-scoped admit — anon reads a fact through a viewable (public) referencing cell → 200 + bytes;
- cross-owner dedup does not leak — A's *private* reference to bytes that
B *also* publishes from a *public* cell stays 404 for everyone but A, even
though the identical bytes are world-readable via B's cell (one deduped
factrow; authz lives on the(cell, hash)edge, never unioned); - 404-mask — a missing cell and a viewable cell that doesn't reference the hash both 404 (never 403, never "exists elsewhere");
- bare-hash admin-only —
GET /api/facts/:hashis admin (keeper) only: non-admin → 403, anonymous → 401; - multi-actor fallthrough — a multi-actor caller resolves to a null
(anonymous) context on the (
acting-less) cell-scoped route, so it can't read its own *private* fact there (admitted only by public cells). Opt-in (needs the multi-actor setup); every spine resolves the acting actor at the dispatcher's authorization phase from account-grain credentials, so the multi-actor account is drivable on TS and Rust alike.
Facts are seeded embedded via
_testing_put_fact(the cross-process driver has no DB handle); the referencing cell via thecell_createRPC (extract_refslifts theblake3:hash indataintocell.refs). Gated oncapabilities.fact_serving; runs under everycross_backend_*project — the TS spine binary and the Rusttesting_spine_stubboth mount the serve routes + the seeder.$lib-free by contract (relative specifiers only) so it imports from the spawnable cross-process test files.testing/cross_backend/full_spine_mount.ts
The full live RPC mount for fuz_app's own spine test binary — the complete action set
testing_spine_server.tsexposes on a single RPC endpoint, in one place.Where
default_spine_surface.tsdefines the declared surface (create_spine_surface_spec / spine_rpc_endpoints — the create_standard_rpc_actions bundle the spec-derived suites auto-enumerate), this module defines its superset: the standard bundle plus the families the binary live-mounts but keeps off the declared surface —- the
_testing_*daemon-token backdoors (create_testing_actions), - the full cell verb set (CRUD + grant + field + item + audit),
- the opt-in
actor_lookup/actor_searchresolvers, - the
_testing_action_manifestbackdoor, appended last (it dumps the live method set for the cross-impl manifest-parity gate, so it must enumerate every method above it).
Single-sourcing the mount here lets the binary, the in-process parity setup, and the
spine_method_coveragereconciliation test all build the same list — so a method can never be mounted in one place and forgotten in another. The reconciliation test enumerates build_full_spine_rpc_actions with stub deps and asserts the live method set equals the tagged coverage manifest; seesrc/test/cross_backend/spine_method_coverage.ts.$lib-free by contract — likedefault_spine_surface.ts, this module is reached by the spawned TS binary under Gro's loader (which resolves.js→.tsbut not the$libalias), so every import is relative. Keep it that way.- the
testing/cross_backend/identity_parity.ts
Cross-backend identity-primitive parity for fuz_app's own spine over real HTTP — the
primitive_schemastwins (Username, UsernameProvided, Email) and the login/signup input handling that enforces them, pinned facet-by-facet so a TS↔Rust divergence in any one surfaces as a failure: how usernames are canonicalized on the login lookup, the ASCII-only creation invariant that bounds what can ever be stored, and the email format rule applied at signup.UsernameProvided (login/lookup) canonicalizes the submitted username via
.trim().toLowerCase(), and the DB matches case-insensitively (LOWER(username) = LOWER($1)); Username (creation) lowercases at store and restricts to ASCII via^[a-zA-Z][0-9a-zA-Z_-]*[0-9a-zA-Z]$. The Rust spine must produce the same canonical form, the same case-insensitive lookup, and the same ASCII-only rejection — or a username that logs in on TS silently 401s on Rust, or a non-ASCII username storable on one backend reopens the homograph-collision surface. The spec-derived round-trip + conformance suites never vary username casing or charset, so this corner was unpinned.Canonicalization (login lookup):
- case-insensitive login — an account created
Mixed_Caselogs in via an all-uppercase submission (proves the *lookup* folds case, not merely that the stored form was lowercased). - whitespace-trim login — the same shape logs in with surrounding
whitespace (proves
.trim()on the lookup path). - no Unicode case-fold collision (negative) — a Turkish-
İ(U+0130) variant of an existing ASCII username must NOT match. Both JS.toLowerCase()and Ruststr::to_lowercase()mapİ→i+ U+0307 (combining dot above), never plaini, so the cased homograph stays a distinct, non-existent username → 401 on every backend.
Username-or-email login lookup: the login identifier resolves against username or email — TS query_account_by_username_or_email, Rust the converged
query_account_with_password_hashOR-lookup (this was the divergence the suite closed: the Rust spine previously matched username only). An account created with an email logs in via that email, case-insensitively (Email stores the original case; folding rides theLOWER(email) = LOWER($1)lookup), and username login keeps working when an email is present. A non-existent email → 401.Login input validation: malformed login input → 400
invalid_request_bodyon every spine — whitespace-only username (empty after trim), over-long username (> 255), empty password, and an unknown body key (strict object). TS runs the full LoginInput Zod schema; the Rust spine enforces the same shape inaccount_login(the convergence the suite closed: the Rust spine previously let three of these fall through to a lookup-miss 401, and TS let a whitespace-only identifier through to a 401). Asserted via the error reason, not just the status, so a same-status-wrong-body backend still fails.ASCII-only creation invariant: a non-ASCII username is rejected at signup input validation → 400 on every backend, so no Unicode username is ever stored — the precondition the login no-collision case relies on. (ASCII-only is the intended invariant for both spines, not an accident of the TS regex.)
Length + format creation parity: the full Username shape —
[USERNAME_LENGTH_MIN, USERNAME_LENGTH_MAX]=[3, 39]and the regex^[a-zA-Z][0-9a-zA-Z_-]*[0-9a-zA-Z]$— pinned across both backends. The TS Zod.min()/.max()/.regex()and the Rust hand-rolled byte-scan (is_valid_username_for_creation) are independent reimplementations of the same rule, so each length boundary is tested just-outside (→ 400) and just-inside (→ 403 no-matching-invite, the settle for a valid username on a spine withopen_signupat itsfalsedefault), and each format violation (leading non-letter, trailing punctuation, embedded disallowed char) → 400, with mid-string_/-accepted siblings as the don't-over-reject control. Signup is also strict-object on both spines (TSz.strictObject, Rust#[serde(deny_unknown_fields)]), so an unknown signup body key → 400.Email format (creation): the optional signup
emailis validated to a looselocal@domain.tldshape on both spines — TS Email (^[^\s@]+@[^\s@]+\.[^\s@]+$plus a 254-byte (RFC 5321 octet) length bound; whitespaceWhite_Space ∪ {U+FEFF}), Rust the hand-rolledis_valid_email. A malformed email → 400, a well-formed one → 403 no-matching-invite (the same accept/reject settle the username tables use). The accepted siblings include the single-char-TLDa@b.cthat Zod'sz.email()would reject — the deliberately-looser rule both spines now share. This closed a real divergence: the Rust spine previously length-checked the email only, accepting any non-empty string TS rejected.Both surfaces are flat REST on every spine, so this is an imperative suite (not a
conformance_tablerow) and ungated. Runs both legs via the shared{setup_test}protocol: the in-process leg (cross_backend/identity_parity.db.test.ts, plaingro test) and the cross-process leg (cross_backend/identity_parity.cross.test.ts, the TS spine binaries + Rusttesting_spine_stubover real HTTP).$lib-free by contract (relative specifiers only).- case-insensitive login — an account created
testing/cross_backend/in_process_setup.ts
In-process fixture producers for the cross-backend suite protocol.
default_in_process_setup(options)wraps create_test_app into the SetupTest contract;default_in_process_suite_options(options)emits the full in-process suite bundle ({setup_test, surface_source, capabilities}plus factory-input pass-through). Both reach the in-process Hono app, so this module transitively importshono— it lives apart fromsetup.ts(the shared fixture protocol + the cross-process producer) so a Rust-only consumer driving a spawned backend can import the cross-process helpers without thehonopeer. The cross-process sibling (default_cross_process_setup) implements the same contract by spawning a binary and bootstrapping over real HTTP.testing/cross_backend/login_security.ts
Cross-backend parity suite for login rate limiting + trusted-proxy (X-Forwarded-For) resolution over real HTTP.
Login throttling and client-IP resolution are wired-but-never-crossed: the in-process describe_rate_limiting_tests covers the limiter, and the
fuz_http/ proxy middleware unit tests cover XFF resolution, but no case exercised either over a real socket on both impls — the limiter is nulled on every standard cross backend and the resolved client IP has no wire-observable downstream there. This dedicated suite spawns a backend with the login limiters enabled + the loopback proxy trusted (seeglobal_setup_login_security.ts), then pins two properties end-to-end:- per-IP login limit fires — the first default_login_ip_rate_limit
failed logins from one forwarded IP each return
401, and the next returns429with the canonical `{error: "rate_limit_exceeded", retry_after}body **and** aRetry-After: ceil(retry_after)` header. The 429 wire shape is the cross-impl contract — TS rate_limit_exceeded_response and Rustroute_response::rate_limit_exceededmust agree. - trusted-proxy / XFF resolution is honored — distinct
X-Forwarded-ForIPs get independent buckets: after exhausting one forwarded IP to429, a *different* forwarded IP is unaffected (401, not429). A backend that ignored XFF and keyed on the (loopback) TCP peer would 429 the fresh-IP request too — so the401proves the limiter keys on the resolvedX-Forwarded-Forclient IP.
Determinism without a limiter reset. Limiter state is in-memory and the per-test
_testing_resetwipes only the DB, never the buckets — so each case uses its own forwarded IP and its own (non-existent) username, keeping every bucket independent across cases and across the two impls (separate processes). The login floor is zeroed on both spines, so the failed-login loop stays fast.Both surfaces are flat REST (
POST /api/account/login) on every spine, so this is an imperative suite (not aconformance_tablerow) — the sibling oforigin.ts/identity_parity.ts. Cross-process only: the limiter+proxy wiring is the point, and the in-process counterparts already exist (describe_rate_limiting_tests + the proxy middleware tests). Cited property:docs/security.md§"Rate Limiting" + §"Trusted Proxy / Client IP".$lib-free by contract (relative specifiers only), like the sibling cross-backend suites.- per-IP login limit fires — the first default_login_ip_rate_limit
failed logins from one forwarded IP each return
testing/cross_backend/make_cross_backend_project.ts
Generic vitest project factory for cross-backend integration suites.
One vitest project per spawned backend; each runs the consumer's shared
*.cross.test.tsfiles against its own bootstrapped binary. The paired create_cross_backend_global_setup (inglobal_setup.ts) reads the project'snameto pick which BackendConfig to spawn, so the project name is the single source of truth for backend selection.Consumers compose these into their
vite.config.ts:const cross_backend_projects = process.env.FUZ_TEST_CROSS_BACKEND ? [ make_cross_backend_project({name: 'cross_backend_ts_deno', global_setup: GLOBAL_SETUP}), make_cross_backend_project({name: 'cross_backend_rust', global_setup: GLOBAL_SETUP}), ] : [];where
GLOBAL_SETUP = './src/test/cross_backend/global_setup.ts'.This module is intentionally dependency-free and
assert_dev_env-free: it runs at vite config time (including production builds, where the consumer gates the projects behind an env flag), so it must not pull in the DEV-only test runtime.testing/cross_backend/method_coverage.ts
Reconcile a backend's live-mounted RPC method set against a tagged coverage manifest, so the off-declared-surface methods stay as drift-proof as the declared ones.
The spec-derived suites (describe_rpc_round_trip_tests / describe_rpc_attack_surface_tests) auto-enumerate the declared surface — add a method to a registry in create_spine_surface_spec and it is tested automatically. But a backend's live RPC endpoint also mounts methods kept *off* that surface (stateful cell verbs, opt-in resolvers,
_testing_*backdoors); those rely on hand-wired imperative suites and get no auto-enumeration. Nothing structurally guaranteed every live method was actually claimed by a suite — so a newly mounted-but-untested method could ship silently.{@link assert_rpc_method_coverage} closes that gap: it diffs the live method set against a {@link MethodCoverageEntry} manifest (both directions — a mounted-but-unclaimed method *and* a stale manifest row both fail loud) and checks each entry's tier is consistent with the declared surface + the backdoor prefix. The manifest becomes the forcing function — a new method can't reach the live mount without a manifest row naming the suite that covers it.
Pairs with
surface_invariants.tsassert_no_testing_methods (which guards the *reverse* — a backdoor must never leak *onto* the declared surface).testing/cross_backend/origin.ts
Cross-backend parity suite for Origin verification.
Origin checking is middleware that runs *before* the RPC dispatcher and returns a flat REST
{error}body — not a JSON-RPC envelope — so it doesn't fit the envelope-shaped conformance-table runner. This dedicated imperative suite drives raw transport calls instead, mirroring how the in-process origin tests were already hand-rolled. Two cases:- disallowed
Origin→ 403forbidden_origin, refused before any handler runs (the allowlist rejects the cross-origin request even with a valid session cookie attached). - absent
Origin→ request passes — non-browser / direct-access clients (curl, CLI, server-to-server) carry noOriginand must not be blocked; token auth is the control for those callers.
Runs both legs via the shared
{setup_test, capabilities}protocol: the in-process leg (auth/origin_parity.db.test.ts, plaingro test) and the cross-process leg (cross_backend/origin.cross.test.ts, the TS spine binaries + Rusttesting_spine_stubover real HTTP). Origin middleware is on every spine, so the suite is ungated.$lib-free by contract (relative specifiers only), like the sibling cross-backend suites.- disallowed
testing/cross_backend/peer_ping_ws.ts
Cross-process server-initiated
peer/pingsuite — the machinery proof for ActionPeer (a backend initiating a JSON-RPC request to a connected client and awaiting its typed reply). The sibling of the one-way notification suite (role_grant_offer_notification_ws.ts), extended from server→client *notifications* to server→client *request/response*.peer/pingisinitiator: both: the client→server direction already exists asheartbeat; the new server→client direction is what this exercises. The observable trigger is the client invoking thepeer/pingaction over its own socket — the handler turns around and *initiates* apeer/pingrequest back to that socket, awaits the client's echo, validates it against PingResponse, and returns the validated shape. So one client RPC drives the whole round-trip, and every outcome (success /Timeout/ wrong-shape / client-error) surfaces as that RPC's wire response — directly assertable.The client side attaches an
on_requestresponder at construction (via create_ws_transport's seam) so the server-initiated request is answered as soon as it arrives. Security negatives use the rawWsClient.sendto inject unsolicited / cross-connection frames.Per-spec auth.
peer/pingisauth: public(a liveness echo is non-sensitive — see the design doc); the WS upgrade itself still authenticates, so the suite drives it over the keeper's session.Gated on
capabilities.peer_request—trueonly for the Rust spine (server-initiated requests landed Rust-first canonical); the TS family skips until its server transport's request path lands (deferred twin-impl convergence). Cross-process only: create_ws_transport needs a real bound socket, so wire it from a*.cross.test.ts.testing/cross_backend/ready.ts
Cross-backend parity suite for the
/readyschema-drift deploy gate.The
/readymechanism already ships on both twins (TS create_ready_route_spec / db/schema_ready.ts; Rustfuz_http::ready/fuz_db::schema_ready), each with its own drift →503unit tests. This suite is the missing automated cross-impl gate: an anonymousGET /readyreturns200 {ready: true}over real HTTP on both spine test servers, proving the success path is wire- identical and that both backends read the same committedexpected_schema.json(column-presence is engine-portable, so one fixture is the cross-impl contract)./readyis a plain public REST route — not an RPC method, not one of the six REST auth routes — and it's deliberately off the declared spine surface (create_spine_surface_spec), like ws/sse/cells/fact-serving. So it needs a bespoke imperative suite (à laorigin.cross.test.ts), gated oncapabilities.ready, rather than aconformance_tablerow or generic round-trip enumeration. The drift →503path stays per-impl unit tests.Runs both legs via the shared
{setup_test, capabilities}protocol: the in-process leg (cross_backend/ready_parity.db.test.ts, plaingro test) and the cross-process leg (cross_backend/ready.cross.test.ts, the TS spine binaries + Rusttesting_spine_stubover real HTTP).$lib-free by contract (relative specifiers only), like the sibling cross-backend suites.testing/cross_backend/role_grant_offer_enumeration.ts
Cross-backend parity suite for the role_grant_offer_accept enumeration boundary — the deliberate 403-vs-404 split on the accept path.
role_grant_offer_acceptdistinguishes two denials, and the distinction is a conscious decision (not an accident to mask away):- a genuinely-nonexistent offer (or one on another account) → 404
role_grant_offer_not_found. The account-scoped IDOR guard refuses to confirm an offer id the caller's account doesn't own — so a cross-account prober learns nothing. - an offer that exists on the caller's OWN account but is targeted to a
different actor (a sibling persona) → 403
role_grant_offer_actor_mismatch. This reveals "an offer exists for a sibling actor" to a co-account caller — but there is no cross-account leak: every actor in the distinction belongs to the one account already authenticated, so the 403 discloses nothing a principal can't already see about its own account. Masking this to 404 would only obscure a legitimate "not yours to accept, pick the right persona" signal. So 403 is the chosen, defensible behavior — and both spines must agree on it (a future Rust or TS change that over-masked the sibling case to 404, or under-masked the cross-account case to 403, is the regression this suite catches).
The actor-mismatch arm only fires for an *actor-targeted* offer accepted by a sibling actor, so the suite needs a multi-actor recipient. The keeper is the only fixture account that can be seeded multi-actor (
extra_actors), and an account can't offer to itself — so the grantor is a separate admin account and the recipient is the keeper (actor A =fixture.actoris the offer target; actor B =fixture.extra_actors[0]is the rejected sibling).Multi-step (create → accept) and using the
actingselector, so this is an imperative suite (not aconformance_tablerow). The accept verb is on every spine's standard RPC surface, so the suite is ungated. Cross-process only: the sibling-actor / actor-targeted-offer setup is a wire flow. Cited property:docs/security.md§"Authorization" (the 404-over-403 mask is scoped to cross-principal leaks; an intra-account sibling-actor offer stays a 403role_grant_offer_actor_mismatch).$lib-free by contract (relative specifiers only), like the sibling cross-backend suites.- a genuinely-nonexistent offer (or one on another account) → 404
testing/cross_backend/role_grant_offer_notification_ws.ts
Cross-process role-grant-offer lifecycle WS notification suite — the machinery proof for the consentful-role-grants notification fan-out across any spine backend. Covers all seven server-initiated notifications:
role_grant_offer_received→ recipient (offer created)role_grant_offer_accepted→ grantor (recipient accepts)role_grant_offer_declined→ grantor (recipient declines)role_grant_offer_retracted→ recipient (grantor retracts)role_grant_revoke(flat, omitsrevoked_by) → revokee (active grant revoked)role_grant_offer_supersede→ each superseded sibling's grantor, fired on BOTH the accept-cascade (reason: 'sibling_accepted') and the revoke-cascade (reason: 'role_grant_revoked')
These exercise only spine primitives (accounts, role-grants, offers, WS notifications) — zero consumer domain — so the suite lives here and runs against any backend that wires the standard RPC actions'
notification_senderand mounts a registered WS socket: fuz_app's own spine self-tests (testing_spine_server+ the Rusttesting_spine_stub) and downstream twin-impl consumers (the fuz_forge Deno/Hono + Rustfuz_forge_serverbackends) alike.Each case is a *targeted* server-initiated notification (vs the broadcast in a
repo_updated-style suite), so it opens the affected counterparty's socket, drives the lifecycle RPC over HTTP, then asserts the frame lands on that socket and strict-parses against the canonical wire schema — the guard against serialization drift (field / null / datetime / the flat revoke shape / the supersedereason+cause_id).Sends are queued on the post-commit drain (handler-emit, not audit-derived), so a frame may land a beat after the RPC resolves —
WsClient.wait_forpolls already-received messages then waits, absorbing the fan-out latency without a sleep, and its method+predicate filter ignores unrelated frames (e.g. thereceivedpush the recipient also gets). ROLE_ADMIN is the only admin-grantable role; accounts that already hold it can still be offered it again (a fresh pending row — the prior accept is terminal, no already-granted guard), and accept stays idempotent on the role_grant while still superseding pending siblings. Gated oncapabilities.ws.Cross-process only: create_ws_transport needs a real bound socket, so wire it from a
*.cross.test.tsfile, never an in-process setup. Authed cookies come from the per-account session minted byfixture.create_account/fixture.create_session_headers.testing/cross_backend/role_grant_participation.ts
Cross-backend parity suite for role-gated participation conferral — the success paths. The imperative escape hatch beneath the declarative
conformance_participation_cases.ts: the multi-step flows that need a real recipient account (so a static conformance row can't express them) — an admin assigns theparticipantapp-role, and an admin offers it through the consent flow and the recipient accepts.Proves the two conferral *write* paths agree on both spines (TS spine binary + Rust
testing_spine_stub):- immediate assign —
role_grant_assignof the admin-grantableparticipantrole lands a grant and returns{ok, role_grant_id}; re-assigning the active grant is idempotent (same id). - consent flow —
role_grant_offer_createofparticipant(admin-only) → the recipientrole_grant_offer_accepts → a role_grant lands.
The single-request gate/denial matrix (grantability refusal, admin-only conferral, dispatcher admin gate, auth) lives in the declarative table (
conformance_participation_cases.ts) — keep new single-request assertions there; this suite is only for flows the table cannot carry.The
participantrole is registered admin-grantable on both spines (spine_roles / the Rust stub'sRoleRegistry), so the suite is ungated; every spine mounts the standard RPC surface it drives.$lib-free by contract (relative specifiers only), like the sibling cross-backend suites.- immediate assign —
testing/cross_backend/rust_spine_stub_backend_config.ts
Cross-process BackendConfig preset for the non-domain spine consumer,
testing_spine_stub— a Rust binary that mounts only the spine surface (auth / account / admin / audit / role-grant offers) with no domain layer. fuz_app drives it fromsrc/test/cross_backend/*.cross.test.tsto verify its TS spec against the Rust spine end-to-end with no domain implementation in the loop — drift becomes a fuz_app failure rather than a downstream consumer's failure with mixed signals.Binary discovery — env-supplied, never hardcoded. The binary lives in a sibling Rust workspace, not in fuz_app, so the preset never bakes a path in.
FUZ_TESTING_RUST_SPINE_STUB_BIN(or thebinary_pathoption) must point at a prebuilt binary; the preset throws a clear error when neither is set rather than guessing. Build once withcargo build -p testing_spine_stub --releaseand point the env var at the resultingtarget/release/testing_spine_stub; operators / CI cache the binary across runs for fast spawns.Operator setup — the target Postgres database must exist before the harness runs (the harness never issues
CREATE DATABASE, to avoid forcing aCREATEDBgrant on the test role):createdb fuz_app_test_rust_spine_stub 2>/dev/null || trueThe binary self-wipes the auth-namespace schema on every boot (
FUZ_TESTING_RESET_DB_ON_STARTUP=true, set by the Rust-family builder), so no manualDROP TABLEbetween sessions is needed; per-test reset is the orthogonal_testing_resetRPC action default_cross_process_setup fires.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.The cross-process producer default_cross_process_setup lives here, alongside the spawn-a-backend transport plumbing — it implements the SetupTest contract by spawning a binary and bootstrapping over real HTTP. The in-process producers (default_in_process_setup / default_in_process_suite_options, which wrap create_test_app) live in the sibling
in_process_setup.tsso this module — and the cross-process consumers that import it — stay free of the in-process Hono app and its optionalhonopeer dependency.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:
- Write the bootstrap token (
config.bootstrap.token) toconfig.bootstrap.token_pathso the binary picks it up at startup. 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.- Poll
{base_url}{health_path}until it returns 2xx orstartup_timeout_mselapses. - Read
config.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 testing/transports/bootstrap.ts against a FetchTransport built aroundhandle.config.base_url. Splitting the two keeps spawn_backend consumer-agnostic — fuz_app knows nothing about specific binary contents.- Write the bootstrap token (
testing/cross_backend/spine_surface_constants.ts
Pure spine-surface path + role constants — the hono-free leaf split out of
default_spine_surface.ts.Cross-process suite modules (which drive a separately-spawned backend binary over HTTP) need only the wire path / role / fixture-URL, not the in-process route handlers. Importing them from
default_spine_surface.tsused to drag its eageraccount_routes.ts/signup_routes.tsimports — and through themsession_middleware→hono/cookie— onto a backend-spawning consumer with nohonopeer installed (a Rust-only spine consumer). Keeping these constants on this handler-free leaf lets such a consumer import the path without the peer.default_spine_surface.tsre-exports them for in-process callers.testing/cross_backend/sse_round_trip.ts
Cross-process SSE round-trip suite — the cross-process counterpart to the in-process testing/sse_round_trip.ts harness.
Where the in-process harness reads a Hono
Response.bodydirectly, this suite opens a real streamingfetchagainst a spawned backend's audit-log SSE endpoint via create_sse_transport, threading the fresh-per-test keeper's session cookie. It is the only coverage of the spawned binary's live SSE path — the standard cross-process bundle (describe_standard_cross_process_tests) omits SSE by design, so consumers call this alongside it (paralleling describe_cross_process_ws_tests).Four cases, mirroring the in-process SSE self-test against fuz_app's standard audit-log stream:
- connects — the stream opens and emits the
: connectedcomment. - data frame (gated on
rpc_path) — a minted secondary's sessions are revoked over the keeper's admin channel (admin_session_revoke_all), broadcasting asession_revoke_allaudit event as onedata:frame to the subscribed keeper without closing its stream (the event targets the secondary, not the subscriber). The secondary is minted *before* the stream opens socreate_account's own audit events (invite / signup / login / token) don't land on it. - close-on-revoke, account-wide (gated on
rpc_path) — the subscriber's *own* sessions are revoked (account_session_revoke_all), so thesession_revoke_allevent targets the keeper and the audit guard drops the live stream via the account-wideclose_for_accountpath. Asserted viaSseTransport.wait_for_close. - close-on-revoke, session-scoped (gated on
rpc_path) — the subscriber's *own* single session is revoked (account_session_revoke), so thesession_revokeevent drops the stream via the session-hash-scopedclose_for_sessionpath (the distinct primitive cases 2–3 don't reach).
The close-on-revoke matrix is layered: cases 3–4 exercise the account-wide and session-scoped paths cross-process; the remaining union events (
token_revoke_all/logout/password_change, all account-wide; androle_grant_revoke, role-matched) are covered by the spine'sfuz_realtimeSSE-registry unit tests and the in-process guard self-test, so a cross-processtoken_revoke_all-with-zero-tokens case (which may emit no audit row) stays out to keep the spawned-backend suite non-flaky.Gated on
capabilities.sse— backends without an end-to-end SSE stream skip (the cases still surface as.skipin the report). Cross-process only: create_sse_transport needs a real bound socket, so wire it from a*.cross.test.tsfile, never an in-process setup.- connects — the stream opens and emits the
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: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's
globalSetupalready consumed it before the suite file loads. Re-running would 409.
testing/cross_backend/test_cell_gated_create_authorize.ts
Test CellCreateAuthorize policy mounted on both reference spines — the TS spine binary's full mount (
full_spine_mount.ts) and the Rusttesting_spine_stub— so the cross-backendcell_gated_createsuite proves the parent-aware cell-creation authorizer agrees TS↔Rust. The twin of the RustTestCellGatedCreateAuthorize, directory model.Admin bypasses everything (
{allow: true, moderation_required: false}). For a non-admin:- Root creation (
root_idnull):kind: 'space'is admin-only (denied); every other parentless kind stays open, so the plain-createcell_crud/cell_relationssuites are unaffected. - Contribution (
root_idset): the governing root'sdata.policy[kind] = {min_role?, moderation_required?}decides — a missing entry denies, a presentmin_rolethe actor lacks denies, and otherwise it admits with the entry'smoderation_requiredfolded into the verdict. The root'sdataarrives ininput.root_data(the handler read it in-tx), so the predicate is pure — no DB read of its own (which also dodges the single-connection PGlite deadlock a separate handle would hit).
$lib-free by contract — reached by the spawned TS spine binary under Gro's loader (no$libalias). Keep every import relative.- Root creation (
testing/cross_backend/testing_backdoor.ts
Cross-backend negative-credential suite for the
_testing_*backdoor actions._testing_reset/_testing_mint_session/_testing_put_fact/_testing_schema_snapshot/_testing_migration_tracker/_testing_action_manifestare privileged test-binary actions the production wire never exposes — three direct DB writes (full auth wipe, forged session row, raw fact insert) plus three introspection reads (the live schema, theschema_versionmigration tracker, and the live RPC registry — the highest info-leak of the set were the gate to break). Their only structural fence is the daemon-token credential gate on each spec'sauthaxis. A test binary live-mounts them on its RPC endpoint but keeps them off the declared surface — so the spec-derived describe_rpc_attack_surface_tests never enumerates them, and nothing else fires them with a non-daemon credential to prove the gate holds end-to-end. This suite does, against each impl's real auth resolution.For every backdoor method, three principals:
- anonymous (no credential) →
401(pre-validation auth refuses an account-less caller before anything else). - session (the keeper's browser-context cookie) →
403credential_type_required— a session cookie, even one carrying the keeper role, tops out below the daemon-token channel. - bearer (the keeper's api-token, non-browser context) →
403credential_type_required— same ceiling; an api token cannot reach keeper operations.
Each method is sent with valid params so the session/bearer cases clear the dispatcher's input-validation (400) phase and actually reach the post-authorization credential gate (the order is 401 → 400 → 403); the handler never runs (the gate refuses first), so the writes never execute.
Complements the spec-level gate check (which pins that each spec *declares*
credential_types: ['daemon_token']) and the surface-absence invariant (assert_no_testing_methods) — this one pins the runtime 401/403 behavior on both impls. Cited property:security.md§Test Backdoor Actions (daemon-token-gated, off-surface, DEV-excluded).Cross-process only — the
_testing_*actions are mounted on the spawned binary, not the in-process app — like the ws/sse suites. Wire from a*.cross.test.ts. Requires the standard_testing_*actions mounted (the same precondition default_cross_process_setup already imposes for its per-test_testing_reset); ungated, since every cross backend mounts them.$lib-free by contract (relative specifiers only), like the sibling cross-backend suites.- anonymous (no credential) →
testing/cross_backend/testing_reset_actions.ts
Test-binary RPC actions for cross-process integration tests.
Six daemon-token-authed actions, bundled by create_testing_actions:
_testing_reset(DB wipe + keeper re-seed),_testing_drain_effects(audit barrier),_testing_mint_session(forge an expired-by-construction server-side session for the expiry conformance cases),_testing_put_fact(seed an embedded fact for the fact-serving suite),_testing_schema_snapshot(introspect the live schema for cross-impl parity diffing against a Rust backend'sfuz_dbsnapshot), and_testing_migration_tracker(dump theschema_versiontracker rows for cross-impl migration-identity parity — the provenance half the snapshot gate excludes by design).A further daemon-token action,
_testing_action_manifest(dump the live RPC registry for cross-impl method-set + auth-shape parity), lives here too but is not bundled by create_testing_actions — it must enumerate *every* mounted method, so it's appended at the full-mount layer (build_full_spine_rpc_actions) where the complete list exists._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.ts';— 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.testing/cross_backend/testing_server_bun.ts
Bun runtime adapter for spawnable cross-process test server binaries.
Binds
Bun.serveandhono/bun's module-levelupgradeWebSocket+websockethandler. The shared testing/cross_backend/testing_server_core.ts owns the rest. Third sibling to testing/cross_backend/testing_server_node.ts / testing/cross_backend/testing_server_deno.ts — together the three isolate the JS-runtime axis (Node V8 / Deno V8 / Bun JSC) on identical TS surfaces, and the Rust spine binary covers the cross-language axis.Needs no extra deps:
hono/bunships with thehonopeer dep andBun.serveis built in (unlike Node, which pulls@hono/node-server+@hono/node-ws). RuntimeDeps reuse create_node_runtime — Bun implements thenode:fs/node:processsurface RuntimeDeps +cli/daemontouch.Bun.serveis declared locally (mirroring testing/cross_backend/testing_server_deno.ts'sDenodeclaration) so this module typechecks under fuz_app's Node-based config without@types/bun. It is only ever *run* under Bun.testing/cross_backend/testing_server_core.ts
Runtime-agnostic core for spawnable cross-process test server binaries.
A test binary mounts a fuz_app-derived surface over a real HTTP socket so the
cross_backend/*suites (and the cross-impl bench) can drive it the same way they drive the Rust spine. This module owns the runtime-neutral orchestration — stale-daemon check, daemon-info write, serve, post-serve WS attach, graceful drain shutdown — and delegates the runtime-boundary primitives (HTTP serve, WS upgrade construction, signals, pid, exit) to a {@link TestingServerAdapter}. The two shipped adapters are testing/cross_backend/testing_server_node.ts (@hono/node-server+@hono/node-ws) and testing/cross_backend/testing_server_deno.ts (Deno.serve+hono/deno).The app itself — routes, RPC, DB,
_testing_reset, optional WS mount — is the caller's {@link StartTestingServerOptions.build_app} seam, so this core stays domain-free. fuz_app's owntesting_spine_serverpasses a no-domain build; consumers (zzz, fuz_forge) pass their domain build.NEVER ships in a release. This module lives under
cross_backend/and opens withimport '../assert_dev_env.ts';, which throws on production-bundle load. The runtime adapters reach for the optional@hono/node-server/@hono/node-wspeer deps; only test binaries import them.testing/cross_backend/testing_server_deno.ts
Deno runtime adapter for spawnable cross-process test server binaries.
Binds
Deno.serveandhono/deno's module-levelupgradeWebSocket. The shared testing/cross_backend/testing_server_core.ts owns the rest. Counterpart to testing/cross_backend/testing_server_node.ts — together they isolate the JS-runtime axis (Deno vs Node V8) on identical TS surfaces, and the Rust spine binary covers the cross-language axis.Denoglobals are declared locally (mirroring runtime/deno.ts) so this module typechecks under fuz_app's Node-based config without adeno.jsonor@types/deno. It is only ever *run* under Deno.testing/cross_backend/testing_server_node.ts
Node runtime adapter for spawnable cross-process test server binaries.
Binds
@hono/node-server'sserve()and@hono/node-ws's two-phasecreateNodeWebSocket(app)/injectWebSocket(server). The shared testing/cross_backend/testing_server_core.ts owns the rest. A test binary builds this adapter and hands it to start_testing_server alongside itsbuild_appseam.@hono/node-server+@hono/node-wsare optional peer deps (same posture asws) — only test binaries import them; production bundles never reach this module (theassert_dev_envguard throws on prod load).testing/cross_backend/ts_spine_backend_config.ts
Cross-process BackendConfig presets for fuz_app's domain-free TS spine test binary (
src/test/cross_backend/testing_spine_server_{node,deno,bun}.ts).The TS analog of rust_spine_stub_backend_config (which spawns the Rust spine): these spawn fuz_app's own TS impl over real HTTP with no domain layer, so the
cross_backend_ts_node/cross_backend_ts_deno/cross_backend_ts_bunself-test projects verify fuz_app's wire path in its own repo across all three JS runtimes. All run against in-memory PGlite (memory://) — no external Postgres, unlike the Rust path.The binary writes its daemon token to
{FUZ_TESTING_TS_SPINE_DIR}/run/daemon_token; anchoring the dir topaths.rootmakes that equalpaths.daemon_token_path, which spawn_backend reads after the health probe.testing/cross_backend/ws_round_trip.ts
Cross-process WebSocket round-trip suite — the cross-process counterpart to the in-process testing/ws_round_trip.ts harness.
Where the in-process harness drives register_action_ws against a fake Hono upgrade (no wire), this suite performs a real
WebSocketupgrade against a spawned backend via create_ws_transport (thewsnpm package), so the actual upgrade handshake + per-connection auth + JSON-RPC dispatch over the socket are exercised end-to-end. It is the only coverage of the spawned binary's live WS path — the standard cross-process bundle (describe_standard_cross_process_tests) omits WS by design, so consumers call this alongside it.Consumer-agnostic. Every case drives the
heartbeatprotocol action, which assert_ws_endpoints_include_protocol_actions guarantees is present on every WS endpoint — so the suite needs no knowledge of a consumer's domain WS methods. It validates the transport, not the domain.The first three cases mirror the upgrade stack register_ws_endpoint wires (origin check → require_auth → dispatch): an authenticated upgrade round-trips
heartbeat; an anonymous upgrade is refused; a disallowed-origin upgrade is refused. Per-connection auth is enforced at upgrade time (not per message), so the negative cases assert the upgrade itself rejects rather than a per-message error frame.A fourth case (gated on
rpc_path) covers server-initiated close: an authenticated socket is dropped when the account's sessions are revoked mid-connection. Per-message dispatch never re-checks credential validity, so the live socket survives on the audit-fed create_ws_auth_guard seam — firingaccount_session_revoke_allover the keeper's session channel emitssession_revoke_all, which closes the socket. Omitrpc_pathto skip it (consumers without the standard account actions on their RPC endpoint).Gated on
capabilities.ws— backends without an end-to-end WS transport skip (the cases still surface as.skipin the report). Cross-process only: create_ws_transport needs a real bound socket, so wire it from a*.cross.test.tsfile, never an in-process setup.testing/cross_backend/xfail.ts
xfail_until — mark a deferred-by-design gap as an expected failure.
A thin wrapper over vitest's
test.failsthat bakes a tracking id + reason into the test label. Two properties make it the right tool for declared gaps (distinct from in-scope gaps, which fail loud as a normal redtest):- Visible — the case shows in the report as a distinct expected
failure, not a silent
.skip, so a deferred gap never disappears from view. - Self-cleaning —
test.failsturns red the moment the body stops throwing (i.e. the impl starts passing), forcing whoever closed the gap to delete the marker. A.skipwould rot silently; this can't.
Sibling to test_if in testing/cross_backend/capabilities.ts. No taxonomy — a one-line marker with a tracking id and a reason, nothing more.
- Visible — the case shows in the report as a distinct expected
failure, not a silent