@fuzdev/fuz_app 0.147.0 → 0.149.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/CLAUDE.md +8 -6
- package/dist/actions/action_codegen.d.ts +15 -1
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +23 -2
- package/dist/actions/action_event.d.ts.map +1 -1
- package/dist/actions/action_event.js +14 -1
- package/dist/actions/perform_action.d.ts +17 -1
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +23 -3
- package/dist/auth/CLAUDE.md +3 -2
- package/dist/auth/account_action_specs.d.ts +12 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +16 -2
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +7 -4
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +29 -8
- package/dist/auth/audit_emitter.d.ts +14 -0
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +5 -1
- package/dist/auth/cell_action_specs.d.ts +3 -1
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +5 -5
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +2 -3
- package/dist/auth/cell_data_schema.d.ts +1 -1
- package/dist/auth/cell_data_schema.js +1 -1
- package/dist/auth/cell_field_action_specs.d.ts +4 -3
- package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_field_action_specs.js +5 -6
- package/dist/auth/cell_field_actions.d.ts.map +1 -1
- package/dist/auth/cell_field_actions.js +8 -1
- package/dist/auth/cell_item_action_specs.d.ts +4 -2
- package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_item_action_specs.js +5 -5
- package/dist/auth/cell_item_actions.d.ts.map +1 -1
- package/dist/auth/cell_item_actions.js +8 -1
- package/dist/auth/role_grant_offer_action_specs.d.ts +2 -0
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +8 -4
- package/dist/http/CLAUDE.md +14 -2
- package/dist/http/error_schemas.d.ts +8 -0
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +12 -0
- package/dist/http/jsonrpc_errors.d.ts +40 -15
- package/dist/http/jsonrpc_errors.d.ts.map +1 -1
- package/dist/http/jsonrpc_errors.js +90 -35
- package/dist/primitive_schemas.d.ts +1 -1
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +2 -1
- package/dist/schema_meta.d.ts +80 -0
- package/dist/schema_meta.d.ts.map +1 -1
- package/dist/schema_meta.js +71 -1
- package/dist/testing/CLAUDE.md +853 -146
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +73 -5
- package/dist/testing/adversarial_input.d.ts +0 -47
- package/dist/testing/adversarial_input.d.ts.map +1 -1
- package/dist/testing/adversarial_input.js +4 -340
- package/dist/testing/adversarial_input_cases.d.ts +124 -0
- package/dist/testing/adversarial_input_cases.d.ts.map +1 -0
- package/dist/testing/adversarial_input_cases.js +393 -0
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +24 -20
- package/dist/testing/audit_drift_guard.d.ts +30 -3
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +55 -3
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +29 -12
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
- package/dist/testing/cross_backend/actor_lookup.js +7 -25
- package/dist/testing/cross_backend/app_settings.d.ts.map +1 -1
- package/dist/testing/cross_backend/app_settings.js +4 -6
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts +57 -0
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/audit_cross_helpers.js +89 -0
- package/dist/testing/cross_backend/backend_config.d.ts +2 -1
- package/dist/testing/cross_backend/backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.d.ts +6 -0
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +26 -44
- package/dist/testing/cross_backend/body_size_smuggling.d.ts +5 -0
- package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size_smuggling.js +7 -14
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts +3 -1
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.d.ts +78 -59
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +25 -24
- package/dist/testing/cross_backend/cargo_binary.d.ts +70 -0
- package/dist/testing/cross_backend/cargo_binary.d.ts.map +1 -0
- package/dist/testing/cross_backend/cargo_binary.js +148 -0
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +174 -3
- package/dist/testing/cross_backend/cell_gated_create.d.ts +0 -60
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +9 -487
- package/dist/testing/cross_backend/cell_grant_role.d.ts +14 -2
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.js +22 -12
- package/dist/testing/cross_backend/cell_moderate.d.ts +66 -0
- package/dist/testing/cross_backend/cell_moderate.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_moderate.js +511 -0
- package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_ref_admission.js +1 -1
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +40 -19
- package/dist/testing/cross_backend/conformance_case.d.ts +43 -4
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +29 -1
- package/dist/testing/cross_backend/conformance_rpc_auth.d.ts +93 -0
- package/dist/testing/cross_backend/conformance_rpc_auth.d.ts.map +1 -0
- package/dist/testing/cross_backend/conformance_rpc_auth.js +125 -0
- package/dist/testing/cross_backend/conformance_table.d.ts +144 -4
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +297 -65
- package/dist/testing/cross_backend/cookie_attributes.d.ts.map +1 -1
- package/dist/testing/cross_backend/cookie_attributes.js +3 -2
- package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts +38 -5
- package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/create_cross_backend_global_setup.js +24 -3
- package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts +5 -5
- package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_deadline.d.ts +10 -6
- package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_deadline.js +32 -31
- package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_header_robustness.js +4 -6
- package/dist/testing/cross_backend/{cell_cross_helpers.d.ts → cross_rpc.d.ts} +14 -2
- package/dist/testing/cross_backend/cross_rpc.d.ts.map +1 -0
- package/dist/testing/cross_backend/{cell_cross_helpers.js → cross_rpc.js} +31 -7
- package/dist/testing/cross_backend/default_backend_configs.d.ts +58 -61
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +97 -91
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +2 -2
- package/dist/testing/cross_backend/differential.d.ts +126 -0
- package/dist/testing/cross_backend/differential.d.ts.map +1 -0
- package/dist/testing/cross_backend/differential.js +201 -0
- package/dist/testing/cross_backend/differential_normalize.d.ts +150 -0
- package/dist/testing/cross_backend/differential_normalize.d.ts.map +1 -0
- package/dist/testing/cross_backend/differential_normalize.js +153 -0
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/fact_serving.js +27 -17
- package/dist/testing/cross_backend/full_spine_mount.d.ts +15 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +55 -1
- package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/identity_parity.js +6 -5
- package/dist/testing/cross_backend/in_process_setup.d.ts +12 -2
- package/dist/testing/cross_backend/in_process_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/in_process_setup.js +10 -7
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +13 -18
- package/dist/testing/cross_backend/origin.js +1 -1
- package/dist/testing/cross_backend/origin_conformance.js +1 -1
- package/dist/testing/cross_backend/origin_scope.d.ts +6 -0
- package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin_scope.js +13 -12
- package/dist/testing/cross_backend/peer_ping_ws.d.ts +2 -5
- package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.js +14 -24
- package/dist/testing/cross_backend/postgres_database.d.ts +85 -0
- package/dist/testing/cross_backend/postgres_database.d.ts.map +1 -0
- package/dist/testing/cross_backend/postgres_database.js +140 -0
- package/dist/testing/cross_backend/provided_context.d.ts +28 -0
- package/dist/testing/cross_backend/provided_context.d.ts.map +1 -0
- package/dist/testing/cross_backend/provided_context.js +35 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/query_shape.js +10 -12
- package/dist/testing/cross_backend/ready.js +2 -2
- package/dist/testing/cross_backend/revocation_drivers.d.ts +4 -3
- package/dist/testing/cross_backend/revocation_drivers.d.ts.map +1 -1
- package/dist/testing/cross_backend/revocation_drivers.js +1 -1
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +3 -2
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts +13 -0
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.js +203 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +2 -5
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +203 -111
- package/dist/testing/cross_backend/role_grant_participation.d.ts +13 -0
- package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_participation.js +32 -21
- package/dist/testing/cross_backend/role_grant_role_grammar.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_role_grammar.js +6 -9
- package/dist/testing/cross_backend/rpc_adversarial.d.ts +47 -0
- package/dist/testing/cross_backend/rpc_adversarial.d.ts.map +1 -0
- package/dist/testing/cross_backend/rpc_adversarial.js +302 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.d.ts +24 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.d.ts.map +1 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.js +124 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts +101 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts.map +1 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.js +113 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +10 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +33 -21
- package/dist/testing/cross_backend/session_cap.d.ts +9 -8
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/session_cap.js +9 -4
- package/dist/testing/cross_backend/session_cookie_codec.js +1 -1
- package/dist/testing/cross_backend/setup.d.ts +58 -56
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +29 -29
- package/dist/testing/cross_backend/simple_request.d.ts.map +1 -1
- package/dist/testing/cross_backend/simple_request.js +4 -4
- package/dist/testing/cross_backend/spawn_backend.d.ts +59 -10
- package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -1
- package/dist/testing/cross_backend/spawn_backend.js +219 -60
- package/dist/testing/cross_backend/spine_suites.d.ts +311 -0
- package/dist/testing/cross_backend/spine_suites.d.ts.map +1 -0
- package/dist/testing/cross_backend/spine_suites.js +897 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +44 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +53 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +3 -4
- package/dist/testing/cross_backend/standard.d.ts +18 -1
- package/dist/testing/cross_backend/standard.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.js +2 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +24 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +15 -0
- package/dist/testing/cross_backend/testing_backdoor.d.ts +29 -12
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +143 -51
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +30 -14
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +58 -37
- package/dist/testing/cross_backend/testing_server_bun.js +1 -1
- package/dist/testing/cross_backend/token_lifetime.js +1 -1
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +9 -13
- package/dist/testing/cross_backend/transport_composition.d.ts +26 -2
- package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -1
- package/dist/testing/cross_backend/transport_composition.js +81 -51
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +8 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +23 -13
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts +2 -5
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_action_rate_limit.js +8 -14
- package/dist/testing/cross_backend/ws_connection_cap.d.ts +4 -9
- package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_connection_cap.js +11 -12
- package/dist/testing/cross_backend/ws_endpoint.d.ts +52 -0
- package/dist/testing/cross_backend/ws_endpoint.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_endpoint.js +54 -0
- package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts +2 -5
- package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_inbound_dispatch.js +9 -15
- package/dist/testing/cross_backend/ws_outbound_overflow.d.ts +2 -7
- package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_outbound_overflow.js +5 -9
- package/dist/testing/cross_backend/ws_role_gated.d.ts +4 -5
- package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_role_gated.js +12 -21
- package/dist/testing/cross_backend/ws_round_trip.d.ts +12 -9
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +29 -34
- package/dist/testing/db_entities.d.ts +16 -3
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +28 -5
- package/dist/testing/integration_helpers.d.ts +61 -2
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +75 -18
- package/dist/testing/round_trip.d.ts +17 -0
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +67 -24
- package/dist/testing/rpc_attack_surface.d.ts +10 -0
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +27 -140
- package/dist/testing/rpc_envelope_cases.d.ts +114 -0
- package/dist/testing/rpc_envelope_cases.d.ts.map +1 -0
- package/dist/testing/rpc_envelope_cases.js +243 -0
- package/dist/testing/rpc_helpers.d.ts +31 -0
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +35 -14
- package/dist/testing/rpc_round_trip.d.ts +62 -6
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +151 -39
- package/dist/testing/schema_generators.d.ts +99 -2
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +261 -73
- package/dist/testing/standard.d.ts +26 -0
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +4 -1
- package/dist/testing/stubs.d.ts +4 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +6 -4
- package/dist/testing/transports/raw_http.d.ts +21 -0
- package/dist/testing/transports/raw_http.d.ts.map +1 -1
- package/dist/testing/transports/raw_http.js +30 -0
- package/dist/zod.d.ts +9 -0
- package/dist/zod.d.ts.map +1 -1
- package/dist/zod.js +20 -0
- package/package.json +6 -4
- package/dist/testing/cross_backend/actor_search.d.ts +0 -6
- package/dist/testing/cross_backend/actor_search.d.ts.map +0 -1
- package/dist/testing/cross_backend/actor_search.js +0 -90
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +0 -1
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -32,7 +32,7 @@ time (never runtime), where a throwing guard would break `vite build`.
|
|
|
32
32
|
- `stub_handler()` — fresh `Response('stub')`.
|
|
33
33
|
- `stub_mw` — pass-through middleware (`async (_c, next) => next()`).
|
|
34
34
|
- `create_stub_app_deps()` — factory: fresh `AppDeps` with no-op keyring/password/`delete_file`, a `read_secure_file` that throws ENOENT (the no-token-file state), a `create_noop_stub` DB, silent `Logger`, no-op `audit`, an empty `connection_closer`.
|
|
35
|
-
- `create_test_audit_emitter()` — no-op `AuditEmitter`; `emit` / `emit_role_grant_target` no-op, `emit_pool` resolves immediately, `notify` no-op, `add_listener` throws (use `create_recording_audit_emitter` for a listener-accepting emitter), `listener_count` returns 0, `drain_inflight` resolves immediately
|
|
35
|
+
- `create_test_audit_emitter()` — no-op `AuditEmitter`; `emit` / `emit_role_grant_target` no-op, `emit_pool` resolves immediately, `notify` no-op, `add_listener` throws (use `create_recording_audit_emitter` for a listener-accepting emitter), `listener_count` returns 0, `drain_inflight` resolves immediately, `is_tracking_inflight` returns `false`.
|
|
36
36
|
- `create_stub_audit_sse()` — no-op `AuditLogSse` for surface-test wiring without booting real SSE. `on_audit_event` no-op (nothing is broadcast); `required_role` is `admin`; `registry` is a fresh `SubscriberRegistry` (live `.count` / `.close_*` for registry-state tests, isolated per call). For real SSE plumbing build via `create_audit_log_sse` against `create_test_app`.
|
|
37
37
|
- `create_stub_api_middleware({include_daemon_token?, origin_carve_outs?})` — stub `MiddlewareSpec[]` matching `create_auth_middleware_specs`'s output (origin/session/request_context/bearer_auth, optional daemon_token) for surface generation without booting real auth — the same names, paths, order, and declared `errors` (`origin` declares a 403; `bearer_auth` and `daemon_token` declare none, since both soft-fail through `next()`), pinned against the real factory by `auth/middleware.mounted.test.ts`. The surface merges middleware errors into every route under the path, so a stub declaring more would widen the test surface with statuses the live server never returns. Each spec gets its own pass-through handler (not the shared `stub_mw`), so `assert_middleware_stack_mounted` tells the layers apart. See `auth/CLAUDE.md` §Middleware for the real stack.
|
|
38
38
|
- `create_stub_app_backend(options?)` — stub `AppBackend` with no database: `create_stub_app_deps()` (or `options.deps`), no migration results, a `close` that does nothing (or `options.close`). For assembling a server in tests that never reach the database.
|
|
@@ -79,6 +79,15 @@ across the auth test suite. Use for query-level tests needing real DB rows
|
|
|
79
79
|
but not a full session/token bundle. For tests also needing an API token +
|
|
80
80
|
session cookie + role_grants, use `bootstrap_test_keeper` from `app_server.ts`.
|
|
81
81
|
|
|
82
|
+
`create_test_extra_actor(db, account_id, name)` adds an actor to an
|
|
83
|
+
account — the multi-actor edge — with `created_at` read from the wall clock
|
|
84
|
+
(`clock_timestamp()`), not the transaction-fixed `NOW()` default. Actor lists
|
|
85
|
+
sort `created_at, id`, so a shared `created_at` would order them by random id;
|
|
86
|
+
seeded through it, the account's first actor sorts first and the extras in
|
|
87
|
+
insert order. `_testing_reset` and the in-process setup seed `extra_actors`
|
|
88
|
+
with it, as the Rust reset does with `query_create_actor_with_clock_timestamp`,
|
|
89
|
+
so both spines list a multi-actor keeper's actors in one order.
|
|
90
|
+
|
|
82
91
|
`create_test_role_grant_direct(db, input)` wraps `query_create_role_grant`
|
|
83
92
|
for tests needing an active role_grant seeded directly, bypassing the
|
|
84
93
|
production offer/accept consent flow. Use only when the test focuses on
|
|
@@ -103,6 +112,7 @@ mismatch.
|
|
|
103
112
|
### `audit_drift_guard.ts` — audit-emission validation
|
|
104
113
|
|
|
105
114
|
- `install_audit_drift_guard()` — `beforeEach` resets + `afterEach` zero-checks the `audit_metadata_validation_failures` + `audit_unknown_event_type_failures` counters from `auth/audit_log_queries.ts` and the emitter's write-failure counter (`get_audit_write_failures` in `auth/audit_emitter.ts`). Call once at the top of any `describe_db` block firing audit emits — production validation and the emitter's write are fail-open, so without this any regression shipping a typo'd `event_type` or undeclared metadata field, or losing a row the database refused, is silent. A test that fails a write on purpose resets the counter (`reset_audit_write_failures`) once it has asserted it.
|
|
115
|
+
- `assert_audit_event_metadata(event, {reject_unknown_keys?})` — the wire-side twin: parses a read audit row's `metadata` against its builtin event type's `audit_metadata_schemas` entry as declared, so a mistyped, `null`, or missing field fails while a consumer-added key passes (the schemas are `z.looseObject`, and a builtin's schema can't be overridden, so added keys are a consumer's only extension path). `reject_unknown_keys` closes the world: an undeclared key fails too, for a suite whose backend adds none. The wire schema leaves `metadata` a loose record and only the TS spine validates it, at insert and fail-open, so this is what holds every spine's rows to their event's contract. A consumer-registered event type passes unexamined. `list_audit_events` and the admin suite run it on every row they read, with `reject_unknown_keys` from the fixture's `reject_unknown_audit_metadata_keys` (a setup option on `default_cross_process_setup`, `default_in_process_setup` and `default_in_process_suite_options`); fuz_app's own spine wiring turns it on.
|
|
106
116
|
- `assert_no_audit_drift()` — the zero-check itself, for a test that manages the counters. Pair with `await_pending_effects: true` (the `create_test_app` default) so fire-and-forget audit writes complete by response time.
|
|
107
117
|
- `create_recording_audit_emitter(calls_ref?)` — no-op `AuditEmitter` pushing every `emit` and `emit_pool` call into `calls`. Pass `calls_ref` to write into a caller-owned array; omit to let the helper allocate. Returns `{emitter, calls}` — destructure `emitter` as the `audit` dep and read `calls` to assert. `add_listener` records (never fires) and returns a remover with the production semantics, so `listener_count` tracks what a server registered and released. Replaces per-file capturing emitters previously duplicated across `password_change.test.ts`, `audit_log.test.ts`, etc.
|
|
108
118
|
- `RecordingAuditEmitter` — `{emitter: AuditEmitter; calls: Array<AuditLogInput>}`.
|
|
@@ -295,6 +305,87 @@ assert_schema_snapshots_equal(snapshot_deno, snapshot_rust, { a: 'deno', b: 'rus
|
|
|
295
305
|
Each impl's _own_ tests still gate its DDL correctness independently — this
|
|
296
306
|
pair is purely the cross-impl drift check.
|
|
297
307
|
|
|
308
|
+
## Cross-impl differential
|
|
309
|
+
|
|
310
|
+
The behavioral twin of the two parity gates above: where they compare
|
|
311
|
+
declarations (DDL, the method set), this compares answers. Each impl is the
|
|
312
|
+
oracle for the other — a case the table pins by status alone, or an extra
|
|
313
|
+
field a loose output schema passes on one spine, shows up as a difference.
|
|
314
|
+
|
|
315
|
+
- `cross_backend/differential_normalize.ts` — `normalize_differential_response({status, headers, body})`
|
|
316
|
+
→ `DifferentialResponse` `{status, body, header_names}`. Volatile values
|
|
317
|
+
become positional placeholders per kind (`VOLATILE_VALUE_PATTERNS`: API
|
|
318
|
+
tokens, `tok_` token ids, lowercase UUIDs, `ISO8601_SECONDS` timestamps,
|
|
319
|
+
bare 64-hex digests — matched inside any string, object keys included): the
|
|
320
|
+
first UUID is `<uuid:1>`, a repeat of it `<uuid:1>` again, so identity
|
|
321
|
+
relations within one response survive while the values differ across
|
|
322
|
+
backends. Placeholders are positional, not tied to fixture identity, so two
|
|
323
|
+
ids that appear once each and trade places normalize equal. Only the
|
|
324
|
+
canonical spelling is placeholdered — an uppercase UUID or a millisecond
|
|
325
|
+
timestamp stays raw and differs — and a `blake3:` content address is
|
|
326
|
+
compared verbatim (equal bytes hash equal on both spines). Keys are sorted (and visited
|
|
327
|
+
in that order when numbering); array order is kept. `header_names` is the
|
|
328
|
+
sorted name set minus `VOLATILE_HEADER_NAMES` (framing and per-request
|
|
329
|
+
headers each server sets its own way); values are never compared —
|
|
330
|
+
`set-cookie` carries a fresh credential, and the contract header values are
|
|
331
|
+
the conformance table's `expect.headers` + no-fingerprint floor.
|
|
332
|
+
`diff_differential_responses(a, b)` → `DifferentialDiff[]`, one per leaf
|
|
333
|
+
path (`status`, `headers.<name>`, `body.error.data.reason`; an object one
|
|
334
|
+
side lacks diffs key by key, and an empty one against a missing one at its
|
|
335
|
+
own path), `differential_path_covers(covered, path)`,
|
|
336
|
+
`format_differential_diffs(diffs, labels)`.
|
|
337
|
+
- `cross_backend/differential.ts` —
|
|
338
|
+
`describe_differential_conformance_tests({cases, backends: [a, b],
|
|
339
|
+
surface_source, rpc_endpoints, live_rpc_endpoints?, session_options,
|
|
340
|
+
principals?, known_divergences?, suite_name?})`, each backend `{name,
|
|
341
|
+
setup_test, ws?}`; methods resolve as the table resolves them
|
|
342
|
+
(`resolve_conformance_rpc_endpoints`).
|
|
343
|
+
`ws` on both runs the cases over their WebSocket endpoints, filtered by
|
|
344
|
+
`is_ws_conformance_case`, with the ledger checked against what is left; on
|
|
345
|
+
one alone, registration throws. Per case, each backend's own `setup_test`
|
|
346
|
+
(separate databases and keepers), the principal resolved and the request
|
|
347
|
+
sent through `send_conformance_case` (`conformance_table.ts`), both answers
|
|
348
|
+
normalized and diffed. The per-case `expect` is not asserted —
|
|
349
|
+
each backend's conformance leg holds it. `KnownDivergence` is the ledger
|
|
350
|
+
row: `{case_name | method, tracking_id, reason, what_differs}`, the names a
|
|
351
|
+
string or a list, `what_differs` the diff paths covered (a path covers
|
|
352
|
+
everything under it), each a string or a `KnownDivergencePath`
|
|
353
|
+
`{path, a?, b?}` pinning the value a side shows there (`undefined` for
|
|
354
|
+
nothing). A pin is compared with the normalized leaf value at the differing
|
|
355
|
+
path, so a placeholdered value never matches the raw one, an object pin
|
|
356
|
+
must list its keys sorted, and a parent-path pin never matches. A case may
|
|
357
|
+
differ at the paths of the entries it matches, with their values, and
|
|
358
|
+
nowhere else. `xfail_until`'s turns-red semantics clean
|
|
359
|
+
the ledger: a case a `case_name` entry names must still show it (checked in
|
|
360
|
+
that case's test), a `method` entry must still show on one of its cases
|
|
361
|
+
(checked by a closing test once they all ran — a filtered or failed run
|
|
362
|
+
leaves it unjudged, and the test skips when nothing was judged), and an
|
|
363
|
+
entry matching no case, or listing a case name that doesn't exist or a
|
|
364
|
+
method no case sends, fails —
|
|
365
|
+
the static half is `check_known_divergence_ledger(cases, ledger)`, exported
|
|
366
|
+
for checking a ledger without a backend. No other ignore path exists.
|
|
367
|
+
|
|
368
|
+
fuz_app runs it under the `cross_backend_parity` project
|
|
369
|
+
(`differential.cross.test.ts`, its ledger `differential_known_divergences.ts`
|
|
370
|
+
in `src/test/cross_backend/`): every `conformance_*_cases.ts` array plus the
|
|
371
|
+
generated auth rows (over the live mount too, so the cell verbs, the actor
|
|
372
|
+
resolvers and the `_testing_*` backdoors are compared), TS spine vs
|
|
373
|
+
`testing_spine_stub`, both on Postgres so no engine difference reads as a body
|
|
374
|
+
difference. The same cases run again over
|
|
375
|
+
WS, against `differential_ws_known_divergences` (the HTTP ledger without its
|
|
376
|
+
header row). The multi-actor `conformance_acting_cases.ts` rows run with the
|
|
377
|
+
generated rows on a keeper seeded with `extra_actors`, over both transports.
|
|
378
|
+
The TS spine runs under `DEV` there, so its DEV-only diagnostics (Zod
|
|
379
|
+
`issues` on `invalid_params`, the `X-Fuz-Auth-Debug` header) are ledger rows.
|
|
380
|
+
The suite composition is `describe_spine_differential_suites`
|
|
381
|
+
(`src/test/cross_backend/spine_differential.ts`), which takes the two
|
|
382
|
+
backends and their ledgers. fuz_app also holds two TS runtimes to each other
|
|
383
|
+
with it: the `cross_backend_ts_differential` project
|
|
384
|
+
(`differential_ts.cross.test.ts`) dual-spawns the TS spine binary on Node and
|
|
385
|
+
on Bun, both on `memory://`, against an empty ledger — both run under `DEV`,
|
|
386
|
+
so the DEV-only rows show on both sides and differ nowhere. It needs no Rust
|
|
387
|
+
or Postgres and runs in the `cross-ts` CI job.
|
|
388
|
+
|
|
298
389
|
## Assertions, coverage, helpers
|
|
299
390
|
|
|
300
391
|
### `assertions.ts` — surface + error-schema assertions
|
|
@@ -383,11 +474,15 @@ Support functions:
|
|
|
383
474
|
|
|
384
475
|
Walks Zod schemas to generate valid values for adversarial/round-trip tests.
|
|
385
476
|
|
|
386
|
-
- `
|
|
387
|
-
- `
|
|
477
|
+
- `to_field_json_schema(field_schema)` — the field's JSON Schema rendered with `io: 'input'`, so a `.transform()` or pipe field (`Username`) yields its input-side bounds and pattern instead of throwing; a `.nullable()` / `.nullish()` field's one non-null `anyOf` branch is returned; `null` for a type JSON Schema can't represent. A pipe yields its input stage only, and a `.refine()` never appears (a code-point length bound generates nothing). Every JSON Schema read in the generators goes through it.
|
|
478
|
+
- `detect_format(field_schema)` — `detect_json_schema_format` over `to_field_json_schema`: the `format`, `'pattern'` for a regex, or `null`.
|
|
479
|
+
- `generate_valid_value(field, field_schema)` — base-type switch producing a valid sample (UUIDs → nil UUID, strings → `'xxxxxxxxxx'`, numbers → `1`, objects → `generate_valid_object` against the field schema, so a nested refinement is satisfied too, enums → first entry, etc.). For branded-string refinements, walks a fallback chain synthesized from the `pattern` string the JSON Schema representation exposes: fixed-length hex (`^[0-9a-f]{N}$` — blake3 / sha256 / md5 digests; `0`.repeat(N)), prefix-lengthed slug (`^<prefix>_[A-Za-z0-9_-]{N}$` — `ApiTokenId`-style ids; `<prefix>_` + `x`.repeat(N)), absolute path prefix, URL prefix, two `/`-joined segments (an `owner/name` slug). First candidate that `safeParse` accepts is used.
|
|
480
|
+
- `generate_valid_string_param(field, field_schema)` — a string a URL-sourced field (route param, query value) accepts: the nil UUID for a UUID field, else the first of `test_value`, the generated valid value as a string, `1`, `0`, `true`, `false`, the field's JSON Schema `default` or lower bound as a string (a preprocessed integer whose minimum is above one) and a synthesized string the field accepts; `null` when none does. The params and query case generators build their base from it.
|
|
388
481
|
- `resolve_valid_path(path, params_schema?)` — swaps `:param` for a value the params schema accepts. Prefers `test_<name>` wherever the field schema admits it (the param name keeps failure messages readable); otherwise a UUID param takes the nil UUID and anything else is synthesized from the schema, percent-encoded. A synthesized value containing `/` is refused (it would route elsewhere) and falls back to `test_<name>`.
|
|
389
482
|
- `to_route_content_type(route)` — the `Content-Type` a well-formed request to the route sends: `application/json`, or `application/octet-stream` for a `raw_body` route. Every `POST` route refuses anything else with 415 before its other gates, so every probe in the harnesses (auth, params and query fuzz, 404, data exposure, round trip) sends it, body or not.
|
|
390
|
-
- `
|
|
483
|
+
- `generate_valid_object(object_schema, schema) => {value, result}` — required and defaulted fields; when `schema` (refinements included) refuses those, each optional field alone in declaration order, the first that parses; failing that, the optional fields added cumulatively until it parses. Covers a `.refine()` over optional fields (at least one of, exactly one of, both of) at any depth, and, where one optional field suffices, keeps the others (an `acting` declared first) out. Returns the last `safeParse` result and leaves the throw to the caller. `format_zod_issues(error)` renders the issues for those errors.
|
|
484
|
+
- `generate_valid_object_with_field(object_schema, schema, base, field)` — a valid object carrying `field`, built from the valid `base`: `base` itself when it carries the field; else the field joins `base`, with the optional fields `base` omits filled as `generate_valid_object` fills them (a pair rule: `scope_kind` needs `scope_id`); failing that, the field replaces `base`'s optional fields, refilled around it (exactly one of: adding `slug` drops `id`). `null` when no object the generator builds carries the field. The body a single-fault case on a field the base omits changes.
|
|
485
|
+
- `generate_valid_body(input_schema) => Record<string, unknown> | undefined` — builds a body satisfying the input schema (`generate_valid_object`). Throws with Zod `issues` if the generated body fails validation — surfaces broken generation logic with a descriptive error rather than a confusing 400 downstream.
|
|
391
486
|
|
|
392
487
|
### `integration_helpers.ts` — route lookup + body checks
|
|
393
488
|
|
|
@@ -432,7 +527,7 @@ Also exported: `describe_adversarial_auth(options)` (group 3 on its own) and
|
|
|
432
527
|
`build_error_schema_lookup(specs, middleware_specs?)` (pre-built
|
|
433
528
|
`Map<string, RouteErrorSchemas>` for per-response validation).
|
|
434
529
|
|
|
435
|
-
### `adversarial_input.ts` — schema-walk payload generation
|
|
530
|
+
### `adversarial_input.ts` + `adversarial_input_cases.ts` — schema-walk payload generation
|
|
436
531
|
|
|
437
532
|
`describe_adversarial_input({build, roles})` — fires input body / params /
|
|
438
533
|
query validation failures at every route with correct-auth credentials so
|
|
@@ -440,11 +535,15 @@ validation middleware is actually exercised (not short-circuited by 401).
|
|
|
440
535
|
All cases expect 400 with one of `ERROR_INVALID_REQUEST_BODY` /
|
|
441
536
|
`_INVALID_JSON_BODY` / `_INVALID_ROUTE_PARAMS` / `_INVALID_QUERY_PARAMS`.
|
|
442
537
|
|
|
443
|
-
|
|
538
|
+
The generators live in the hono-free `adversarial_input_cases.ts`, so the
|
|
539
|
+
cross-process suites reuse them:
|
|
540
|
+
|
|
541
|
+
- `generate_input_test_cases(input_schema, options?)` → `InputTestCase[]` — whole-body structural (non-object, extra key when `strictObject`), missing required fields, one wrong-type per field, null for required non-nullable, one format violation per constrained field, and boundary cases read from the field's input-side JSON Schema (`to_field_json_schema` — through a transform or pipe, and under a nullable's `anyOf`): the empty string, one under `minLength` (above one; a prefix of the valid value) and one over `maxLength`; below `minimum`, above `maximum`, at an exclusive bound, zero and negative for a positive-only field, a non-integer for an integer field and a value off a `multipleOf` grid (each half a step from the valid value, inside the range); the empty array, one under `minItems` (above one; the valid value's own items) and one over `maxItems`. A per-field case is emitted only when the field refuses its value (a `z.coerce` field given a wrong type, a pattern admitting the violation string, a preprocess that repairs the value: dropped), so no generated case is accidentally valid. Each per-field case is single-fault — valid but for the field it breaks: it changes one field of a valid base (`generate_valid_object`'s, or `options.base` (`InputTestCaseOptions`) for an input whose valid shape the generator can't reach), or, for a field that base omits, of the body `generate_valid_object_with_field` builds around it, so a case can't also break an object-level rule. A field no valid body carries keeps its wrong-type and null cases on the base and gets no format or boundary cases. Either base failing the schema throws, naming `options.label` (the method or route) when given. A per-field case names the field it breaks (`InputTestCase.field`; absent on the whole-body cases).
|
|
542
|
+
- `expected_invalid_reason(input_schema, test_case)` → `string | undefined` — the `data.reason` an RPC refusal of the case owes: the broken field's `invalid_reason` (`schema_meta.ts`), resolved by the same `resolve_invalid_reason` the dispatcher resolves through, or none for an undeclared field or a whole-body case.
|
|
543
|
+
- `generate_params_test_cases(params_schema)` → `ParamsTestCase[]` — format violations only (unconstrained string params accept anything), each one the field refuses.
|
|
544
|
+
- `generate_query_test_cases(query_schema)` → `QueryTestCase[]` — missing required fields the field refuses to go without (a preprocessed field with a default accepts its absence) + format violations the field refuses.
|
|
444
545
|
|
|
445
|
-
|
|
446
|
-
- `generate_params_test_cases(params_schema)` — format violations only (unconstrained string params accept anything).
|
|
447
|
-
- `generate_query_test_cases(query_schema)` — missing required + format violations.
|
|
546
|
+
Both build their base from `generate_valid_string_param` and check it against the whole schema, throwing when a field has no string it accepts or the record fails, so each case breaks one field of a valid record (a URL-sourced integer such as `db_routes`' `offset` / `limit` gets `'1'`, not a `test_value` it refuses).
|
|
448
547
|
|
|
449
548
|
GET-with-input routes hit the RPC `?params=` query convention; invalid-JSON
|
|
450
549
|
arrays there collapse to `ERROR_INVALID_REQUEST_BODY` (schema failure)
|
|
@@ -509,9 +608,15 @@ the response against declared schemas. DB-backed via `create_test_app`.
|
|
|
509
608
|
Per-route test (`test.each`) — one line per route in the vitest output.
|
|
510
609
|
|
|
511
610
|
Options: `{setup_test, surface_source, skip_routes?, input_overrides?, success_fixtures?}`.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
611
|
+
Principals and the 401 check are the RPC round-trip's (`pick_auth_principal`;
|
|
612
|
+
`assert_route_authenticated` over `is_unauthenticated_refusal`, reading the
|
|
613
|
+
flat `error`), and a non-`GET` route called as the authed
|
|
614
|
+
account runs on a fresh account — a generated logout would otherwise end the
|
|
615
|
+
shared session, leaving `/password` and every later authed route a vacuous
|
|
616
|
+
401. `src/test/testing/round_trip.test.ts` pins both, the RPC test's twin.
|
|
617
|
+
`input_overrides` is a `Map<"METHOD /path", body>` — override generated bodies
|
|
618
|
+
for routes whose input schema can't round-trip cleanly (e.g. fields that must
|
|
619
|
+
reference DB state).
|
|
515
620
|
|
|
516
621
|
SSE routes are skipped by Content-Type sniff; `describe_sse_route_tests`
|
|
517
622
|
picks them up separately.
|
|
@@ -521,9 +626,62 @@ picks them up separately.
|
|
|
521
626
|
DB-backed round-trip for RPC: one POST test for all methods, one GET test
|
|
522
627
|
for `side_effects: false` methods. Successful responses validate against
|
|
523
628
|
`action.spec.output`; error responses validate as well-formed JSON-RPC error
|
|
524
|
-
envelopes.
|
|
525
|
-
|
|
526
|
-
|
|
629
|
+
envelopes. Three refusals fail instead of passing as an envelope: `method not
|
|
630
|
+
found` (the backend lacks a method the surface declares), a crash — HTTP
|
|
631
|
+
500, the `internal_error` code (-32603), or a ≥500 whose body is no JSON-RPC
|
|
632
|
+
envelope — and a 401 to a non-public call (`assert_rpc_authenticated`): the
|
|
633
|
+
call carried a credential its auth admits, so a 401 means it stopped working.
|
|
634
|
+
A public method may still answer 401 (a wrong-password login), and so may a
|
|
635
|
+
handler naming its own reason (below). A ≥500 carrying a declared code
|
|
636
|
+
(`service_unavailable` 503, `timeout` 504, a consumer code declared with
|
|
637
|
+
`register_jsonrpc_error_code`) is a server condition and passes like a
|
|
638
|
+
4xx. The failure names the method, status, code and message; a non-JSON body
|
|
639
|
+
is reported as text. Options: `{setup_test, surface_source, session_options,
|
|
640
|
+
rpc_endpoints, skip_methods?, input_overrides?, success_fixtures?}`.
|
|
641
|
+
`pick_rpc_auth` picks the principal from `method.auth` through
|
|
642
|
+
`pick_auth_principal` (`integration_helpers.ts`, shared with the REST
|
|
643
|
+
round-trip; `AuthPrincipal`): none for a public method, the keeper's daemon
|
|
644
|
+
token when `credential_types` names it, the admin account's session for an
|
|
645
|
+
admin-gated method, the keeper's session for another role, else the authed
|
|
646
|
+
account's session. The 401 check is `is_unauthenticated_refusal`: a 401
|
|
647
|
+
reading `authentication_required` or naming no reason fails; a handler's own
|
|
648
|
+
401 reason on an authenticated call (`invalid_credentials` for a wrong current
|
|
649
|
+
password) passes. So a handler that answers 401 to a valid credential must name
|
|
650
|
+
its reason — `data.reason` over RPC, the flat `error` over REST: a reason-less
|
|
651
|
+
401 reads as unauthenticated and fails the round-trip. TS's
|
|
652
|
+
`jsonrpc_errors.unauthenticated(message)` sends no `data` unless given one, so
|
|
653
|
+
pass `{reason}` as its second argument.
|
|
654
|
+
|
|
655
|
+
A side-effecting method picked for the authed account runs on a fresh account
|
|
656
|
+
of its own (`round_trip_uses_fresh_account`, keyed on the spec's
|
|
657
|
+
`side_effects`): generated params omit optional fields, and an omitted target
|
|
658
|
+
on a self-service method is the caller — `account_delete` with no
|
|
659
|
+
`account_id` deletes it, `account_session_revoke_all` ends its session — which
|
|
660
|
+
on the shared account would turn every later account method into a 401. Keyed
|
|
661
|
+
on `side_effects` rather than a list of known destroyers, so a consumer's
|
|
662
|
+
self-targeting method is covered without registering it. The keeper, its
|
|
663
|
+
daemon token and the admin account stay shared (one keeper per fixture, and
|
|
664
|
+
admin methods with generated nil ids target no existing account); a
|
|
665
|
+
credential they lose still fails on the 401 check.
|
|
666
|
+
`src/test/testing/rpc_round_trip.test.ts` pins both against a fake backend
|
|
667
|
+
whose side-effecting method ends the caller's session.
|
|
668
|
+
|
|
669
|
+
`success_fixtures` (`RpcSuccessFixtures`, method → `(fixture) =>
|
|
670
|
+
Promise<RpcSuccessFixtureCase>`) drives a **success** response for a method
|
|
671
|
+
whose generated nil ids can only refuse: the factory builds the row, then
|
|
672
|
+
returns `{params, headers?}` — `headers` replaces the picked identity, for a
|
|
673
|
+
row only its owner may act on. The response must be `ok`, not an internal
|
|
674
|
+
failure, and parse against `output`. `cross_backend/rpc_success_fixtures.ts`
|
|
675
|
+
— `create_spine_rpc_success_fixtures({rpc_path?})` builds the map for the
|
|
676
|
+
standard spine methods that act on an existing row (`account_token_revoke`,
|
|
677
|
+
`account_session_revoke`, `invite_delete`, `role_grant_revoke`,
|
|
678
|
+
`role_grant_offer_accept` / `_decline` / `_retract`) from standard RPC
|
|
679
|
+
actions and the builtin `ROLE_ADMIN` only, each factory on fresh accounts
|
|
680
|
+
and calling as one of them, so any consumer mounting
|
|
681
|
+
`create_standard_rpc_actions` can pass it. The fixtures run last on the
|
|
682
|
+
describe's fixture, since they mutate.
|
|
683
|
+
fuz_app wires it through the standard bundle and in the in-process
|
|
684
|
+
`cross_backend/rpc_round_trip_spine.db.test.ts`.
|
|
527
685
|
|
|
528
686
|
### `sse_round_trip.ts` — `describe_sse_route_tests`
|
|
529
687
|
|
|
@@ -666,7 +824,12 @@ lifecycle (via `role_grant_offer_create` + `role_grant_offer_accept` +
|
|
|
666
824
|
`role_grant_revoke` RPC flows — **not** REST, **not** direct
|
|
667
825
|
`query_accept_offer`; see `auth/CLAUDE.md` for
|
|
668
826
|
`role_grant_offer_action_specs.ts` + `role_grant_offer_actions.ts`),
|
|
669
|
-
session / token management, audit log reads (RPC
|
|
827
|
+
session / token management, audit log reads (RPC — every row read is
|
|
828
|
+
parsed by `assert_audit_event_metadata`, closed to undeclared keys when the
|
|
829
|
+
fixture asks, and the completeness test drives login, logout, the offer flow,
|
|
830
|
+
grant + revoke, token create, password change, and — each where the surface
|
|
831
|
+
mounts its actions — token revoke, session revoke, invite create + delete and
|
|
832
|
+
`app_settings_update`), admin-to-admin
|
|
670
833
|
isolation, error coverage, response schema validation.
|
|
671
834
|
|
|
672
835
|
The shared `role_grant_offer_and_accept` helper (`role_grant_helpers.ts`)
|
|
@@ -679,6 +842,12 @@ from `db_entities.ts`.
|
|
|
679
842
|
|
|
680
843
|
Required options: `{setup_test, surface_source, session_options, rpc_endpoints: RpcEndpointsSuiteOption, roles: RoleSchemaResult, admin_prefix?}`.
|
|
681
844
|
|
|
845
|
+
fuz_app runs it cross-process through the standard bundle
|
|
846
|
+
and in-process as a consumer wires it
|
|
847
|
+
(`cross_backend/admin_integration_spine.db.test.ts`) — the in-process fixture's
|
|
848
|
+
`create_account` inserts rows directly, writing no `invite_create` or `signup`
|
|
849
|
+
audit row, so the suite expects neither from it.
|
|
850
|
+
|
|
682
851
|
`rpc_endpoints` is `Array<RpcEndpointSpec> | ((ctx: AppServerContext) => Array<RpcEndpointSpec>)` —
|
|
683
852
|
the same `RpcEndpointsSuiteOption` union every DB-backed suite accepts
|
|
684
853
|
(`integration`, `admin_integration`, `audit_completeness`, `rate_limiting`,
|
|
@@ -763,6 +932,13 @@ invokes the right per-test fixture. Consumers wiring the standard surface
|
|
|
763
932
|
call once instead of seven times; forgetting a suite no longer silently
|
|
764
933
|
loses coverage.
|
|
765
934
|
|
|
935
|
+
`rpc_success_fixtures`, `rpc_skip_methods` and `rpc_input_overrides`
|
|
936
|
+
forward to the RPC round-trip's `success_fixtures`, `skip_methods` and
|
|
937
|
+
`input_overrides` (same options on `describe_standard_cross_process_tests`;
|
|
938
|
+
`describe_spine_cross_suites` also forwards `rpc_input_overrides` to
|
|
939
|
+
`rpc_adversarial`) — `rpc_skip_methods` is the consumer escape hatch for a
|
|
940
|
+
method the generic round-trip can't drive in its test wiring.
|
|
941
|
+
|
|
766
942
|
`StandardTestOptions` requires `create_route_specs` (for rate_limiting) and
|
|
767
943
|
`rpc_endpoints` (for admin/audit_completeness/rpc_round_trip); the admin
|
|
768
944
|
suite's requirement is enforced at the type level so a missing
|
|
@@ -810,6 +986,7 @@ One-shot transport:
|
|
|
810
986
|
- `RpcCallResult` — discriminated `{ok: true, status, headers, result}` / `{ok: false, status, headers, error: {code, message, data?}}`. `headers` is the lowercased-key response-header snapshot so callers can assert header-level properties (e.g. the conformance runner's no-fingerprint invariant).
|
|
811
987
|
- `headers_to_record(headers)` — snapshot a `Headers` into a lowercased-key `Record<string, string>` (multi-value collapses to the comma-joined form; `Set-Cookie` excluded). Used by `rpc_call` and the conformance table's REST branch to thread response headers through for assertions.
|
|
812
988
|
- `RpcCallArgs` — `{app, path, method, params?, headers?, id?, verb?}`. `verb` defaults to `'POST'`; use `'GET'` for `side_effects: false` methods.
|
|
989
|
+
- `rpc_call_raw(args)` — the same request as `rpc_call`, the response unparsed: `RpcCallRawResult` `{status, headers, body}` with the whole envelope. `parse_rpc_call_result(raw, method)` turns it into an `RpcCallResult`; `rpc_call` is the two composed.
|
|
813
990
|
- `rpc_call(args)` — merges `RPC_CALL_DEFAULT_HEADERS` (`host: 'localhost'`, `origin: 'http://localhost:5173'`, `Content-Type: 'application/json'`) under caller headers. Envelope-shape violations throw; JSON-RPC errors return `{ok: false, error}` so callers assert on `error.code` / `error.data.reason`.
|
|
814
991
|
- `rpc_call_typed<T>(args, output_schema)` — parses the success `result` through the schema; throws on envelope failure, error response, or schema mismatch. Use `rpc_call` when the test needs to assert on error shapes.
|
|
815
992
|
- `rpc_call_for_spec<TSpec>(args)` — spec-bound variant: takes `{..., spec, params}` in place of `{..., method, params}`. `params` is typed from `spec.input` and the success `result` is typed from `spec.output` (runtime-validated, same contract as `rpc_call_typed`). Error branch stays untyped (JSON-RPC `error.data` shapes vary per call site). Use at happy-path + denial-path call sites; fall back to `rpc_call` for adversarial tests that send deliberately-malformed params.
|
|
@@ -822,7 +999,7 @@ Registry lookups:
|
|
|
822
999
|
- `RpcEndpointsSuiteOption` — union `Array<RpcEndpointSpec> | ((ctx: AppServerContext) => Array<RpcEndpointSpec>)` accepted by every DB-backed suite's `rpc_endpoints` field.
|
|
823
1000
|
- `resolve_rpc_endpoints_for_setup(rpc_endpoints, session_options)` — resolves the union to an array for setup-time inspection (path lookup, `find_rpc_action` presence checks). Factory form is invoked once with a stub `AppServerContext`; the produced actions are discarded because `create_app_server` invokes the factory a second time per-test with its real ctx. Safe when the factory is pure wrt endpoint `path` and action `spec.method` list.
|
|
824
1001
|
|
|
825
|
-
### `rpc_attack_surface.ts` — `describe_rpc_attack_surface_tests`
|
|
1002
|
+
### `rpc_attack_surface.ts` + `rpc_envelope_cases.ts` — `describe_rpc_attack_surface_tests`
|
|
826
1003
|
|
|
827
1004
|
3 test groups for JSON-RPC endpoints:
|
|
828
1005
|
|
|
@@ -833,13 +1010,13 @@ Registry lookups:
|
|
|
833
1010
|
- **keeper rejects non-daemon credentials** — session and api_token credentials are rejected even when the account has the keeper role (only `daemon_token` passes). The credential-type gate fires before the role gate (see `auth/CLAUDE.md` §Keeper auth shape).
|
|
834
1011
|
- correct auth passes (not 401/403)
|
|
835
1012
|
- GET unauthenticated for `side_effects: false` reads
|
|
836
|
-
2. **RPC adversarial envelopes** — fixed set exercising
|
|
837
|
-
3. **RPC adversarial params** —
|
|
1013
|
+
2. **RPC adversarial envelopes** — the fixed set `create_rpc_envelope_cases(endpoint, {skip_methods?})` builds, exercising the envelope parse, method lookup, and GET read restriction: non-JSON body → `parse_error`, wrong `jsonrpc` version, missing `jsonrpc` / `method` / `id`, batch array, non-object POST `params` → `invalid_request`, unknown method → `method_not_found`, GET missing `method`/`id` → `invalid_request`, GET invalid-JSON params → `invalid_params`, GET non-object params on **every** read (each of `RPC_NON_OBJECT_PARAMS`: a number, an array, a string, a boolean) → `invalid_params`, GET mutation method → `invalid_request`, plus three ordering cases sent with no credential (an anonymous GET mutation and an anonymous batch, both `invalid_request`, never 401; an anonymous GET with non-object params on a read requiring an identity, the 401 — params validation runs after authorization). Each case is sent by the app its method's auth admits (`select_auth_app`) and pins status, code, echoed `id`, and `data.reason` (exact where the dispatcher sets one, absent otherwise) via `assert_rpc_envelope_refusal`. The cases are data (`RpcEnvelopeCase`) in the hono-free `rpc_envelope_cases.ts`, shared with the cross-process `describe_rpc_adversarial_cross_tests`; `to_rpc_envelope_request` builds a case's request, and `rpc_get_side_effects_reason(method)` is the GET read restriction's reason.
|
|
1014
|
+
3. **RPC adversarial params** — `generate_rpc_params_test_cases(input, options?)` (`rpc_envelope_cases.ts`): `generate_input_test_cases` minus its structural cases (a non-object `params` is refused at the envelope, not at params validation), taking the same `{base?, label?}`. Every case expects 400 `invalid_params` with `data.reason` the broken field's declared `invalid_reason` (`expected_invalid_reason`), absent for an undeclared field. Shared with the cross-process suite. `input_overrides` (method name → valid params) seeds a method's cases from that base; an override its schema refuses throws at collection, naming the method.
|
|
838
1015
|
|
|
839
1016
|
Skips silently when `surface.rpc_endpoints` is empty. Uses stub deps — no
|
|
840
1017
|
DB needed.
|
|
841
1018
|
|
|
842
|
-
Options: `{build: () => AppSurfaceSpec, roles: Array<string
|
|
1019
|
+
Options: `{build: () => AppSurfaceSpec, roles: Array<string>, input_overrides?}`.
|
|
843
1020
|
|
|
844
1021
|
**Opt-in bundles need their own per-bundle suite file.** Action bundles not
|
|
845
1022
|
folded into `create_standard_rpc_actions` (today `self_service_role_actions`,
|
|
@@ -874,10 +1051,17 @@ test-server core below (fuz_app's own domain-free `testing_spine_server`, run
|
|
|
874
1051
|
on Node + Deno + Bun). In-process is the fast feedback path; cross-process is the
|
|
875
1052
|
source of truth for wire-shape conformance.
|
|
876
1053
|
|
|
1054
|
+
fuz_app's own spines run every single-backend suite below through one
|
|
1055
|
+
`describe_spine_cross_suites` call, `src/test/cross_backend/spine_suites.cross.test.ts`
|
|
1056
|
+
(§`cross_backend/spine_suites.ts`), so a suite's section names the suite, not a
|
|
1057
|
+
file; the dual-spawn and limiter-enabled suites name their own files.
|
|
1058
|
+
|
|
877
1059
|
### Fixture protocol + capabilities
|
|
878
1060
|
|
|
879
1061
|
- `testing/cross_backend/setup.ts` — `SetupTest` / `TestFixture` /
|
|
880
|
-
`
|
|
1062
|
+
`CreateTestAccountOptions` types (`fixture.create_account` and
|
|
1063
|
+
`fixture.extra_accounts` hand out `TestAccount`s from `app_server.ts`, built
|
|
1064
|
+
by `to_test_account`),
|
|
881
1065
|
`default_in_process_setup(options)` (wraps `create_test_app`; pass
|
|
882
1066
|
`migration_namespaces` for suites needing tables beyond the auth-only
|
|
883
1067
|
default — the cell parity suite passes `[CELL_MIGRATION_NS]`, and
|
|
@@ -913,7 +1097,11 @@ source of truth for wire-shape conformance.
|
|
|
913
1097
|
lands with an email set — the hook the username-or-email login parity suite
|
|
914
1098
|
uses; the in-process path sets it directly on the inserted row. Must be a
|
|
915
1099
|
valid, per-test-unique `Email` (the `LOWER(email)` partial-unique index
|
|
916
|
-
rejects collisions).
|
|
1100
|
+
rejects collisions). The signup and login it drives go to the setup's
|
|
1101
|
+
`signup_path` / `login_path` (default `SPINE_ACCOUNT_SIGNUP_PATH` /
|
|
1102
|
+
`SPINE_ACCOUNT_LOGIN_PATH`, from `spine_surface_constants.ts`, which also
|
|
1103
|
+
carries the other account routes, `SPINE_WS_PATH`, and the prefix).
|
|
1104
|
+
Caller-supplied `username`s pass through _as-is_ now that the DB
|
|
917
1105
|
wipes between tests — hardcoded names like `'user_two'` work and the
|
|
918
1106
|
earlier uniquification prefix is gone. Every `TestFixture` also exposes
|
|
919
1107
|
`fresh_transport({origin?: string | null})` — cookie-jar-free probe;
|
|
@@ -930,22 +1118,34 @@ source of truth for wire-shape conformance.
|
|
|
930
1118
|
|
|
931
1119
|
- `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
|
|
932
1120
|
(the **gating** flags `ws` / `sse` / `cell_crud` / `cell_relations` /
|
|
933
|
-
`cell_gated_create` / `account_lifecycle` / `fact_serving` /
|
|
1121
|
+
`cell_gated_create` / `account_lifecycle` / `fact_serving` /
|
|
1122
|
+
`fact_serving_admin_hash` / `ready` /
|
|
934
1123
|
`account_status` / `peer_request` /
|
|
935
1124
|
`ws_handshake_pipelining` / `ws_account_actions` /
|
|
936
1125
|
`ws_role_gated` / `ws_refusal_body` — each has a `test_if`
|
|
937
1126
|
reader (`ws_refusal_body` picks how the front-door table reads a WS
|
|
938
1127
|
refusal instead); `peer_request` (server-initiated requests — the ActionPeer
|
|
939
|
-
`peer/ping` round-trip) is `true`
|
|
940
|
-
(`BackendWebsocketTransport.request_connection`);
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1128
|
+
`peer/ping` round-trip) is `true` on fuz_app's Rust and TS spine presets
|
|
1129
|
+
(`BackendWebsocketTransport.request_connection`); a backend states it
|
|
1130
|
+
`true` once it wires the `peer/ping` HTTP + WS mount), `test_if(cond, name, fn)`
|
|
1131
|
+
for capability-gated cases, and `in_process_capabilities` preset. Only
|
|
1132
|
+
flags a suite reads are capabilities — a wiring fact that gates nothing is
|
|
1133
|
+
not recorded. **Mount flags have no family default**: `MountFlag` names the
|
|
1134
|
+
flags that describe a backend's own mounts (`ws`, `sse`, `cell_crud`,
|
|
1135
|
+
`cell_relations`, `cell_gated_create`, `fact_serving`,
|
|
1136
|
+
`fact_serving_admin_hash`, `ready`,
|
|
1137
|
+
`peer_request`, `ws_account_actions`, `ws_role_gated`), and the family
|
|
1138
|
+
defaults (`ts_default_capabilities` / `rust_default_capabilities`, typed
|
|
1139
|
+
`FamilyDefaultCapabilities`) omit them and the `testing_backdoors` list (the
|
|
1140
|
+
`_testing_*` backdoors the backend mounts, from `TESTING_BACKDOOR_METHODS` —
|
|
1141
|
+
§Testing-backdoor declaration below; `[]` in-process, every one on fuz_app's
|
|
1142
|
+
spine presets), with `capabilities` required on
|
|
1143
|
+
`make_default_{ts,rust}_backend_config` — so a config that leaves one out
|
|
1144
|
+
fails typecheck naming it, rather than silently skipping its suites. The
|
|
1145
|
+
family defaults carry the declared surface's mounts (`account_lifecycle`,
|
|
1146
|
+
`account_status`) and the runtime-behavior flags (`ws_handshake_pipelining`,
|
|
1147
|
+
`ws_refusal_body`) of the family's reference runtime — Node for TS, hyper
|
|
1148
|
+
for Rust; a backend on another runtime (Deno, Bun) overrides them. `cell_crud`
|
|
949
1149
|
gates the CRUD parity suite, `cell_relations` the relation / ACL / audit
|
|
950
1150
|
parity suite — both `true` on every backend that live-mounts the full cell
|
|
951
1151
|
surface (TS spine binary, in-process app, Rust stub). A backend mounting only
|
|
@@ -965,15 +1165,19 @@ source of truth for wire-shape conformance.
|
|
|
965
1165
|
auto-enumerated for shape/auth) — the flag gates the behavioral parity the
|
|
966
1166
|
generic round-trip can't drive (it can't delete the subject). `true` on every
|
|
967
1167
|
spine. `fact_serving`
|
|
968
|
-
gates `describe_fact_serving_cross_tests` (the cell-scoped per-reference
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1168
|
+
gates `describe_fact_serving_cross_tests` (the cell-scoped per-reference
|
|
1169
|
+
fact-serving parity suite, incl. the served fact's hardening headers and
|
|
1170
|
+
bodyless masked 404s); like cells it stays off the declared surface and is
|
|
1171
|
+
`true` on every spine that mounts the cell-gated serve route +
|
|
972
1172
|
the `_testing_put_fact` seeder (whose optional `holder` records a
|
|
973
1173
|
`fact_holder` row, so a non-admin can reference what it seeds, and whose
|
|
974
1174
|
optional `external_url` writes a bare external row naming that URL, so the
|
|
975
1175
|
suite can pin that a URL naming another hash's file is refused) — it also
|
|
976
|
-
gates `describe_cell_ref_admission_cross_tests`. `
|
|
1176
|
+
gates `describe_cell_ref_admission_cross_tests`. `fact_serving_admin_hash`
|
|
1177
|
+
is the admin-only bare-hash route (`GET /api/facts/:hash`), its own flag
|
|
1178
|
+
because a backend can serve the cell-gated route alone: it gates the
|
|
1179
|
+
fact-serving suite's bare-hash cases (with `fact_serving`) and the bare-hash
|
|
1180
|
+
cases of the query-shape and token-scope-surface suites. `ready` gates `describe_ready_cross_tests`
|
|
977
1181
|
(anonymous `GET /ready` → `200 {ready: true}` on a clean spine bootstrap);
|
|
978
1182
|
like cells/sse the `/ready` deploy gate stays off the declared surface, `true`
|
|
979
1183
|
on every spine that live-mounts it over the shared `expected_schema.json`.
|
|
@@ -993,7 +1197,7 @@ source of truth for wire-shape conformance.
|
|
|
993
1197
|
as it does behind auth, and `false` in-process, where there is no socket to
|
|
994
1198
|
write raw bytes to).
|
|
995
1199
|
`ws_account_actions` (the WS endpoint mounts the account actions — `true` on
|
|
996
|
-
fuz_app's own spine presets
|
|
1200
|
+
fuz_app's own spine presets; a consumer states it like every mount flag) gates the WS
|
|
997
1201
|
round-trip suite's self-revocation case (which requires the reply ahead of
|
|
998
1202
|
the close on every backend) and, with `rpc_path`, its WS-mutation audit case
|
|
999
1203
|
(which calls `_testing_drain_effects`).
|
|
@@ -1026,6 +1230,171 @@ consumer needs partial opt-out, add the knob then.
|
|
|
1026
1230
|
`bootstrap`, `rate_limiting_app_options`, `bootstrap_token`) — those drive
|
|
1027
1231
|
the omitted suites.
|
|
1028
1232
|
|
|
1233
|
+
### `cross_backend/spine_suites.ts` — `describe_spine_cross_suites`
|
|
1234
|
+
|
|
1235
|
+
One call that runs every shipped single-backend cross suite against one
|
|
1236
|
+
spawned backend: `describe_spine_cross_suites(handle, {surface_source,
|
|
1237
|
+
session_options, rpc_endpoints, live_rpc_endpoints?, roles?, setup?, exclude?,
|
|
1238
|
+
exclude_cases?, overrides?, rpc_success_fixtures?, rpc_skip_methods?, rpc_input_overrides?,
|
|
1239
|
+
rpc_max_body_bytes?, ws_max_message_bytes?})`.
|
|
1240
|
+
`live_rpc_endpoints` is the backend's whole live mount; given, `rpc_adversarial`
|
|
1241
|
+
also walks the methods it keeps off the declared surface. `rpc_input_overrides`
|
|
1242
|
+
reaches the standard bundle's RPC round-trip and `rpc_adversarial`'s
|
|
1243
|
+
`input_overrides`, so one valid body per method serves both; a key naming a
|
|
1244
|
+
method on neither the declared surface nor `live_rpc_endpoints` throws at
|
|
1245
|
+
collection. Without it a consumer writes a file per suite, and a suite added
|
|
1246
|
+
here never reaches it.
|
|
1247
|
+
|
|
1248
|
+
- **The registry.** `SUITE_NAMES` / `SuiteName` — the closed set of suites the
|
|
1249
|
+
call runs, each named by its module (`ws_round_trip` is
|
|
1250
|
+
`describe_cross_process_ws_tests`, `standard` the standard bundle).
|
|
1251
|
+
`STANDALONE_CROSS_SUITES` (keyed by the closed `StandaloneSuiteName`) names
|
|
1252
|
+
the modules left out, each with why: the
|
|
1253
|
+
`differential` (two handles), `login_security` and `ws_action_rate_limit`
|
|
1254
|
+
(a backend spawned with live limiters, which the standard suites would
|
|
1255
|
+
exhaust), and the `conformance_table` runner (the consumer's own cases).
|
|
1256
|
+
`src/test/testing/cross_backend/spine_suites.test.ts` imports every module
|
|
1257
|
+
under `cross_backend/` and holds the two lists to the ones exporting a
|
|
1258
|
+
`describe_*` function, whatever the export form (the Bun and Deno adapters,
|
|
1259
|
+
which load only on their own runtime, are held to naming no `describe_` at
|
|
1260
|
+
all), so a new suite fails until it is registered or stated standalone; the
|
|
1261
|
+
dispatch table
|
|
1262
|
+
is a `Record<SuiteName, …>`, so a registered name without an entry fails
|
|
1263
|
+
typecheck.
|
|
1264
|
+
- **Options from the handle.** Paths, cookie name and capabilities come from
|
|
1265
|
+
`handle.config` — `to_cross_suite_options(handle)`, exported for a suite
|
|
1266
|
+
called on its own — the daemon token from the handle, and each suite's
|
|
1267
|
+
`setup_test` from `default_cross_process_setup` over the `setup` options
|
|
1268
|
+
plus the seeding the suite needs (concatenated): a keeper-only
|
|
1269
|
+
`non_admin_keeper` for the standard bundle's keeper ≠ admin probe, the
|
|
1270
|
+
multi-actor keepers of `fact_serving` and `transport_composition`, the
|
|
1271
|
+
offer grantor of `role_grant_offer_enumeration`, the `cell_editor` holder of
|
|
1272
|
+
`cell_grant_role`. Suites are registered without a wrapping `describe`, so
|
|
1273
|
+
test names match a standalone call.
|
|
1274
|
+
- **`ws_required_roles`** — the roles the backend's WS upgrade on `ws_path`
|
|
1275
|
+
requires (an admin-gated `/api/ws`: `[ROLE_ADMIN]`). The suites that open
|
|
1276
|
+
sockets for accounts they create (`credential_deadline`, `session_cap`,
|
|
1277
|
+
`ws_round_trip`) create them holding these (`ws_account_roles`), and
|
|
1278
|
+
`role_grant_offer_notification_ws` expects revoking one to close the socket
|
|
1279
|
+
(its `ws_required_roles`) — one option for every socket a role gates.
|
|
1280
|
+
- **`rpc_max_body_bytes`** / **`ws_max_message_bytes`** — the backend's
|
|
1281
|
+
request-body cap on `rpc_path` and its inbound WS message cap, when they
|
|
1282
|
+
aren't the spine defaults (`SPINE_BODY_LIMIT_DEFAULT_BYTES`,
|
|
1283
|
+
`DEFAULT_WS_MAX_MESSAGE_BYTES`). The first reaches `body_size`,
|
|
1284
|
+
`body_size_smuggling` and `transport_composition`'s RPC over-cap row (each
|
|
1285
|
+
takes it as `rpc_max_body_bytes`), the second `ws_round_trip`'s
|
|
1286
|
+
`max_message_bytes` — one value per limit for every suite probing it.
|
|
1287
|
+
- **`exclude`** — `{[suite]: reason}`; the reason is required and shows in the
|
|
1288
|
+
run as a skipped test. An empty reason, an unknown suite name, a suite
|
|
1289
|
+
both excluded and overridden, `testing_backdoor` (it checks the backdoor
|
|
1290
|
+
declaration the gating reads), or a suite already skipping itself for an
|
|
1291
|
+
undeclared backdoor throws before anything registers.
|
|
1292
|
+
- **Backdoors.** `capabilities.testing_backdoors` must declare
|
|
1293
|
+
`SPINE_SUITES_REQUIRED_BACKDOORS` (`_testing_reset`, every setup's per-test
|
|
1294
|
+
reset, and `_testing_drain_effects`, the audit-read barrier), and
|
|
1295
|
+
`_testing_put_fact` when `fact_serving` is declared; the call throws
|
|
1296
|
+
otherwise. A suite every case of which drives another backdoor
|
|
1297
|
+
(`SuiteEntry.backdoors`: `credential_deadline` →
|
|
1298
|
+
`_testing_shorten_credential_expiry`, `origin_conformance` →
|
|
1299
|
+
`_testing_origin_check`, `ws_outbound_overflow` →
|
|
1300
|
+
`_testing_flood_notifications`) skips itself on a backend not declaring it,
|
|
1301
|
+
as a skipped test beside the excludes whose reason names the backdoor; an
|
|
1302
|
+
exclude, override or `exclude_cases` entry for it throws as dead.
|
|
1303
|
+
`spine_suites.test.ts` holds the backdoors each suite module names — quoted
|
|
1304
|
+
`_testing_*` names, `testing_*_action_spec` references, and the
|
|
1305
|
+
`cross_put_fact` / `cross_drain_effects` seeders — to its entry's
|
|
1306
|
+
`backdoors`, the required pair, or `_testing_put_fact` under
|
|
1307
|
+
`capabilities.fact_serving`; a helper module the suite calls isn't read.
|
|
1308
|
+
- **`exclude_cases`** — `SuiteCaseExclusions`, `{[suite]: {[name path]:
|
|
1309
|
+
reason}}`: single tests of a suite that runs, each by its name path (the
|
|
1310
|
+
describe titles and the test name joined with `' > '` — what the reporter
|
|
1311
|
+
prints after the file and any `describe` wrapping the call; a full path, not
|
|
1312
|
+
a leaf name, so a title repeated under two describes names one test). One
|
|
1313
|
+
`beforeEach` on the calling suite (`register_spine_case_exclusions`) skips
|
|
1314
|
+
them at run time with `ctx.skip(reason)`, so the suite files stay as they
|
|
1315
|
+
are and each shows as skipped with its reason; it finds a test's suite
|
|
1316
|
+
from the top-level task ranges each suite registered (read through
|
|
1317
|
+
`TestRunner.getCurrentSuite()` at collection), so an entry matches only
|
|
1318
|
+
below its own suite. A reasonless entry, an empty suite map, or an entry for
|
|
1319
|
+
an unknown, excluded or self-skipping suite, or for `testing_backdoor`,
|
|
1320
|
+
throws at collection; an entry
|
|
1321
|
+
naming no test its suite collected fails the closing test
|
|
1322
|
+
(`check_spine_case_exclusions`, which reads the collected paths, so a
|
|
1323
|
+
filtered run still checks them) — the dead-name check, as the known-divergence
|
|
1324
|
+
ledger has.
|
|
1325
|
+
- **`overrides`** — `SuiteOverrides`, per suite a `Partial` of its own options
|
|
1326
|
+
(`SuiteOptionsMap`), shallow-merged over the derived ones: a role list, an
|
|
1327
|
+
`xfail`, a narrower capability set, `ws: undefined` to drop optional socket
|
|
1328
|
+
cases (`session_cap` and `testing_backdoor` gate no case on `capabilities.ws`,
|
|
1329
|
+
so `ws` is their only socket switch), `extra_methods` to fire a consumer's own
|
|
1330
|
+
daemon-token seams through `testing_backdoor` (whose
|
|
1331
|
+
`capabilities.testing_backdoors` an override can't change — the call throws
|
|
1332
|
+
unless the merge keeps the backend's list). An entry can instead be a function of the derived
|
|
1333
|
+
options returning the patch (`SuiteOverride`), for a change built on a
|
|
1334
|
+
derived value — an `xfail` added to the derived multi-actor setup — so the
|
|
1335
|
+
seeding isn't restated. After the merge, every option the suite's type
|
|
1336
|
+
requires (each entry's `required` set, exhaustive by type) must be neither
|
|
1337
|
+
`undefined` nor `''`, or the call throws at collection naming suite and key.
|
|
1338
|
+
- **Inputs the handle doesn't carry.** The declared surface is a required
|
|
1339
|
+
option. Paths and roles that name the reference spines' mounts (the
|
|
1340
|
+
role-gated WS endpoint and its roles) default to the `SPINE_*` constants —
|
|
1341
|
+
a suite whose flag is off never reads them, and a backend mounting its own
|
|
1342
|
+
overrides them. Anything else comes from the config when it can
|
|
1343
|
+
(`session_cookie_codec`'s `cookie_keys` from `SECRET_FUZ_COOKIE_KEYS`), else
|
|
1344
|
+
from `overrides`; with neither, the call throws naming the suite and the
|
|
1345
|
+
field — provide it or exclude the suite.
|
|
1346
|
+
|
|
1347
|
+
Each suite reads its own capability flags, so a backend's truthful
|
|
1348
|
+
`BackendConfig.capabilities` decides what runs. The role-shaped suites default
|
|
1349
|
+
to the spine's own roles — `cell_grant_role`'s `cell_editor`, the
|
|
1350
|
+
`participant` `role_grant_offer_lifecycle` offers and
|
|
1351
|
+
`role_grant_participation` confers — and take another through `suite_roles`
|
|
1352
|
+
(`SpineSuiteRoles`: `cell_grant`, `conferral`), which the call also seeds on
|
|
1353
|
+
the holder each needs (set it there, not in `overrides`, which would leave the
|
|
1354
|
+
seed on the default). A backend with only the builtin roles passes
|
|
1355
|
+
`{cell_grant: ROLE_KEEPER, conferral: ROLE_ADMIN}`: keeper reach is not admin
|
|
1356
|
+
reach, so every `cell_grant_role` arm still turns on the grant, while an admin
|
|
1357
|
+
views and edits every cell — `describe_cell_grant_role_cross_tests` refuses
|
|
1358
|
+
`ROLE_ADMIN` — and the scoped-assign case of `role_grant_participation` skips
|
|
1359
|
+
for a builtin. fuz_app's own wiring is
|
|
1360
|
+
`src/test/cross_backend/spine_suites.cross.test.ts`.
|
|
1361
|
+
|
|
1362
|
+
### `cross_backend/rust_spine_consumer_surface.ts` — `create_rust_spine_consumer_surface_spec`
|
|
1363
|
+
|
|
1364
|
+
The declared surface of a consumer served by the Rust spine, for its cross
|
|
1365
|
+
suites: `create_rust_spine_consumer_surface_spec({session_options,
|
|
1366
|
+
domain_action_specs, extra_rpc_actions?, roles?, exclude_methods?,
|
|
1367
|
+
rpc_path?, host_gate?})`, built on `create_test_app_surface_spec` from the hono-free route
|
|
1368
|
+
shapes. It declares `/health`, the `ACCOUNT_ROUTER_PATHS` under
|
|
1369
|
+
`/api/account` (`/status`, `/login`, `/logout`, `/password` — what
|
|
1370
|
+
`fuz_auth::account_router` mounts, so the TS-only `/verify` stays off), signup,
|
|
1371
|
+
bootstrap as a `surface_only` shape, the daemon-token layer the consumer test
|
|
1372
|
+
binaries wire, a `host_validation` stub layer at the head of the middleware
|
|
1373
|
+
list when `host_gate` says the server turns on the spine's opt-in host gate
|
|
1374
|
+
(default off), and one RPC endpoint: the standard actions (over `roles`), the
|
|
1375
|
+
`extra_rpc_actions` (opt-in families built against the stub context), then the
|
|
1376
|
+
request-response, non-protocol `domain_action_specs` minus `exclude_methods`
|
|
1377
|
+
(methods the spec-derived suites can't drive; an entry naming no domain spec
|
|
1378
|
+
throws). `create_rust_spine_consumer_rpc_endpoints(options)` is that endpoint
|
|
1379
|
+
alone, for `describe_spine_cross_suites`'s `rpc_endpoints`. Handlers are
|
|
1380
|
+
no-ops — live dispatch happens in the spawned binary. A server mounting no
|
|
1381
|
+
signup router (a single-keeper daemon) is not this shape.
|
|
1382
|
+
|
|
1383
|
+
### `cross_backend/provided_context.ts` — `inject_backend_handle`
|
|
1384
|
+
|
|
1385
|
+
Augments vitest's `ProvidedContext` with the keys the shipped global setups
|
|
1386
|
+
provide — `backend_handle` (`create_cross_backend_global_setup`) and
|
|
1387
|
+
`parity_handle_a` / `parity_handle_b` (`create_dual_spawn_global_setup`'s
|
|
1388
|
+
default keys) — and reads them: `inject_backend_handle()` /
|
|
1389
|
+
`inject_parity_handles()` return the handles rebuilt by
|
|
1390
|
+
`reconstruct_bootstrapped_handle`. The augmentation ships in the module's
|
|
1391
|
+
`.d.ts`, so a test file importing either helper gets the typed `inject` with
|
|
1392
|
+
no augmentation of its own — a type-only import of the module carries it too,
|
|
1393
|
+
and a global setup built on the shipped makers needs none. A project providing other
|
|
1394
|
+
keys — a dual spawn under its own `provide_keys`, a probe result — augments
|
|
1395
|
+
`ProvidedContext` for those itself; fuz_app's are in
|
|
1396
|
+
`src/test/cross_backend/cross_test_types.ts`.
|
|
1397
|
+
|
|
1029
1398
|
### `cross_backend/conformance_table.ts` + `conformance_case.ts` + `xfail.ts` — declarative behavioral/security cases
|
|
1030
1399
|
|
|
1031
1400
|
The opinionated behavioral/security layer on top of the spec-derived
|
|
@@ -1036,7 +1405,7 @@ conformance cases assert _expected behavior_ — the security negatives
|
|
|
1036
1405
|
wire-shape check passes green on even when behavior is wrong.
|
|
1037
1406
|
|
|
1038
1407
|
- `conformance_case.ts` — `ConformanceCase` Zod schema:
|
|
1039
|
-
`{name, request: {method, params?, as, verb?}, expect: {status,
|
|
1408
|
+
`{name, request: {method, params?, as, acting?, verb?}, expect: {status,
|
|
1040
1409
|
error_reason?, fields?, headers?, equivalence_group?}, note?, xfail?}`. A case is **data** — `method`
|
|
1041
1410
|
resolves its `input`/`output` from the live registry (RPC) or `RouteSpec`
|
|
1042
1411
|
(the 6 REST auth routes), so the case never carries a schema. `as` is the
|
|
@@ -1069,10 +1438,17 @@ error_reason?, fields?, headers?, equivalence_group?}, note?, xfail?}`. A case i
|
|
|
1069
1438
|
`expired_session` is the keeper behind an expired server-side session
|
|
1070
1439
|
(`fixture.mint_expired_session()`: a backdated `auth_session` row behind a
|
|
1071
1440
|
still-valid signed cookie, so the DB-row expiry gate is what refuses it).
|
|
1441
|
+
`acting` (`ConformanceActingBinding`) has the runner fill `params.acting`
|
|
1442
|
+
with a real actor id: `principal` (the actor of the account the
|
|
1443
|
+
principal's credential names), `extra_actor` (`fixture.extra_actors[0]`),
|
|
1444
|
+
`keeper_actor` (`fixture.actor`, foreign to every non-keeper principal);
|
|
1445
|
+
params must then be an object or omitted, without an `acting` of their own,
|
|
1446
|
+
and a principal presenting no account throws.
|
|
1072
1447
|
`error_reason` is the imported
|
|
1073
1448
|
`ERROR_*` constant (asserted against the RPC `error.data.reason` or the
|
|
1074
|
-
REST flat-body `error`; the
|
|
1075
|
-
|
|
1449
|
+
REST flat-body `error`; the dispatcher's 401 carries
|
|
1450
|
+
`authentication_required`, and a denial with no reason — a handler's bare
|
|
1451
|
+
`forbidden` — is pinned by `status`). `equivalence_group` is the
|
|
1076
1452
|
**negative-space** axis: every case tagged with the same group string must
|
|
1077
1453
|
produce a **byte-identical** normalized response (`{status, body}`),
|
|
1078
1454
|
asserted per impl after the case loop. It promotes a masked pair
|
|
@@ -1091,14 +1467,76 @@ error_reason?, fields?, headers?, equivalence_group?}, note?, xfail?}`. A case i
|
|
|
1091
1467
|
backend-fingerprinting oracle). Headers are deliberately kept out of the
|
|
1092
1468
|
`equivalence_group` `{status, body}` comparison (`Set-Cookie` / `Date` vary).
|
|
1093
1469
|
- `conformance_table.ts` — `describe_conformance_table_tests({cases,
|
|
1094
|
-
setup_test, surface_source, capabilities, rpc_endpoints,
|
|
1095
|
-
principals?, suite_name?})`.
|
|
1470
|
+
setup_test, surface_source, capabilities, rpc_endpoints, live_rpc_endpoints?,
|
|
1471
|
+
session_options, principals?, ws?, suite_name?})`. A case's RPC method
|
|
1472
|
+
resolves from `rpc_endpoints` first, then from `live_rpc_endpoints` — the
|
|
1473
|
+
endpoints the backend live-mounts, for methods kept off the declared surface
|
|
1474
|
+
(cell verbs, opt-in resolvers, `_testing_*` backdoors); a backend's whole
|
|
1475
|
+
live mount is a fine value, since a declared method still resolves from the
|
|
1476
|
+
declared surface. `resolve_conformance_rpc_endpoints({rpc_endpoints,
|
|
1477
|
+
live_rpc_endpoints?, session_options})` (exported) builds that lookup list,
|
|
1478
|
+
for the table and the differential alike. fuz_app's spine value is
|
|
1479
|
+
`create_spine_live_rpc_endpoints()` (`full_spine_mount.ts`), the binary's
|
|
1480
|
+
whole mount built on stub deps for resolution only. Same `{setup_test, surface_source,
|
|
1096
1481
|
capabilities}` protocol every Tier 1 suite uses, so **one case array runs
|
|
1097
1482
|
both transports** — in-process (`gro test`) and cross-process (the gate,
|
|
1098
|
-
each backend's real auth resolution). `
|
|
1483
|
+
each backend's real auth resolution). `resolve_conformance_principal` (exported) maps the five
|
|
1099
1484
|
always-available principals to fixture accessors; `role_holder` /
|
|
1100
1485
|
`wrong_role` read a seeded `extra_accounts` username named via
|
|
1101
|
-
`options.principals`.
|
|
1486
|
+
`options.principals`. `send_conformance_case(c, {fixture, surface_source, principals?,
|
|
1487
|
+
resolved_rpc_endpoints})` (exported) is the send half — principal resolved,
|
|
1488
|
+
request sent, the raw `{target, status, headers, body}` back unasserted; the
|
|
1489
|
+
table asserts on it, and the cross-backend differential compares two.
|
|
1490
|
+
**WS leg:** `ws: WsEndpointTarget` (`{base_url, ws_path, origin?}`) on
|
|
1491
|
+
either sends each case as one frame on an admitted socket the principal
|
|
1492
|
+
opens — its headers on the upgrade, no `Origin` where the HTTP leg
|
|
1493
|
+
suppresses it, its own actor as `?acting=`. Only the TS spine resolves an
|
|
1494
|
+
actor at the upgrade, refusing a multi-actor account's upgrade without
|
|
1495
|
+
`?acting=` (400 `actor_required`); the Rust spine's upgrade resolves the
|
|
1496
|
+
account and ignores it. Per-frame dispatch resolves each frame's `acting` on
|
|
1497
|
+
both. The leg always sends `?acting=`, so it does not cover that
|
|
1498
|
+
upgrade-level difference; the front-door table's multi-actor row does. The frame is the envelope the HTTP
|
|
1499
|
+
body is (`perform_action_result_to_envelope` on TS), so the response reads
|
|
1500
|
+
`{status, headers: {}, body: frame}` with `status` the code's
|
|
1501
|
+
`jsonrpc_error_code_to_http_status` (200 for a result) and every row holds
|
|
1502
|
+
unchanged. What WS loses is headers — the TS DEV-only `X-Fuz-Auth-Debug`
|
|
1503
|
+
among them. `is_ws_conformance_case(c)` (exported) is the filter: RPC, sent
|
|
1504
|
+
`POST`, no header value pinned, a principal that can hold a socket — not
|
|
1505
|
+
`anonymous` / `bearer_browser` / `malformed_bearer` / `expired_session`,
|
|
1506
|
+
refused at the upgrade, nor `scoped_token`, refused `surface:ws_upgrade`
|
|
1507
|
+
there (a narrowed bearer can never send a frame, so no per-frame scope row
|
|
1508
|
+
exists; the front-door table pins the upgrade refusal). An equivalence
|
|
1509
|
+
group left with one member on the WS leg is dropped. Cross-process only.
|
|
1510
|
+
- `conformance_rpc_auth.ts` — `generate_rpc_auth_conformance_cases({surface_source,
|
|
1511
|
+
live_rpc_endpoints?, skip_methods?})` → `ConformanceCase[]` from each declared RPC method's
|
|
1512
|
+
`RouteAuth`, then each live-mounted one the declared surface lacks
|
|
1513
|
+
(`live_rpc_endpoints`, resolved — the rows then need the same endpoints on
|
|
1514
|
+
the runner's `live_rpc_endpoints`): `anonymous` → 401 `authentication_required` for every method
|
|
1515
|
+
needing an account or actor (all in the
|
|
1516
|
+
`RPC_AUTH_ANONYMOUS_EQUIVALENCE_GROUP`, since the 401 must not vary by
|
|
1517
|
+
method); for a `credential_types` gate, each builtin channel it refuses
|
|
1518
|
+
(`keeper` = session, `token` = api_token, `daemon` = daemon_token, each the
|
|
1519
|
+
keeper) → 403 `credential_type_required` with `required_credential_types`;
|
|
1520
|
+
for `roles` on a method admitting a session, `fresh_non_admin` → 403
|
|
1521
|
+
`insufficient_permissions` with `required_roles`; public methods get
|
|
1522
|
+
nothing. Params are `generate_valid_body` minus its generated `acting`
|
|
1523
|
+
(read before validation, so the generator's id would answer
|
|
1524
|
+
`actor_not_on_account`), so a missing gate reaches the handler rather than
|
|
1525
|
+
a 400. On an actor-bound method the credential and role rows bind
|
|
1526
|
+
`acting: 'principal'` instead — both spines resolve the acting actor before
|
|
1527
|
+
the credential gate, so a keeper with a second actor (`extra_actors`) would
|
|
1528
|
+
otherwise answer `actor_required` — and the rows hold for a single-actor and
|
|
1529
|
+
a multi-actor keeper alike. The rows pin each
|
|
1530
|
+
gate, not its order against validation (the security slate's
|
|
1531
|
+
401-before-400 rows do). Skips `PROTOCOL_ACTION_METHODS` (fixed shared specs,
|
|
1532
|
+
WS-scoped — `cancel` has no reply, `peer/ping` is public — and covered by
|
|
1533
|
+
the WS suites). Given the live mount, the `_testing_*` backdoors get rows too:
|
|
1534
|
+
each is daemon-token-gated, so anonymous → 401 and the keeper's session and
|
|
1535
|
+
API token → 403 `credential_type_required`. Exists
|
|
1536
|
+
because the action-manifest parity gate compares declarations, not the
|
|
1537
|
+
live gates, and the Rust spine dispatches its own. Input-dependent handler
|
|
1538
|
+
checks stay hand-written rows. fuz_app spreads it into both conformance
|
|
1539
|
+
legs.
|
|
1102
1540
|
- `xfail.ts` — `xfail_until(tracking_id, reason, name, fn)`, a thin
|
|
1103
1541
|
`test.fails` wrapper for deferred-by-design rows (visible + self-cleaning:
|
|
1104
1542
|
turns red when the gap closes, forcing marker removal). In-scope gaps fail
|
|
@@ -1108,7 +1546,36 @@ capabilities}` protocol every Tier 1 suite uses, so **one case array runs
|
|
|
1108
1546
|
Wire from a `.db.test.ts` (in-process) and a `.cross.test.ts`
|
|
1109
1547
|
(cross-process) with the same case array — fuz_app's own runner-proof is
|
|
1110
1548
|
`../../test/cross_backend/conformance.{db,cross}.test.ts` sharing
|
|
1111
|
-
`conformance_proof_cases.ts
|
|
1549
|
+
`conformance_proof_cases.ts`, the generated auth rows, and
|
|
1550
|
+
`conformance_admin_cases.ts` (offer list/history across accounts, invite
|
|
1551
|
+
errors, pagination bounds on the admin lists); two checks the table can't
|
|
1552
|
+
express ride beside it on both legs: the duplicate-invite refusal (two
|
|
1553
|
+
requests, `describe_invite_duplicate_tests` in `invite_duplicate.ts`) and the
|
|
1554
|
+
offer reads' cross-account refusal being identical for the keeper's real id
|
|
1555
|
+
and the nil id (`describe_offer_cross_account_tests` in
|
|
1556
|
+
`offer_cross_account.ts` — the existing id is known only to the fixture).
|
|
1557
|
+
The cross legs pass the spine's live mount, so the table also runs
|
|
1558
|
+
`conformance_actor_cases.ts` (`actor_lookup`'s empty-`ids` bound, the
|
|
1559
|
+
`actor_search` scope gate) and `conformance_cell_cases.ts` (the cell refusals
|
|
1560
|
+
one request reaches with no cell in place: `kind` inside `data`, `cell_get`
|
|
1561
|
+
naming neither `id` nor `path`, the relation lists naming neither or both
|
|
1562
|
+
endpoints — each `invalid_params` with its declared reason — a grant miss
|
|
1563
|
+
`cell_grant_not_found`, the `cell_list` pagination bounds and enum-valued
|
|
1564
|
+
filters, and the schema-first rows where a bad page or filter value must answer
|
|
1565
|
+
before a handler's reason; `conformance_cell_cases.test.ts` requires a `{}` row
|
|
1566
|
+
with a reason for every cell action whose input is marked
|
|
1567
|
+
`empty_input_handler_guarded`), with the
|
|
1568
|
+
generated rows over the off-surface methods; the in-process leg for those is
|
|
1569
|
+
`conformance_off_surface.db.test.ts`, over an app mounting the cell surface
|
|
1570
|
+
and the two resolvers. The cross leg runs the same cases
|
|
1571
|
+
over WS too, and both legs run `conformance_acting_cases.ts` with the
|
|
1572
|
+
generated rows on a keeper seeded with `extra_actors`: no `acting` or a
|
|
1573
|
+
malformed one → 400 `actor_required`; the nil id, an unused id and another
|
|
1574
|
+
account's real actor → one identical 400 `actor_not_on_account`; acting as
|
|
1575
|
+
the extra actor (no grants) → 403 `insufficient_permissions` on an admin
|
|
1576
|
+
method over a session and a bearer, and on the keeper-gated `account_purge`
|
|
1577
|
+
over the daemon token; acting as the bootstrap actor passes the same gates.
|
|
1578
|
+
The role gate reads the resolved actor's grants on both spines.
|
|
1112
1579
|
|
|
1113
1580
|
### `cross_backend/ws_round_trip.ts` — `describe_cross_process_ws_tests`
|
|
1114
1581
|
|
|
@@ -1119,7 +1586,7 @@ needs raw `base_url` / `ws_path` the standard bundle doesn't carry, mirroring
|
|
|
1119
1586
|
how `describe_ws_round_trip_tests` sits beside `describe_standard_tests`
|
|
1120
1587
|
in-process. `describe_cross_process_ws_tests({setup_test, capabilities,
|
|
1121
1588
|
base_url, ws_path, origin?, rpc_path?, max_message_bytes?, logout_path?,
|
|
1122
|
-
password_path?, max_tokens?})` opens a live
|
|
1589
|
+
password_path?, max_tokens?, ws_account_roles?})` opens a live
|
|
1123
1590
|
`WebSocket` via `create_ws_transport` (the `ws` npm package) and asserts
|
|
1124
1591
|
cases against the upgrade stack `register_ws_endpoint` wires (origin →
|
|
1125
1592
|
`require_auth` → dispatch): authed upgrade round-trips `heartbeat`,
|
|
@@ -1149,7 +1616,9 @@ bearer socket (a socket on its other token stays open);
|
|
|
1149
1616
|
bearer socket of the token the cap evicts. The account-wide rows come from
|
|
1150
1617
|
`cross_backend/revocation_drivers.ts` (`create_account_revocations`, shared
|
|
1151
1618
|
with the SSE suite), and a case that opens several sockets closes whichever
|
|
1152
|
-
opened when a later open fails.
|
|
1619
|
+
opened when a later open fails. The second accounts are created holding
|
|
1620
|
+
`ws_account_roles`, for a backend whose WS upgrade requires a role (an
|
|
1621
|
+
admin-gated `/api/ws` refuses a roleless account). Per-connection auth is enforced **at upgrade
|
|
1153
1622
|
time**, so the negative upgrade cases assert the upgrade itself rejects, not
|
|
1154
1623
|
a per-message error; the close cases prove a revocation closes an
|
|
1155
1624
|
already-open socket, since per-message dispatch never re-checks credential
|
|
@@ -1168,7 +1637,7 @@ so it validates the transport without touching domain WS methods. Gated on
|
|
|
1168
1637
|
`capabilities.ws`; cross-process only (needs a real bound socket — wire from
|
|
1169
1638
|
a `*.cross.test.ts`, never an in-process setup). Authed cookies come from the
|
|
1170
1639
|
fresh-per-test keeper via `fixture.transport.cookies()`, not the stale
|
|
1171
|
-
globalSetup handle.
|
|
1640
|
+
globalSetup handle.
|
|
1172
1641
|
|
|
1173
1642
|
### `cross_backend/role_grant_offer_notification_ws.ts` — `describe_role_grant_offer_notification_ws_tests`
|
|
1174
1643
|
|
|
@@ -1185,7 +1654,23 @@ lifecycle RPC over `fixture.transport`, then
|
|
|
1185
1654
|
strict-parses the delivered frame against its canonical params schema from
|
|
1186
1655
|
`auth/role_grant_offer_notifications.ts` (the guard against serialization drift —
|
|
1187
1656
|
field / null / datetime / the flat revoke shape / the supersede `reason` +
|
|
1188
|
-
`cause_id`).
|
|
1657
|
+
`cause_id`). Every case also opens a **bystander** — a third account's
|
|
1658
|
+
admitted socket, party to nothing the case drives — and once the targeted
|
|
1659
|
+
frame has landed sends a `heartbeat` on it and asserts it received nothing
|
|
1660
|
+
but the answers to its own requests. The barrier is what makes the silence
|
|
1661
|
+
mean something: both spines queue a fan-out to every recipient in one pass
|
|
1662
|
+
with no await in it, and neither writes a response ahead of a notification
|
|
1663
|
+
already queued for the same connection — the TS transport sends both through
|
|
1664
|
+
one outbound queue, and the Rust loop writes a connection's queued frames
|
|
1665
|
+
before a response (`flush_queued_frames`). So a frame the pass addressed to
|
|
1666
|
+
the bystander reaches it ahead of the barrier's response. That is strict on TS,
|
|
1667
|
+
which sends a fan-out in one synchronous closure. On Rust, `deliver_all` pushes
|
|
1668
|
+
to recipients one after another on one worker while the target's connection
|
|
1669
|
+
loop runs on another, so the target's frame can be written while the pass is
|
|
1670
|
+
still pushing — a window the barrier's round trip outlasts in practice. The
|
|
1671
|
+
check covers every pass of the drain: the Rust `PostCommitEffects::drain_all`
|
|
1672
|
+
joins effects whose notify futures push synchronously, so every pass
|
|
1673
|
+
completes in the drain's first poll. Unlike `describe_cross_process_ws_tests` (consumer-agnostic,
|
|
1189
1674
|
`heartbeat`-only), this drives a real **domain** notification family — but one
|
|
1190
1675
|
built from spine primitives only (accounts / role-grants / offers / WS), zero
|
|
1191
1676
|
consumer domain, so it lives here and runs against any backend that wires the
|
|
@@ -1204,8 +1689,7 @@ grant and expects its socket to close with `WS_CLOSE_PERMISSION_REVOKED` (the
|
|
|
1204
1689
|
notification may or may not land first, and is held to the flat shape when it
|
|
1205
1690
|
does), where an ungated socket expects the frame. The offer/accept fixtures are `create_pending_role_grant_offer` /
|
|
1206
1691
|
`create_active_role_grant` (`cross_backend/role_grant_cross_helpers.ts`).
|
|
1207
|
-
Gated on `capabilities.ws`; cross-process only. fuz_app's
|
|
1208
|
-
`src/test/cross_backend/role_grant_offer_notification_ws.cross.test.ts`, and
|
|
1692
|
+
Gated on `capabilities.ws`; cross-process only. fuz_app's
|
|
1209
1693
|
`role_grant_offer_notification_ws_gated.cross.test.ts` runs it again over the
|
|
1210
1694
|
spine binaries' role-gated endpoint with `ws_required_roles` declared.
|
|
1211
1695
|
|
|
@@ -1227,7 +1711,6 @@ must hold `ROLE_ADMIN` and `scoped_role` (a role an admin can grant scoped).
|
|
|
1227
1711
|
fuz_app's spine binaries mount `SPINE_ROLE_GATED_WS_PATH` gated on
|
|
1228
1712
|
`SPINE_ROLE_GATED_WS_ROLES` (`admin` + `participant`). Gated on
|
|
1229
1713
|
`capabilities.ws` and `capabilities.ws_role_gated`; cross-process only.
|
|
1230
|
-
fuz_app's own wiring is `src/test/cross_backend/ws_role_gated.cross.test.ts`.
|
|
1231
1714
|
|
|
1232
1715
|
### `cross_backend/peer_ping_ws.ts` — `describe_peer_ping_ws_tests`
|
|
1233
1716
|
|
|
@@ -1251,8 +1734,7 @@ the HTTP no-transport path. Gated on `capabilities.peer_request` — `true` on
|
|
|
1251
1734
|
both the Rust spine and the TS spine (the `BackendWebsocketTransport.request_connection`
|
|
1252
1735
|
path + `register_action_ws` response correlation); `peer/ping` is a protocol
|
|
1253
1736
|
action (manifest-excluded), so parity is behavioral here, not via the manifest
|
|
1254
|
-
gate. Cross-process only
|
|
1255
|
-
`src/test/cross_backend/peer_ping_ws.cross.test.ts`.
|
|
1737
|
+
gate. Cross-process only.
|
|
1256
1738
|
|
|
1257
1739
|
### `cross_backend/ws_inbound_dispatch.ts` — `describe_ws_inbound_dispatch_cross_tests`
|
|
1258
1740
|
|
|
@@ -1267,8 +1749,7 @@ client's responder withholds the server's ping echo until the test releases
|
|
|
1267
1749
|
it — so the suite gates on `capabilities.peer_request`.
|
|
1268
1750
|
`describe_ws_inbound_dispatch_cross_tests({setup_test, capabilities, base_url,
|
|
1269
1751
|
ws_path})`. The wire literals are independent copies of the producers'
|
|
1270
|
-
constants, so a value changed on one spine fails here. Cross-process only
|
|
1271
|
-
fuz_app's own wiring is `src/test/cross_backend/ws_inbound_dispatch.cross.test.ts`.
|
|
1752
|
+
constants, so a value changed on one spine fails here. Cross-process only.
|
|
1272
1753
|
|
|
1273
1754
|
### `cross_backend/ws_outbound_overflow.ts` — `describe_ws_outbound_overflow_cross_tests`
|
|
1274
1755
|
|
|
@@ -1297,8 +1778,7 @@ flood action is daemon-token-gated, so the flooding socket upgrades with the
|
|
|
1297
1778
|
daemon token and no `Origin` (`create_ws_transport`'s `origin: null`). The wire
|
|
1298
1779
|
literals are independent copies of the producers' constants. Gated on
|
|
1299
1780
|
`capabilities.ws`; cross-process only, and only for a backend that mounts the
|
|
1300
|
-
flood action on its WS endpoint.
|
|
1301
|
-
`src/test/cross_backend/ws_outbound_overflow.cross.test.ts`.
|
|
1781
|
+
flood action on its WS endpoint.
|
|
1302
1782
|
|
|
1303
1783
|
### `cross_backend/ws_connection_cap.ts` — `describe_ws_connection_cap_cross_tests`
|
|
1304
1784
|
|
|
@@ -1326,8 +1806,7 @@ overridden; pass the consumer's value when it sets one, `null` (cap disabled)
|
|
|
1326
1806
|
to skip. **Consumer-agnostic** like the WS round-trip suite (`heartbeat` only).
|
|
1327
1807
|
A separate call rather than more cases on that suite because each case opens
|
|
1328
1808
|
more than a cap's worth of real sockets. Gated on `capabilities.ws`;
|
|
1329
|
-
cross-process only.
|
|
1330
|
-
`src/test/cross_backend/ws_connection_cap.cross.test.ts`.
|
|
1809
|
+
cross-process only.
|
|
1331
1810
|
|
|
1332
1811
|
### `cross_backend/session_cap.ts` — `describe_session_cap_cross_tests`
|
|
1333
1812
|
|
|
@@ -1335,12 +1814,13 @@ Parity for the per-account session cap over real HTTP: logging in past
|
|
|
1335
1814
|
`max_sessions` (default `DEFAULT_MAX_SESSIONS`) never refuses a login and
|
|
1336
1815
|
evicts the oldest session.
|
|
1337
1816
|
`describe_session_cap_cross_tests({setup_test, max_sessions?, login_path?,
|
|
1338
|
-
status_path?, ws?})` — pass `ws` (`{base_url, ws_path, origin?}`) to also
|
|
1817
|
+
status_path?, ws?, ws_account_roles?})` — pass `ws` (`{base_url, ws_path, origin?}`) to also
|
|
1339
1818
|
assert that the evicted session's socket is closed with
|
|
1340
1819
|
`WS_CLOSE_SESSION_REVOKED` once the evicting login commits; omitted, that case
|
|
1341
|
-
is skipped, so a caller passes it only for a backend with `capabilities.ws
|
|
1342
|
-
|
|
1343
|
-
|
|
1820
|
+
is skipped, so a caller passes it only for a backend with `capabilities.ws`;
|
|
1821
|
+
`ws_account_roles` gives that case's account roles, for a backend whose WS
|
|
1822
|
+
upgrade requires one.
|
|
1823
|
+
Cross-process only.
|
|
1344
1824
|
|
|
1345
1825
|
### `cross_backend/sse_round_trip.ts` — `describe_cross_process_sse_tests`
|
|
1346
1826
|
|
|
@@ -1370,8 +1850,7 @@ admin each drop its stream — the rows `create_account_revocations`
|
|
|
1370
1850
|
session closes the oldest and leaves the rest open. The data-frame + close
|
|
1371
1851
|
cases gate on `rpc_path` (they drive the standard account/admin actions); all
|
|
1372
1852
|
cases gate on `capabilities.sse`. Cross-process only — wire from a
|
|
1373
|
-
`*.cross.test.ts`. fuz_app
|
|
1374
|
-
`src/test/cross_backend/sse.cross.test.ts`, which runs on every backend — the
|
|
1853
|
+
`*.cross.test.ts`. It runs on every fuz_app backend — the
|
|
1375
1854
|
TS spines wire `audit_log_sse` and the Rust `spine_stub` serves the same
|
|
1376
1855
|
stream.
|
|
1377
1856
|
|
|
@@ -1389,7 +1868,7 @@ covered on WS and SSE at once. The drivers stand alone too: `cross_post_as`
|
|
|
1389
1868
|
`cross_admin_token_revoke_all`, and `cross_account_purge` (over the daemon
|
|
1390
1869
|
token, no `Origin`).
|
|
1391
1870
|
|
|
1392
|
-
### `cross_backend/cell_crud.ts` + `cell_relations.ts` + `cell_gated_create.ts` — cell parity suites
|
|
1871
|
+
### `cross_backend/cell_crud.ts` + `cell_relations.ts` + `cell_gated_create.ts` + `cell_moderate.ts` — cell parity suites
|
|
1393
1872
|
|
|
1394
1873
|
The cell-layer parity coverage is split across two sibling suites. Cells
|
|
1395
1874
|
can't ride the generic `describe_rpc_round_trip_tests` (stateful verbs need a
|
|
@@ -1400,7 +1879,21 @@ cell validators (`describe_standard_cross_process_tests`' generic round-trip
|
|
|
1400
1879
|
never sees them). Both parse every success response against the verb's Zod
|
|
1401
1880
|
**output** schema, so a TS↔Rust envelope drift fails the suite — not just a
|
|
1402
1881
|
payload-field drift. Call-site primitives (`cross_rpc_call` / `error_reason` /
|
|
1403
|
-
`expect_output`) live in `cross_backend/
|
|
1882
|
+
`expect_output`) live in `cross_backend/cross_rpc.ts`, shared by every
|
|
1883
|
+
stateful cross suite (`RpcResult` carries the HTTP `status` beside the
|
|
1884
|
+
envelope; a reply that is no JSON object throws naming the method,
|
|
1885
|
+
the HTTP status and the body's head), and every
|
|
1886
|
+
suite that reads audit rows goes through `cross_backend/audit_cross_helpers.ts`
|
|
1887
|
+
— `list_audit_events(fixture, params, rpc_path?)` drains
|
|
1888
|
+
(`cross_drain_effects`, the `_testing_drain_effects` barrier, refusing an answer
|
|
1889
|
+
without `tracked: true`) before it lists and parses, so no reader skips the
|
|
1890
|
+
barrier and every read checks the barrier is live, and parses every row's metadata
|
|
1891
|
+
(`assert_audit_event_metadata`, closed to undeclared keys when the fixture's
|
|
1892
|
+
setup asked), so every reader holds the rows it reads to their event's
|
|
1893
|
+
schema. A suite pinning a row's values uses `assert_audit_metadata_fields(fixture,
|
|
1894
|
+
metadata, expected, absent?)` — each field compared on its own so a consumer's
|
|
1895
|
+
added key passes, the `absent` keys refused, and the whole object compared only
|
|
1896
|
+
when the fixture closed the world; the shared
|
|
1404
1897
|
options shape is `RpcPathCapabilityGatedCrossSuiteOptions` from
|
|
1405
1898
|
`cross_backend/setup.ts`. `cross_backend/wire_shapes.ts` carries
|
|
1406
1899
|
`assert_iso8601_seconds` / `assert_iso8601_seconds_nullable` — the byte-level
|
|
@@ -1413,9 +1906,9 @@ an audit row, an admin account listing, and an offer's `created_at` /
|
|
|
1413
1906
|
imperative cross suite aliases: `CrossSuiteOptions` (`setup_test` alone),
|
|
1414
1907
|
`CapabilityGatedCrossSuiteOptions` (+ `capabilities` — `ready`),
|
|
1415
1908
|
`RpcPathCrossSuiteOptions` (+ `rpc_path?` — origin, body-size (which adds `base_url`), actor
|
|
1416
|
-
lookup
|
|
1909
|
+
lookup, app settings), and the intersection
|
|
1417
1910
|
`RpcPathCapabilityGatedCrossSuiteOptions` (every `cell_*` suite, query-shape,
|
|
1418
|
-
token-scope surface, fact-serving, account lifecycle). A suite takes the
|
|
1911
|
+
token-scope surface, fact-serving, account lifecycle, testing backdoor). A suite takes the
|
|
1419
1912
|
capability-carrying variant only when it actually gates cases on a flag, so
|
|
1420
1913
|
the option type states whether the bundle is read.
|
|
1421
1914
|
|
|
@@ -1426,20 +1919,36 @@ the option type states whether the bundle is read.
|
|
|
1426
1919
|
`path` write by non-admin → 403 on create + update; `cell_get` with no
|
|
1427
1920
|
id/path → `invalid_params`; null-auth `cell_list` `created_by` →
|
|
1428
1921
|
`invalid_params`; a `kind` at `CELL_KIND_LENGTH_MAX` stored whole, one
|
|
1429
|
-
UTF-16 code unit past it → `invalid_params` on create and as a list filter
|
|
1922
|
+
UTF-16 code unit past it → `invalid_params` on create and as a list filter;
|
|
1923
|
+
`cell_list` pages honoring `limit` + `offset`, a page holding visible rows
|
|
1924
|
+
only and filling to `limit` past a newer run of rows the caller can't see —
|
|
1925
|
+
two owners' private cells, read by a non-owner and by anon — and an admin's
|
|
1926
|
+
pages tiling every row; a `data` that is no `CellData` — a non-object, or a
|
|
1927
|
+
non-string `label` / `summary` — refused 400 `invalid_params` with the
|
|
1928
|
+
generic message and no reason (no `data` beyond a TS spine's DEV-only
|
|
1929
|
+
`issues`, an array of Zod issues) on `cell_create`, on `cell_update` of a
|
|
1930
|
+
missing and of a hidden cell, and, with `cell_relations`, on `cell_clone`'s
|
|
1931
|
+
`with_data_patch` — ahead of the 404 mask the same hidden-cell call answers
|
|
1932
|
+
when well-typed, with the keeper's cell count unchanged; those cases run as
|
|
1933
|
+
the keeper and plain accounts only, so a backend that refuses a non-admin
|
|
1934
|
+
root cell can keep them through `exclude_cases` while excluding the rest).
|
|
1430
1935
|
- **`describe_cell_relations_cross_tests`** (gates on
|
|
1431
1936
|
`capabilities.cell_relations`) — the verbs beyond CRUD: grant lifecycle
|
|
1432
1937
|
(actor-shaped editor grant enables edit, manage-tier `cell_grant_list`,
|
|
1433
1938
|
revoke), the now-reachable `cell_visibility_manage_only` 403 (editor-grant
|
|
1434
1939
|
holder can't flip visibility), field set / forward+reverse list / idempotent
|
|
1435
1940
|
delete, item insert / ordered forward+reverse list / move / idempotent
|
|
1436
|
-
delete,
|
|
1941
|
+
delete, an insert or move onto a taken position → `cell_item_position_taken`
|
|
1942
|
+
with the rows unchanged, clone shallow (shares edges) vs deep (clones children), and
|
|
1437
1943
|
manage-tier `cell_audit_list` (owner reads the timeline; a viewer-grant
|
|
1438
1944
|
holder who can `cell_get` still gets the IDOR 404). Only **actor-shaped**
|
|
1439
1945
|
grants are exercised — role-shaped principals need a closed role registry the
|
|
1440
1946
|
Rust spine deliberately lacks.
|
|
1441
|
-
- **`describe_cell_gated_create_cross_tests`** +
|
|
1442
|
-
(
|
|
1947
|
+
- **`describe_cell_gated_create_cross_tests`** (`cell_gated_create.ts`) +
|
|
1948
|
+
**`describe_cell_moderate_cross_tests`** (`cell_moderate.ts`)
|
|
1949
|
+
(gate on `capabilities.cell_gated_create`, **cross-only**; both build their
|
|
1950
|
+
spaces on `TEST_SPACE_POLICY`, exported beside the authorizer in
|
|
1951
|
+
`test_cell_gated_create_authorize.ts`) — the parent-aware
|
|
1443
1952
|
cell-creation authorizer (`CellCreateAuthorize`, **directory model**) +
|
|
1444
1953
|
the `cell_moderate` verb parity proofs. Both spines mount the same
|
|
1445
1954
|
`test_cell_gated_create_authorize` policy (its admin bypass and `min_role`
|
|
@@ -1453,7 +1962,9 @@ moderation_required}`, with `moderation` set per the verdict (`pending`+private
|
|
|
1453
1962
|
**visible** one **403**s; plain parentless creates stay open. The moderate
|
|
1454
1963
|
suite then proves the `pending → approved | rejected` transition: an admin
|
|
1455
1964
|
(root manager) approves → public, the **author** self-approve → 403
|
|
1456
|
-
(anti-self-approval), a non-viewer → 404, reject → stays private
|
|
1965
|
+
(anti-self-approval), a non-viewer → 404, reject → stays private, and a cell
|
|
1966
|
+
with no governing root → 400 `cell_not_a_contribution` for any viewer, the
|
|
1967
|
+
404 mask first for a non-viewer. It also
|
|
1457
1968
|
pins the moderation read gate (a pending or rejected post reaches no
|
|
1458
1969
|
non-steward reader past its author and admins on `cell_get`, `cell_list`,
|
|
1459
1970
|
`cell_item_list`, and fact serving, even flipped public or granted while
|
|
@@ -1469,8 +1980,7 @@ moderation_required}`, with `moderation` set per the verdict (`pending`+private
|
|
|
1469
1980
|
|
|
1470
1981
|
`cell_crud` / `cell_relations` gate `true` on TS + Rust (cells run on both, no
|
|
1471
1982
|
`.skip`); `cell_gated_create` is cross-only (the policy is a spine-binary
|
|
1472
|
-
fixture).
|
|
1473
|
-
(all the cell suites); the in-process legs (plain `gro test`) for crud/relations
|
|
1983
|
+
fixture). The in-process legs (plain `gro test`) for crud/relations
|
|
1474
1984
|
are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
1475
1985
|
|
|
1476
1986
|
- `cell_relations_parity.db.test.ts`, sharing the full-surface
|
|
@@ -1486,11 +1996,59 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
|
1486
1996
|
`rust_spine_stub_backend_config()` as a convenience preset for the non-domain
|
|
1487
1997
|
third spine consumer, but otherwise backend-specific paths and env are a
|
|
1488
1998
|
consumer concern.
|
|
1999
|
+
- `testing/cross_backend/default_backend_configs.ts` —
|
|
2000
|
+
`make_default_ts_backend_config` / `make_default_rust_backend_config`, the
|
|
2001
|
+
family builders a consumer config composes: the env baseline (`HOST` bound
|
|
2002
|
+
to `DEFAULT_TEST_BACKEND_HOST`, `127.0.0.1`; the port under
|
|
2003
|
+
`port_env_var`; cookie keys; allowed origins; the bootstrap token path; the
|
|
2004
|
+
Rust startup self-wipe), the default paths, and the required `capabilities`.
|
|
2005
|
+
`daemon_token_dir_env` names the env var the binary reads its daemon-token
|
|
2006
|
+
directory from, set to the `{dir}` whose `{dir}/run/daemon_token` is the
|
|
2007
|
+
bootstrap's `daemon_token_path` (`to_daemon_token_dir`, which throws for a
|
|
2008
|
+
path in another layout).
|
|
2009
|
+
- `testing/cross_backend/create_cross_backend_global_setup.ts` —
|
|
2010
|
+
`create_cross_backend_global_setup({configs, derive_name?, provide_key?,
|
|
2011
|
+
ensure_databases?, before_spawn?})`, the single-backend `globalSetup` maker:
|
|
2012
|
+
the backend name derived from the vitest project name selects a config,
|
|
2013
|
+
which is spawned, bootstrapped and provided. `before_spawn` (by backend
|
|
2014
|
+
name; a key naming no config, an inherited one included, throws at
|
|
2015
|
+
construction) runs first for the selected backend — `prepare_cargo_binary`
|
|
2016
|
+
for a Rust one — then its config factory, then `ensure_databases: true`
|
|
2017
|
+
creates the database the config's `postgres:` `env.DATABASE_URL` names
|
|
2018
|
+
(`ensure_database_for_url`), then the spawn. A second argument
|
|
2019
|
+
(`CrossBackendGlobalSetupDeps`) swaps the database creator and the spawn,
|
|
2020
|
+
for testing the setup itself.
|
|
2021
|
+
- `testing/cross_backend/cargo_binary.ts` — `prepare_cargo_binary({workspace_dir,
|
|
2022
|
+
package, bin?, profile?})` builds a Cargo binary (`cargo build -p --bin
|
|
2023
|
+
--profile`, output inherited, a failed build throws) unless
|
|
2024
|
+
`NO_REBUILD_ENV` (`FUZ_TESTING_NO_REBUILD`, `is_no_rebuild_requested`) asks
|
|
2025
|
+
to skip it, and returns its path; `resolve_cargo_binary_path(options)` is the
|
|
2026
|
+
path alone, for a config's `start_command` — under the `target_directory`
|
|
2027
|
+
`cargo metadata` reports (memoized per process per workspace), in the
|
|
2028
|
+
profile's directory (`to_cargo_profile_dir`: `dev` → `debug`;
|
|
2029
|
+
`to_cargo_binary_path` reads it from the metadata JSON). Run as
|
|
2030
|
+
`create_cross_backend_global_setup`'s `before_spawn` hook, the build happens
|
|
2031
|
+
before the spawn's startup window, with its output shown. fuz_app's own stub
|
|
2032
|
+
rebuild (`rebuild_rust_spine_stub`) is built on it.
|
|
2033
|
+
- `testing/cross_backend/postgres_database.ts` — `ensure_database(database,
|
|
2034
|
+
server_url?, deps?)` (idempotent `createdb` on the server the URL names, its
|
|
2035
|
+
password passed in the environment; `createdb`'s already-exists refusal is
|
|
2036
|
+
the quiet success case, `is_createdb_already_exists`, and any other failure
|
|
2037
|
+
is reported through `deps.warn` — `console.warn` by default — with
|
|
2038
|
+
`createdb`'s stderr, never thrown), `ensure_database_for_url(url, deps?)` (the
|
|
2039
|
+
database a `postgres:` URL names; anything else left alone),
|
|
2040
|
+
`with_database_name`, `to_maintenance_database_url`, `redact_database_url`
|
|
2041
|
+
(a URL fit to print), `is_postgres_url`. The harness creates a database only
|
|
2042
|
+
when a setup asks, so a test role needs no `CREATEDB` grant otherwise.
|
|
1489
2043
|
- `testing/cross_backend/spawn_backend.ts` — `spawn_backend(config) => BackendHandle`.
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
2044
|
+
Refuses a port that already accepts connections, writes the bootstrap
|
|
2045
|
+
token, deletes a stale daemon-token file, spawns `detached: true` in its
|
|
2046
|
+
own process group (so SIGTERM to the negative pid tears down descendants),
|
|
2047
|
+
polls health (each probe request timed out), reads the deterministic daemon
|
|
2048
|
+
token from the binary-written file. Keeps a bounded stderr tail
|
|
2049
|
+
(`create_output_tail`) for an early exit's error — with its exit code or
|
|
2050
|
+
signal — and for the log line an unexpected exit after startup writes.
|
|
2051
|
+
Registers exit-time + signal cleanup so vitest worker death or
|
|
1494
2052
|
Ctrl+C kills children before they strand ports.
|
|
1495
2053
|
- `testing/transports/fetch_transport.ts` — cookie-threading HTTP transport
|
|
1496
2054
|
satisfying `RpcTestTransport`. Carries a name-keyed cookie jar that
|
|
@@ -1530,6 +2088,18 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
|
1530
2088
|
opens and round-trips `heartbeat` (response id `WS_ADMISSION_PROBE_ID`) so the
|
|
1531
2089
|
connection is admitted when it resolves; suites that wait for a push, or need
|
|
1532
2090
|
connections registered in a known order, open through it.
|
|
2091
|
+
- `testing/cross_backend/ws_endpoint.ts` — `WsEndpointTarget`
|
|
2092
|
+
(`{base_url, ws_path, origin?}`), the endpoint shape the socket-driving
|
|
2093
|
+
cross suites take (flat, or as an optional `ws` on
|
|
2094
|
+
`describe_session_cap_cross_tests`, `describe_testing_backdoor_cross_tests`
|
|
2095
|
+
and the conformance table; `describe_transport_composition_cross_tests`
|
|
2096
|
+
keeps its own nullable `base_url` and defaulted `ws_path`), and
|
|
2097
|
+
`open_session_ws(target, account, {admitted, on_request?, headers?,
|
|
2098
|
+
default_timeout_ms?})`, which opens a socket on an account's session cookie
|
|
2099
|
+
(`to_session_cookie`, asserted present) with `target.origin` or `base_url`
|
|
2100
|
+
as the `Origin` — through `create_admitted_ws_transport` when `admitted`,
|
|
2101
|
+
`create_ws_transport` otherwise. The cookie is the account's static one, so
|
|
2102
|
+
a session rotated through `fixture.transport` upgrades stale.
|
|
1533
2103
|
- `testing/transports/ws_raw_client.ts` — `connect_raw_ws({port, hostname?, path, headers?, pipelined?})`
|
|
1534
2104
|
→ `RawWsClient` (`{frames, send_text, wait_for, destroy}`), a WebSocket client
|
|
1535
2105
|
over a raw `node:net` socket for the two things a client library can't do:
|
|
@@ -1551,8 +2121,12 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
|
1551
2121
|
`raw_http_parse_response` (the first response, `null` until its head and
|
|
1552
2122
|
framed body are whole), `raw_http_count_responses` (unanchored status-line
|
|
1553
2123
|
count, so a response concatenated after a body is seen),
|
|
1554
|
-
`raw_http_first_status
|
|
1555
|
-
|
|
2124
|
+
`raw_http_first_status`, `raw_http_header_lines` (headers as CRLF lines),
|
|
2125
|
+
`raw_http_parse_json` (a body's JSON, `undefined` when it isn't), and
|
|
2126
|
+
`RAW_HTTP_READ_TIMEOUT_MS`, the suites' default read window. Shared by the
|
|
2127
|
+
body-size, body-size smuggling, credential-header, origin-scope and
|
|
2128
|
+
transport-composition suites, which take the 1 MiB cap they probe from
|
|
2129
|
+
`SPINE_BODY_LIMIT_DEFAULT_BYTES` (`spine_surface_constants.ts`).
|
|
1556
2130
|
- `testing/transports/sse_frame_reader.ts` — `create_sse_frame_reader(reader, default_timeout_ms?)`,
|
|
1557
2131
|
the transport-agnostic SSE framing core over a
|
|
1558
2132
|
`ReadableStreamDefaultReader<Uint8Array>`: `\n\n` framing, per-read timeout,
|
|
@@ -1576,22 +2150,25 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
|
1576
2150
|
`create_testing_actions(deps, options)` factory returning the
|
|
1577
2151
|
`_testing_reset` RPC action. Test binaries register it on their RPC
|
|
1578
2152
|
endpoint; `default_cross_process_setup` fires it unconditionally per
|
|
1579
|
-
test. Handler
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
2153
|
+
test. Handler first fires the consumer-supplied `reset_state(db)`
|
|
2154
|
+
callback for domain-state reset — before the auth wipe, because domain
|
|
2155
|
+
rows depend on auth rows (a domain table with a restricting FK to `actor`
|
|
2156
|
+
must empty before the actor delete) — then DELETEs every auth-namespace
|
|
2157
|
+
row (no keeper-preserve filter), resets `app_settings` to production
|
|
2158
|
+
defaults, flips `bootstrap_lock.bootstrapped = true`, inline-seeds a fresh
|
|
2159
|
+
keeper via `create_test_account_with_credentials` (same primitive
|
|
2160
|
+
in-process uses, keeping write semantics in parity), queues the refresh
|
|
1586
2161
|
of `DaemonTokenState.keeper_account_id` to the new row as a post-commit
|
|
1587
|
-
effect (so a reset that rolls back — a
|
|
1588
|
-
commit — leaves the cache on the keeper
|
|
1589
|
-
next reset can still authenticate), then
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
2162
|
+
effect as soon as the keeper exists (so a reset that rolls back — a
|
|
2163
|
+
failing extra seed, a failed commit — leaves the cache on the keeper
|
|
2164
|
+
still in the database, and the next reset can still authenticate), then
|
|
2165
|
+
seeds any `extra_accounts` at the same bootstrap-equivalent step (the
|
|
2166
|
+
only path for roles like `ROLE_KEEPER` whose `grant_paths` is
|
|
2167
|
+
bootstrap-only). `reset_state` is passed the **transactional** `Db` the
|
|
2168
|
+
auth wipe then runs on, so DB-domain consumers (e.g. fuz_forge truncating
|
|
2169
|
+
its cell / fact / file tables) reset on the same connection rather than a
|
|
2170
|
+
separately-pooled one that would deadlock against this open transaction
|
|
2171
|
+
under the in-memory drivers. Auth
|
|
1595
2172
|
gates on `credential_types: ['daemon_token']` — effectively keeper-only
|
|
1596
2173
|
without forcing the `actor: 'required'` ⟺ `acting?: ActingActor`
|
|
1597
2174
|
biconditional. No free-form runtime grant action exists — see the
|
|
@@ -1606,6 +2183,10 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
|
1606
2183
|
emitter must be built with `create_audit_emitter({track_inflight: true})`
|
|
1607
2184
|
(the spine binary's `audit_factory` does — pinned by
|
|
1608
2185
|
`cross_backend/testing_spine_server.audit.test.ts`), or the drain resolves at once.
|
|
2186
|
+
The action answers `{ok: true, tracked}`, `tracked` read from
|
|
2187
|
+
`audit.is_tracking_inflight()`, and `cross_drain_effects` refuses anything but
|
|
2188
|
+
`tracked: true` — so a binary built over an untracked emitter fails its first
|
|
2189
|
+
audit read rather than racing it. The Rust twin answers the same shape.
|
|
1609
2190
|
The barrier is needed whatever the transport: HTTP on a binary running
|
|
1610
2191
|
`await_pending_effects: true` settles a mutation's writes before
|
|
1611
2192
|
responding, but WS replies before its effects flush, so a WS mutation's
|
|
@@ -1690,10 +2271,57 @@ refs); and a hidden cell's update, a create under a hidden parent, and a clone
|
|
|
1690
2271
|
of a hidden source answer `404` whatever `data` names. Facts are put through
|
|
1691
2272
|
`cross_put_fact` (exported by `fact_serving.ts`), whose `holder` maps onto
|
|
1692
2273
|
`_testing_put_fact`'s optional `holder` key. Gated on
|
|
1693
|
-
`capabilities.fact_serving`; cross-process only.
|
|
1694
|
-
`src/test/cross_backend/cell_ref_admission.cross.test.ts`; the in-process
|
|
2274
|
+
`capabilities.fact_serving`; cross-process only. The in-process
|
|
1695
2275
|
cases are `src/test/auth/cell_ref_admission{,.fact_less}.db.test.ts`.
|
|
1696
2276
|
|
|
2277
|
+
### Malformed-envelope and invalid-params parity — `cross_backend/rpc_adversarial.ts`
|
|
2278
|
+
|
|
2279
|
+
`describe_rpc_adversarial_cross_tests({setup_test, surface_source, rpc_path?,
|
|
2280
|
+
live_rpc_endpoints?, skip_methods?, input_overrides?, xfail_params?})` — the
|
|
2281
|
+
envelope and params groups of `describe_rpc_attack_surface_tests`, sent over
|
|
2282
|
+
`fixture.transport` to a spawned backend. The in-process groups run on stub
|
|
2283
|
+
apps, while the Rust spine parses envelopes and params by hand and the
|
|
2284
|
+
action-manifest parity gate
|
|
2285
|
+
compares no param schemas, so this is what holds the spines to one answer.
|
|
2286
|
+
|
|
2287
|
+
- **envelopes** — every `create_rpc_envelope_cases` case, one test each, on a
|
|
2288
|
+
fresh fixture; status, code, echoed `id`, and `data.reason` per case, and a
|
|
2289
|
+
guard test that the endpoint has both a read and an authenticated mutation to
|
|
2290
|
+
carry them.
|
|
2291
|
+
- **params** — one test per declared method, sending every
|
|
2292
|
+
`generate_rpc_params_test_cases` case (seeded from the method's
|
|
2293
|
+
`input_overrides` entry when it has one — the cases go as a single-actor
|
|
2294
|
+
principal, so an override omits `acting`) and failing with the list of
|
|
2295
|
+
cases not answered 400 `invalid_params` with the broken field's declared
|
|
2296
|
+
`invalid_reason` as `data.reason` (`expected_invalid_reason`; none for an
|
|
2297
|
+
undeclared field or a whole-body case). `xfail_params`
|
|
2298
|
+
(`RpcParamsXfail` — `{method, labels, tracking_id, reason}`) moves the named
|
|
2299
|
+
cases of a method into one `xfail_until` test each, for a known divergence on
|
|
2300
|
+
one backend; the method's other cases keep their test, and an entry naming no
|
|
2301
|
+
generated case fails. Pass it only for the backend that diverges — fuz_app
|
|
2302
|
+
keys it on `RUST_SPINE_STUB_DEFAULT_NAME`.
|
|
2303
|
+
|
|
2304
|
+
The authorized principal is the keeper on the first channel the method's auth
|
|
2305
|
+
admits — its session, else its daemon token or API token over a no-`Origin`
|
|
2306
|
+
transport — resolved through `resolve_conformance_principal`.
|
|
2307
|
+
**Preconditions:** the keeper holds every role a declared method gates on
|
|
2308
|
+
(`default_cross_process_setup` seeds `keeper` and `admin`; `extra_keeper_roles`
|
|
2309
|
+
for others) and has a single actor. No generated body carries a valid `acting`,
|
|
2310
|
+
and a malformed one reads as omitted ahead of validation on both spines, so a
|
|
2311
|
+
single-actor keeper resolves its actor and validation refuses; a keeper with a
|
|
2312
|
+
second actor answers `actor_required` first, so such a suite lists the
|
|
2313
|
+
actor-required methods in `skip_methods`.
|
|
2314
|
+
|
|
2315
|
+
**Off the declared surface.** `live_rpc_endpoints` (resolved) adds the methods
|
|
2316
|
+
the endpoint at `rpc_path` live-mounts beyond the declared surface — cell
|
|
2317
|
+
verbs, opt-in resolvers, `_testing_*` backdoors — to both groups: their
|
|
2318
|
+
envelope cases in a group of their own, and a params test per method,
|
|
2319
|
+
`_testing_*` included (a daemon-only backdoor goes with the keeper's daemon
|
|
2320
|
+
token). fuz_app's spine self-test passes `create_spine_live_rpc_endpoints()`.
|
|
2321
|
+
Skips `_testing_*` on the declared surface and `PROTOCOL_ACTION_METHODS`, which
|
|
2322
|
+
`generate_rpc_auth_conformance_cases` skips too.
|
|
2323
|
+
Ungated; cross-process only.
|
|
2324
|
+
|
|
1697
2325
|
### Token-scope surface parity — `cross_backend/token_scope_surface.ts`
|
|
1698
2326
|
|
|
1699
2327
|
`describe_token_scope_surface_cross_tests({setup_test, capabilities, rpc_path?,
|
|
@@ -1708,7 +2336,7 @@ the spec-derived suites can't see it (REST routes, off the declared RPC surface)
|
|
|
1708
2336
|
Mints a narrowed bearer through the production `account_token_create` path and
|
|
1709
2337
|
fires it at the two surfaces whose denial **body** is observable over plain
|
|
1710
2338
|
HTTP — the audit SSE stream (`capabilities.sse`) and the admin-only bare-hash
|
|
1711
|
-
fact read (`capabilities.
|
|
2339
|
+
fact read (`capabilities.fact_serving_admin_hash`) — asserting the exact flat
|
|
1712
2340
|
`{error: 'token_scope_required', required_scope: 'surface:<name>'}` both spines
|
|
1713
2341
|
contract on (`token_scope_denied_body` ↔
|
|
1714
2342
|
`fuz_auth::token_scope_surface_denied_response`).
|
|
@@ -1733,20 +2361,21 @@ _does_ name, and a full bearer must get past the scope gate on the same route.
|
|
|
1733
2361
|
The WS upgrade is deliberately omitted — `create_ws_transport` threads cookies,
|
|
1734
2362
|
not bearer headers, and a refused upgrade's body isn't readable, so it can't
|
|
1735
2363
|
serve a body-parity gate (its per-spine coverage stays in the two surface
|
|
1736
|
-
censuses). Cross-process only
|
|
1737
|
-
`src/test/cross_backend/token_scope_surface.cross.test.ts`.
|
|
2364
|
+
censuses). Cross-process only.
|
|
1738
2365
|
|
|
1739
2366
|
### Front-door contract — `cross_backend/transport_composition.ts`
|
|
1740
2367
|
|
|
1741
2368
|
`describe_transport_composition_cross_tests({setup_test, capabilities, base_url,
|
|
1742
|
-
rpc_path?, origin?, rest_path?, password_path?, ws_path?, sse_path
|
|
2369
|
+
rpc_path?, origin?, rest_path?, password_path?, ws_path?, sse_path?,
|
|
2370
|
+
multi_actor?, rpc_max_body_bytes?})` — the per-transport
|
|
1743
2371
|
composition table: what each transport answers before per-action dispatch,
|
|
1744
2372
|
when one request trips several refusals. Rows are REST `POST` (session-only
|
|
1745
2373
|
`/api/account/logout`), RPC `POST`, RPC `GET`, the WS upgrade (gated on
|
|
1746
2374
|
`capabilities.ws`) and the audit stream (`capabilities.sse`); columns are a
|
|
1747
2375
|
foreign `Origin` with the keeper's session (→ 403 `forbidden_origin`), an
|
|
1748
2376
|
over-cap declared length with a foreign `Origin` and no credential (→ 413
|
|
1749
|
-
`payload_too_large`, body-carrying rows only
|
|
2377
|
+
`payload_too_large`, body-carrying rows only; one byte past the spine default,
|
|
2378
|
+
or past `rpc_max_body_bytes` on the RPC row), no credential (→ 401
|
|
1750
2379
|
`authentication_required`), and a narrowed bearer with no `Origin` (→ 403
|
|
1751
2380
|
`token_scope_required` naming `rpc:<method>` on RPC and `surface:ws_upgrade` on
|
|
1752
2381
|
the upgrade; 403 `credential_type_required` on the session-only REST route and
|
|
@@ -1756,12 +2385,17 @@ envelope's `error.data`. A WS refusal's body is required where the backend
|
|
|
1756
2385
|
declares `capabilities.ws_refusal_body`; where it doesn't (Node's
|
|
1757
2386
|
`@hono/node-ws` drops the body), an empty body passes on its status and a sent
|
|
1758
2387
|
one is still asserted. The table
|
|
1759
|
-
is the exported `transport_composition_cases` data.
|
|
2388
|
+
is the exported `transport_composition_cases` data. `multi_actor:
|
|
2389
|
+
{setup_test, xfail?}` adds `transport_composition_multi_actor_cases`, run on
|
|
2390
|
+
a fixture whose keeper has `extra_actors`: the keeper's session upgrading with
|
|
2391
|
+
no `?acting=` → 400 `actor_required` (flat body), the TS upgrade's actor
|
|
2392
|
+
resolution. The Rust spine's upgrade resolves no actor and admits the socket,
|
|
2393
|
+
so fuz_app passes `xfail` (tracking id `rust-ws-upgrade-acting-fail-open`) for
|
|
2394
|
+
the Rust stub only. Given `base_url`, every
|
|
1760
2395
|
probe is written to the socket (`raw_http_exchange`) and an over-cap request
|
|
1761
2396
|
sends its head alone; with `base_url: null` the probes go through the
|
|
1762
2397
|
fixture's transport and the WS rows skip. Not in the standard bundle. fuz_app's
|
|
1763
|
-
|
|
1764
|
-
the in-process `src/test/auth/transport_composition_parity.db.test.ts`.
|
|
2398
|
+
in-process leg is `src/test/auth/transport_composition_parity.db.test.ts`.
|
|
1765
2399
|
|
|
1766
2400
|
`cross_backend/narrowed_api_token.ts` — `mint_narrowed_api_token({app,
|
|
1767
2401
|
rpc_path, session_headers, methods, name?})` mints a method-scoped api token
|
|
@@ -1773,7 +2407,7 @@ principal.
|
|
|
1773
2407
|
### Credential deadline parity — `cross_backend/credential_deadline.ts`
|
|
1774
2408
|
|
|
1775
2409
|
`describe_credential_deadline_cross_tests({setup_test, capabilities, base_url,
|
|
1776
|
-
ws_path, sse_path?, rpc_path?})` — a connection closes when the credential it
|
|
2410
|
+
ws_path, sse_path?, rpc_path?, ws_account_roles?})` — a connection closes when the credential it
|
|
1777
2411
|
opened on expires: each case pulls a fresh account's credential expiries in to
|
|
1778
2412
|
two seconds with `_testing_shorten_credential_expiry` before the open, then
|
|
1779
2413
|
checks the connection still dispatches short of the deadline and closes at it.
|
|
@@ -1783,17 +2417,24 @@ stream on a session (`capabilities.sse`) ends; and a second seam call asking
|
|
|
1783
2417
|
for a later expiry moves nothing (the seam only shortens). API token × audit
|
|
1784
2418
|
stream is not reachable over the wire — the stock stream is session-only — so
|
|
1785
2419
|
the Rust spine pins it on a widened gate and the TS twin shares one deadline
|
|
1786
|
-
wait across both transports.
|
|
1787
|
-
`
|
|
2420
|
+
wait across both transports. The cases run concurrently (`describe.concurrent`)
|
|
2421
|
+
under one `setup_test()` for the suite, each on its own account, so a leg waits
|
|
2422
|
+
out one deadline rather than one per case. `ws_account_roles` gives the
|
|
2423
|
+
socket cases' accounts roles, for a backend whose WS upgrade requires one.
|
|
2424
|
+
Cross-process only.
|
|
1788
2425
|
|
|
1789
2426
|
### Origin check scope parity — `cross_backend/origin_scope.ts`
|
|
1790
2427
|
|
|
1791
|
-
`describe_origin_scope_cross_tests({setup_test, capabilities, probe_path
|
|
1792
|
-
the origin check covers paths outside `/api`:
|
|
1793
|
-
foreign `Origin` → 403 `forbidden_origin`, an
|
|
1794
|
-
on `capabilities.ready`.
|
|
2428
|
+
`describe_origin_scope_cross_tests({setup_test, capabilities, probe_path?,
|
|
2429
|
+
base_url?, unrouted_status?})` — the origin check covers paths outside `/api`:
|
|
2430
|
+
on `GET /ready` (`probe_path`), a foreign `Origin` → 403 `forbidden_origin`, an
|
|
2431
|
+
absent or allowed one → 200, gated on `capabilities.ready`. On an unrouted
|
|
2432
|
+
path, ungated: a foreign `Origin` → 403, an absent one passes to the fallback
|
|
2433
|
+
(`unrouted_status`, default 404 — a backend whose fallback serves an SPA shell
|
|
2434
|
+
passes 200; the in-process leg runs that shape too), and an over-cap `POST`
|
|
2435
|
+
with a foreign `Origin` → 413. Runs both legs (the in-process
|
|
1795
2436
|
`cross_backend/origin_scope_parity.db.test.ts` + the cross-process
|
|
1796
|
-
|
|
2437
|
+
one call).
|
|
1797
2438
|
|
|
1798
2439
|
### Origin verification parity — `cross_backend/origin.ts`
|
|
1799
2440
|
|
|
@@ -1803,7 +2444,7 @@ imperative Origin-verification suite: absent `Origin` → request passes
|
|
|
1803
2444
|
table's. Imperative (not a conformance-table row) because absent-Origin needs
|
|
1804
2445
|
`fresh_transport({origin: null})`. Runs both legs (the
|
|
1805
2446
|
in-process `auth/origin_parity.db.test.ts` + the cross-process
|
|
1806
|
-
|
|
2447
|
+
one call). The promotion surfaced a twin-impl divergence — the
|
|
1807
2448
|
Rust spine returned a plain-text body — now converged to the canonical TS
|
|
1808
2449
|
`{error: "forbidden_origin"}` via `fuz_http::forbidden_origin_response()`.
|
|
1809
2450
|
|
|
@@ -1823,7 +2464,6 @@ keeper's daemon-token channel; the TS unit leg runs the same rows through
|
|
|
1823
2464
|
`run_testing_origin_check` (`src/test/http/origin.conformance.test.ts`), so the
|
|
1824
2465
|
Rust spine is held to the table without reading the file. Ungated; edit the
|
|
1825
2466
|
table by hand — it is the statement of the grammar, not generated output.
|
|
1826
|
-
fuz_app's own wiring is `src/test/cross_backend/origin_conformance.cross.test.ts`.
|
|
1827
2467
|
|
|
1828
2468
|
### CORS simple-request parity — `cross_backend/simple_request.ts`
|
|
1829
2469
|
|
|
@@ -1839,7 +2479,7 @@ and login, form-encoded login, a forged `text/plain` logout with a session
|
|
|
1839
2479
|
`application/json` with a charset is dispatched. The TS spine checks per route
|
|
1840
2480
|
(`apply_route_specs`), the Rust spine in each JSON handler. Runs both legs:
|
|
1841
2481
|
the in-process `auth/simple_request_parity.db.test.ts` and the cross-process
|
|
1842
|
-
|
|
2482
|
+
one call.
|
|
1843
2483
|
|
|
1844
2484
|
### Query-string shape parity — `cross_backend/query_shape.ts`
|
|
1845
2485
|
|
|
@@ -1850,7 +2490,7 @@ suite over the three spine surfaces that read a query:
|
|
|
1850
2490
|
unknown keys ignored, anonymous junk query → 401 never a query 400), the
|
|
1851
2491
|
JSON-RPC GET endpoint (`method` / `id` / `params` first-wins — a last-wins
|
|
1852
2492
|
reader dispatches a different method per backend on the same request), and
|
|
1853
|
-
`GET /api/facts/:hash` (gated on `capabilities.
|
|
2493
|
+
`GET /api/facts/:hash` (gated on `capabilities.fact_serving_admin_hash`; strict schema —
|
|
1854
2494
|
unknown key / malformed `acting` → 400 `invalid_query_params` **before** the
|
|
1855
2495
|
401 guard, duplicated `acting` first-wins). Imperative because duplicated
|
|
1856
2496
|
query keys can't be expressed through the structured RPC helpers. Pinned
|
|
@@ -1859,8 +2499,7 @@ after the Rust spine's derived-serde `Query` extractors were converged onto
|
|
|
1859
2499
|
"duplicate field" 400 is an anonymously observable backend fingerprint).
|
|
1860
2500
|
Cross-process only — the per-surface in-process TS pins live beside each
|
|
1861
2501
|
module's own tests (`account_status.test.ts`, `action_rpc.test.ts`,
|
|
1862
|
-
`serve_fact_route.db.test.ts`);
|
|
1863
|
-
`cross_backend/query_shape.cross.test.ts`.
|
|
2502
|
+
`serve_fact_route.db.test.ts`);
|
|
1864
2503
|
|
|
1865
2504
|
### Grant-verb role grammar parity — `cross_backend/role_grant_role_grammar.ts`
|
|
1866
2505
|
|
|
@@ -1875,8 +2514,7 @@ vacuous, and the same refusal for a role exactly at the length cap pins that
|
|
|
1875
2514
|
boundary. Exists because the action-manifest parity gate captures no param
|
|
1876
2515
|
schemas while the Rust spine parses params by hand, and the grant verbs write
|
|
1877
2516
|
the role they refuse into their failure rows. Ungated; cross-process only (it
|
|
1878
|
-
reads the audit log behind `_testing_drain_effects`).
|
|
1879
|
-
`src/test/cross_backend/role_grant_role_grammar.cross.test.ts`.
|
|
2517
|
+
reads the audit log behind `_testing_drain_effects`).
|
|
1880
2518
|
|
|
1881
2519
|
### API-token lifetime parity — `cross_backend/token_lifetime.ts`
|
|
1882
2520
|
|
|
@@ -1891,7 +2529,7 @@ schemas while the Rust spine hand-parses params — a spine that silently
|
|
|
1891
2529
|
dropped the field would answer the same mint with a 200 and an eternal
|
|
1892
2530
|
token; only the stored-state read-back catches it. Ungated (mint + list are
|
|
1893
2531
|
on every spine's standard surface). Runs both legs
|
|
1894
|
-
(`token_lifetime_parity.db.test.ts` +
|
|
2532
|
+
(`token_lifetime_parity.db.test.ts` + the cross-process one call).
|
|
1895
2533
|
|
|
1896
2534
|
### Readiness probe parity — `cross_backend/ready.ts`
|
|
1897
2535
|
|
|
@@ -1906,7 +2544,7 @@ success-path gate. Gated on `capabilities.ready`. Imperative (not a
|
|
|
1906
2544
|
JSON-RPC envelope, and the probe needs `fresh_transport({origin: null})` —
|
|
1907
2545
|
the same reasons the sibling `cross_backend/origin.ts` suite is imperative.
|
|
1908
2546
|
Runs both legs (`cross_backend/ready_parity.db.test.ts` +
|
|
1909
|
-
|
|
2547
|
+
the cross-process one call).
|
|
1910
2548
|
|
|
1911
2549
|
Both backends read the **same** committed
|
|
1912
2550
|
`testing/cross_backend/expected_schema.json` (column-presence is engine-portable,
|
|
@@ -1931,15 +2569,35 @@ same MAC under `atob`), an unpadded signature, and correctly signed
|
|
|
1931
2569
|
with `authentication_required`. Controls: re-signing the keeper cookie's
|
|
1932
2570
|
payload reproduces it byte for byte and it authenticates, and a leading-zero
|
|
1933
2571
|
`0N` authenticates (the `u64` bound is on the value, not the digit count).
|
|
1934
|
-
Ungated; cross-process only.
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2572
|
+
Ungated; cross-process only.
|
|
2573
|
+
|
|
2574
|
+
### Testing-backdoor declaration + credential gate — `cross_backend/testing_backdoor.ts`
|
|
2575
|
+
|
|
2576
|
+
`describe_testing_backdoor_cross_tests({setup_test, capabilities, rpc_path?,
|
|
2577
|
+
ws?, extra_methods?})` — the backend's backdoor declaration, then the
|
|
2578
|
+
negative-credential parity suite for the `_testing_*` backdoor actions.
|
|
2579
|
+
|
|
2580
|
+
**Declaration.** `capabilities.testing_backdoors` lists the backdoors the
|
|
2581
|
+
backend mounts, from `TESTING_BACKDOOR_METHODS` (`spine_surface_constants.ts`,
|
|
2582
|
+
the one list; `spine_surface_constants.testing_backdoors.test.ts` holds it to
|
|
2583
|
+
the `_testing_*` actions fuz_app's spine binary mounts). Each known backdoor
|
|
2584
|
+
the list leaves out is sent anonymously and must answer `method_not_found`
|
|
2585
|
+
(404): both spines look the method up before they authenticate (the TS
|
|
2586
|
+
dispatcher's lookup step, the Rust `perform_action`'s), so an unmounted method
|
|
2587
|
+
answers that whatever the credential, and a mounted one a 401. Each declared
|
|
2588
|
+
one gets the rows below, whose anonymous row fails as "declared but not
|
|
2589
|
+
mounted" on a `method_not_found`. So the declaration can't lie in either
|
|
2590
|
+
direction, and the suites gated on it skip only what the backend lacks. A
|
|
2591
|
+
declared name outside the known list, or a duplicate, throws at collection.
|
|
2592
|
+
The suite runs on every backend, one declaring only the required backdoors
|
|
2593
|
+
included — `describe_spine_cross_suites` refuses to exclude it, skip any of
|
|
2594
|
+
its cases, or hand it other `capabilities` through `overrides`.
|
|
2595
|
+
|
|
2596
|
+
**Credential gate.** For each declared backdoor (the privileged writes, the
|
|
2597
|
+
audit barrier, the connection flood, and the introspection reads), and each
|
|
2598
|
+
`extra_methods` entry (`TestingBackdoorProbe` `{method, params?}` — a
|
|
2599
|
+
consumer's own daemon-token seam, which must start with `_testing_` and not be
|
|
2600
|
+
a known backdoor), it fires three principals over real HTTP: **anonymous** →
|
|
1943
2601
|
401 `unauthenticated`, **session** → 403 `forbidden` `credential_type_required`,
|
|
1944
2602
|
**bearer** → 403 `forbidden` `credential_type_required` — and, when `ws`
|
|
1945
2603
|
(`{base_url, ws_path, origin?}`) is passed, sends each on a socket opened with
|
|
@@ -1950,15 +2608,23 @@ on the real dispatcher (the spec-derived `describe_rpc_attack_surface_tests`
|
|
|
1950
2608
|
never enumerates them because they're off the declared surface). Each method is
|
|
1951
2609
|
sent with **valid** params so each case varies only the credential (validation
|
|
1952
2610
|
runs after the gates — order is 401 → authz → 403 → 429 → 400); the handler never
|
|
1953
|
-
runs.
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
2611
|
+
runs. The known backdoors' params are a `Record<TestingBackdoorMethod, …>`, so a
|
|
2612
|
+
backdoor added to the list fails typecheck until it has them. Cited property:
|
|
2613
|
+
`docs/security.md` §Test Backdoor Actions. The credential rows run
|
|
2614
|
+
cross-process only (the `_testing_*` actions are mounted on the spawned binary,
|
|
2615
|
+
not the in-process app — like the ws/sse suites); in-process, an app mounting
|
|
2616
|
+
none declares none and gets the declaration rows alone, one per known backdoor
|
|
2617
|
+
— fuz_app's in-process leg is `cross_backend/testing_backdoor_parity.db.test.ts`,
|
|
2618
|
+
while its cross legs declare every backdoor, so they run no declaration row.
|
|
2619
|
+
The complement is the
|
|
1959
2620
|
in-process spec-level gate test (`src/test/testing/testing_actions_auth.test.ts`,
|
|
1960
2621
|
asserting each spec declares `credential_types: ['daemon_token']`) + the
|
|
1961
|
-
`assert_no_testing_methods` surface invariant.
|
|
2622
|
+
`assert_no_testing_methods` surface invariant. fuz_app's conformance table
|
|
2623
|
+
also covers the same gates, through the auth rows it generates over the live
|
|
2624
|
+
mount (`live_rpc_endpoints`), which the differential compares across spines;
|
|
2625
|
+
this suite stays for consumers whose backends mount the `_testing_*` actions
|
|
2626
|
+
without a conformance table over their live mount, and for the declaration
|
|
2627
|
+
check.
|
|
1962
2628
|
|
|
1963
2629
|
### Identity-primitive parity — `cross_backend/identity_parity.ts`
|
|
1964
2630
|
|
|
@@ -2015,7 +2681,7 @@ USERNAME_LENGTH_MAX]` = `[3, 39]` boundary edges (just-outside → 400,
|
|
|
2015
2681
|
|
|
2016
2682
|
Ungated (login + signup are on every spine). Runs both legs via the shared
|
|
2017
2683
|
`{setup_test}` protocol: in-process (`identity_parity.db.test.ts`, plain
|
|
2018
|
-
`gro test`) and cross-process (
|
|
2684
|
+
`gro test`) and cross-process (the one call).
|
|
2019
2685
|
The accepted-case 403s rely on `create_spine_route_specs` zeroing
|
|
2020
2686
|
`signup_fail_floor_ms` (mirroring `login_fail_floor_ms: 0`) so they don't each
|
|
2021
2687
|
pay the 250ms denial floor.
|
|
@@ -2146,6 +2812,15 @@ need a real recipient): admin **assigns** `participant` to a fresh account
|
|
|
2146
2812
|
`participant` → recipient **accepts** → a role_grant lands. The `role_holder`
|
|
2147
2813
|
principal is seeded via `extra_accounts: [{username, roles: ['participant']}]`
|
|
2148
2814
|
(bootstrap-cradle direct grant) and named through `principals.role_holder`.
|
|
2815
|
+
The replay cases count audit rows behind `_testing_drain_effects`, so the
|
|
2816
|
+
backend must mount it — every spine binary does; an in-process leg mounts
|
|
2817
|
+
`create_testing_drain_effects_action` over an emitter built with
|
|
2818
|
+
`track_inflight: true` (`conformance_participation.db.test.ts`).
|
|
2819
|
+
`role` (default `SPINE_PARTICIPANT_ROLE`) names the role conferred, which must
|
|
2820
|
+
be admin-grantable in the backend's registry; `scope_kind` (default `'space'`,
|
|
2821
|
+
or `null` for a builtin role) is the scope the scoped-assign case uses, and
|
|
2822
|
+
`null` skips it. A backend with only the builtin roles runs the suite with
|
|
2823
|
+
`ROLE_ADMIN` — the scoped case skips, as a builtin is global-only.
|
|
2149
2824
|
|
|
2150
2825
|
Both legs run in-process (`conformance_participation.db.test.ts`, plain
|
|
2151
2826
|
`gro test`) and cross-process (`conformance_participation.cross.test.ts`, TS
|
|
@@ -2154,6 +2829,38 @@ the `cross_backend_parity` action-manifest gate green (it was Rust-only before).
|
|
|
2154
2829
|
Cited property: `docs/identity.md` + the role-gated-participation design
|
|
2155
2830
|
(admin-only conferral, immediate assign).
|
|
2156
2831
|
|
|
2832
|
+
### Offer decline + retract refusals — `cross_backend/role_grant_offer_lifecycle.ts`
|
|
2833
|
+
|
|
2834
|
+
`describe_role_grant_offer_lifecycle_cross_tests({setup_test, rpc_path?, role?})` —
|
|
2835
|
+
the error paths of the two verbs that end a pending offer without a grant,
|
|
2836
|
+
which the WS notification suite drives only on the happy path. Canonical
|
|
2837
|
+
answers, from the handlers and the queries behind them
|
|
2838
|
+
(`auth/role_grant_offer_actions.ts`, `auth/role_grant_offer_queries.ts`):
|
|
2839
|
+
|
|
2840
|
+
- **not a party → 404** `not_found` with reason `role_grant_offer_not_found`.
|
|
2841
|
+
Decline is scoped to the recipient account and retract to the grantor actor,
|
|
2842
|
+
so a stranger, the grantor declining, or the recipient retracting hears what
|
|
2843
|
+
a missing offer answers — the case compares the two refusals whole — and the
|
|
2844
|
+
offer stays pending (`docs/security.md` §"404-over-403 is the general mask").
|
|
2845
|
+
The scope check runs before the terminal check, so a stranger hears 404 on
|
|
2846
|
+
a decided offer too.
|
|
2847
|
+
- **decided → 400** `invalid_request` with reason `role_grant_offer_terminal`:
|
|
2848
|
+
a decline or retract after an accept, a second decline, a second retract,
|
|
2849
|
+
and either after the other.
|
|
2850
|
+
- **audit**: a decline writes one `role_grant_offer_decline` row
|
|
2851
|
+
(`account_id` / `actor_id` the recipient, `target_*` the grantor, metadata
|
|
2852
|
+
`{offer_id, role, scope_id}` plus `reason` only when one was given), a
|
|
2853
|
+
retract one `role_grant_offer_retract` row (the grantor, targeting the
|
|
2854
|
+
recipient, no target actor on an account-grain offer); refusals write none.
|
|
2855
|
+
Read through `list_audit_events`.
|
|
2856
|
+
|
|
2857
|
+
The single-request rows — decline and retract of an id naming no offer → 404
|
|
2858
|
+
— are in the conformance security slate, so the differential sends them to
|
|
2859
|
+
both spines. The keeper is the grantor and offers `role` — an admin-grantable
|
|
2860
|
+
role, `SPINE_PARTICIPANT_ROLE` by default; `ROLE_ADMIN` serves a backend with
|
|
2861
|
+
only the builtin roles. Ungated. The
|
|
2862
|
+
in-process leg is on the drained setup of `conformance_participation.db.test.ts`.
|
|
2863
|
+
|
|
2157
2864
|
### Building a TS test-server binary — `testing_server_core.ts` + adapters
|
|
2158
2865
|
|
|
2159
2866
|
The reusable shape for standing up a **spawnable TS** cross-process test
|
|
@@ -2165,7 +2872,7 @@ re-roll the serve / daemon-info / WS-attach / drain boilerplate:
|
|
|
2165
2872
|
- `testing/cross_backend/testing_server_deno.ts` — `create_deno_testing_adapter()` (`Deno.serve` + `@hono/deno`, an optional peer dep; `Deno` declared locally so it typechecks under the Node toolchain). Spawn the entry with `--sloppy-imports` (Deno doesn't do `.js`→`.ts`; Gro's loader does, so the Node path needs no flag).
|
|
2166
2873
|
- `testing/cross_backend/testing_server_bun.ts` — `create_bun_testing_adapter()` (`Bun.serve` + `@hono/bun`'s module-level `getConnInfo`, `upgradeWebSocket`, and `websocket`; `Bun.serve` declared locally so it typechecks under the Node toolchain). Needs the optional `@hono/bun` peer dep (the counterpart to Node's `@hono/node-server` + `@hono/node-ws`; `Bun.serve` is built in), and Bun resolves `.js`→`.ts` natively (no flag, unlike Deno). Reuses `create_node_runtime` (Bun implements the `node:fs`/`node:process` surface). WS is module-level + stateless (like Deno) — the `websocket` handler is threaded into `serve`, where `Bun.serve` wants it, so no post-serve attach. It carries `maxPayloadLength` at twice `max_message_bytes` rather than Bun's 16 MiB default: past its cap Bun drops the socket without a close frame (1006), so the headroom leaves a message just over the limit to the per-message check (`WS_CLOSE_MESSAGE_TOO_BIG`, as on Node), while a grossly oversized one is refused before Bun buffers it whole.
|
|
2167
2874
|
- `testing/cross_backend/default_spine_surface.ts` — the canonical no-domain spine surface (account/admin/audit/signup + bootstrap): `spine_session_options`, `spine_roles`, `create_spine_route_specs`, `spine_rpc_endpoints`, `create_spine_surface_spec`. This is the **declared** surface — the `create_standard_rpc_actions` bundle the spec-derived suites auto-enumerate. It passes `daemon_token: true` (both spine binaries mount the daemon-token layer) and leaves `GET /api/surface` at its default, off — the TS spine binary does too, and the Rust `testing_spine_stub` serves none. Shared by the spine_stub cross test, the TS cross tests, and the binary.
|
|
2168
|
-
- `testing/cross_backend/full_spine_mount.ts` — `build_full_spine_rpc_actions(deps, options)` — the **full** live RPC mount: the declared bundle **plus** the off-declared-surface families the binary live-mounts (`_testing_*` backdoors, the cell verb set, the opt-in `actor_lookup` / `actor_search` resolvers). `build_full_spine_mount(deps, options)` builds it once for both of the binary's endpoints, returning `FullSpineMount` (`{rpc_actions, ws_actions}`): the WS list is `protocol_actions` plus every RPC action not already among them, twinning the Rust `testing_spine_stub`, which serves one registry on RPC and WS (the one difference is `cancel` — a registered protocol action on TS, owned by the read loop on Rust). Single-sources the mount so the binary and the `spine_method_coverage` reconciliation test build the same list.
|
|
2875
|
+
- `testing/cross_backend/full_spine_mount.ts` — `build_full_spine_rpc_actions(deps, options)` — the **full** live RPC mount: the declared bundle **plus** the off-declared-surface families the binary live-mounts (`_testing_*` backdoors, the cell verb set, the opt-in `actor_lookup` / `actor_search` resolvers). `build_full_spine_mount(deps, options)` builds it once for both of the binary's endpoints, returning `FullSpineMount` (`{rpc_actions, ws_actions}`): the WS list is `protocol_actions` plus every RPC action not already among them, twinning the Rust `testing_spine_stub`, which serves one registry on RPC and WS (the one difference is `cancel` — a registered protocol action on TS, owned by the read loop on Rust). Single-sources the mount so the binary and the `spine_method_coverage` reconciliation test build the same list. `create_spine_live_rpc_endpoints()` wraps the list, built on stub deps, as one endpoint at `SPINE_RPC_PATH` — the `live_rpc_endpoints` value the conformance table, the differential and the auth-row generator resolve off-surface methods from (no handler built there runs).
|
|
2169
2876
|
- `testing/cross_backend/ts_spine_backend_config.ts` — `ts_spine_node_backend_config()` / `ts_spine_deno_backend_config()` / `ts_spine_bun_backend_config()` presets (in-memory pglet, no external infra), the TS analog of `rust_spine_stub_backend_config()`.
|
|
2170
2877
|
|
|
2171
2878
|
fuz_app's own binary wiring (`src/test/cross_backend/testing_spine_server{,_node,_deno,_bun}.ts`) is the worked example: ~one `build_app` over `create_app_backend` + `create_app_server` (`rate_limiters: 'disabled_for_testing'` plus whichever login / action limiters its env toggles built, `build_full_spine_mount`'s two lists on one `rpc_endpoints` entry and two `ws_endpoints` entries — `/api/ws` and the role-gated `SPINE_ROLE_GATED_WS_PATH` — sharing one `transport`, which is also the role-grant-offer `notification_sender`), reusing `default_spine_surface`. `src/test/cross_backend/testing_spine_server.db.test.ts` builds it with the Node adapter in-process and asserts its surface lists both WS endpoints (the gated one with its `required_roles`) and passes the surface invariants. The `_node`/`_deno`/`_bun` entries differ only in which adapter they wire — `build_spine_app` is runtime-agnostic. It leaves `create_app_server`'s `auth_cleanup` off, as the Rust `testing_spine_stub` leaves its twin unscheduled: a background pass would delete rows and write audit rows under a running suite. A consumer's cross-process test binary may schedule it when nothing its suites seed is already expired (the conformance table's `expired_session` principal seeds a backdated session row); an in-process harness leaves it off.
|