@fuzdev/fuz_app 0.147.0 → 0.148.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 +6 -4
- 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/perform_action.js +1 -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 +3 -1
- 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 +20 -0
- package/dist/schema_meta.d.ts.map +1 -1
- package/dist/schema_meta.js +7 -1
- package/dist/testing/CLAUDE.md +720 -114
- 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 +64 -0
- package/dist/testing/adversarial_input_cases.d.ts.map +1 -0
- package/dist/testing/adversarial_input_cases.js +348 -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 +27 -1
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +50 -0
- 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 +51 -0
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/audit_cross_helpers.js +78 -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 +55 -59
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +20 -23
- 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 +57 -1
- 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} +12 -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 +56 -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 +38 -0
- package/dist/testing/cross_backend/rpc_adversarial.d.ts.map +1 -0
- package/dist/testing/cross_backend/rpc_adversarial.js +292 -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 +27 -18
- 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 +56 -54
- 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 +231 -0
- package/dist/testing/cross_backend/spine_suites.d.ts.map +1 -0
- package/dist/testing/cross_backend/spine_suites.js +626 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +29 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +29 -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 +2 -8
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +7 -10
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +3 -2
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +8 -4
- 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 +17 -10
- 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.map +1 -1
- package/dist/testing/rpc_attack_surface.js +13 -138
- package/dist/testing/rpc_envelope_cases.d.ts +111 -0
- package/dist/testing/rpc_envelope_cases.d.ts.map +1 -0
- package/dist/testing/rpc_envelope_cases.js +240 -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.map +1 -1
- package/dist/testing/schema_generators.js +22 -2
- 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/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/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
|
@@ -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
|
|
@@ -432,7 +523,7 @@ Also exported: `describe_adversarial_auth(options)` (group 3 on its own) and
|
|
|
432
523
|
`build_error_schema_lookup(specs, middleware_specs?)` (pre-built
|
|
433
524
|
`Map<string, RouteErrorSchemas>` for per-response validation).
|
|
434
525
|
|
|
435
|
-
### `adversarial_input.ts` — schema-walk payload generation
|
|
526
|
+
### `adversarial_input.ts` + `adversarial_input_cases.ts` — schema-walk payload generation
|
|
436
527
|
|
|
437
528
|
`describe_adversarial_input({build, roles})` — fires input body / params /
|
|
438
529
|
query validation failures at every route with correct-auth credentials so
|
|
@@ -440,11 +531,12 @@ validation middleware is actually exercised (not short-circuited by 401).
|
|
|
440
531
|
All cases expect 400 with one of `ERROR_INVALID_REQUEST_BODY` /
|
|
441
532
|
`_INVALID_JSON_BODY` / `_INVALID_ROUTE_PARAMS` / `_INVALID_QUERY_PARAMS`.
|
|
442
533
|
|
|
443
|
-
|
|
534
|
+
The generators live in the hono-free `adversarial_input_cases.ts`, so the
|
|
535
|
+
cross-process suites reuse them:
|
|
444
536
|
|
|
445
|
-
- `generate_input_test_cases(input_schema)` — 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, numeric/array/string boundary cases via JSON Schema introspection.
|
|
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.
|
|
537
|
+
- `generate_input_test_cases(input_schema)` → `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, numeric/array/string boundary cases via JSON Schema introspection.
|
|
538
|
+
- `generate_params_test_cases(params_schema)` → `ParamsTestCase[]` — format violations only (unconstrained string params accept anything).
|
|
539
|
+
- `generate_query_test_cases(query_schema)` → `QueryTestCase[]` — missing required + format violations.
|
|
448
540
|
|
|
449
541
|
GET-with-input routes hit the RPC `?params=` query convention; invalid-JSON
|
|
450
542
|
arrays there collapse to `ERROR_INVALID_REQUEST_BODY` (schema failure)
|
|
@@ -509,9 +601,15 @@ the response against declared schemas. DB-backed via `create_test_app`.
|
|
|
509
601
|
Per-route test (`test.each`) — one line per route in the vitest output.
|
|
510
602
|
|
|
511
603
|
Options: `{setup_test, surface_source, skip_routes?, input_overrides?, success_fixtures?}`.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
604
|
+
Principals and the 401 check are the RPC round-trip's (`pick_auth_principal`;
|
|
605
|
+
`assert_route_authenticated` over `is_unauthenticated_refusal`, reading the
|
|
606
|
+
flat `error`), and a non-`GET` route called as the authed
|
|
607
|
+
account runs on a fresh account — a generated logout would otherwise end the
|
|
608
|
+
shared session, leaving `/password` and every later authed route a vacuous
|
|
609
|
+
401. `src/test/testing/round_trip.test.ts` pins both, the RPC test's twin.
|
|
610
|
+
`input_overrides` is a `Map<"METHOD /path", body>` — override generated bodies
|
|
611
|
+
for routes whose input schema can't round-trip cleanly (e.g. fields that must
|
|
612
|
+
reference DB state).
|
|
515
613
|
|
|
516
614
|
SSE routes are skipped by Content-Type sniff; `describe_sse_route_tests`
|
|
517
615
|
picks them up separately.
|
|
@@ -521,9 +619,62 @@ picks them up separately.
|
|
|
521
619
|
DB-backed round-trip for RPC: one POST test for all methods, one GET test
|
|
522
620
|
for `side_effects: false` methods. Successful responses validate against
|
|
523
621
|
`action.spec.output`; error responses validate as well-formed JSON-RPC error
|
|
524
|
-
envelopes.
|
|
525
|
-
|
|
526
|
-
|
|
622
|
+
envelopes. Three refusals fail instead of passing as an envelope: `method not
|
|
623
|
+
found` (the backend lacks a method the surface declares), a crash — HTTP
|
|
624
|
+
500, the `internal_error` code (-32603), or a ≥500 whose body is no JSON-RPC
|
|
625
|
+
envelope — and a 401 to a non-public call (`assert_rpc_authenticated`): the
|
|
626
|
+
call carried a credential its auth admits, so a 401 means it stopped working.
|
|
627
|
+
A public method may still answer 401 (a wrong-password login), and so may a
|
|
628
|
+
handler naming its own reason (below). A ≥500 carrying a declared code
|
|
629
|
+
(`service_unavailable` 503, `timeout` 504, a consumer code mapped in
|
|
630
|
+
`JSONRPC_ERROR_CODE_TO_HTTP_STATUS`) is a server condition and passes like a
|
|
631
|
+
4xx. The failure names the method, status, code and message; a non-JSON body
|
|
632
|
+
is reported as text. Options: `{setup_test, surface_source, session_options,
|
|
633
|
+
rpc_endpoints, skip_methods?, input_overrides?, success_fixtures?}`.
|
|
634
|
+
`pick_rpc_auth` picks the principal from `method.auth` through
|
|
635
|
+
`pick_auth_principal` (`integration_helpers.ts`, shared with the REST
|
|
636
|
+
round-trip; `AuthPrincipal`): none for a public method, the keeper's daemon
|
|
637
|
+
token when `credential_types` names it, the admin account's session for an
|
|
638
|
+
admin-gated method, the keeper's session for another role, else the authed
|
|
639
|
+
account's session. The 401 check is `is_unauthenticated_refusal`: a 401
|
|
640
|
+
reading `authentication_required` or naming no reason fails; a handler's own
|
|
641
|
+
401 reason on an authenticated call (`invalid_credentials` for a wrong current
|
|
642
|
+
password) passes. So a handler that answers 401 to a valid credential must name
|
|
643
|
+
its reason — `data.reason` over RPC, the flat `error` over REST: a reason-less
|
|
644
|
+
401 reads as unauthenticated and fails the round-trip. TS's
|
|
645
|
+
`jsonrpc_errors.unauthenticated(message)` sends no `data` unless given one, so
|
|
646
|
+
pass `{reason}` as its second argument.
|
|
647
|
+
|
|
648
|
+
A side-effecting method picked for the authed account runs on a fresh account
|
|
649
|
+
of its own (`round_trip_uses_fresh_account`, keyed on the spec's
|
|
650
|
+
`side_effects`): generated params omit optional fields, and an omitted target
|
|
651
|
+
on a self-service method is the caller — `account_delete` with no
|
|
652
|
+
`account_id` deletes it, `account_session_revoke_all` ends its session — which
|
|
653
|
+
on the shared account would turn every later account method into a 401. Keyed
|
|
654
|
+
on `side_effects` rather than a list of known destroyers, so a consumer's
|
|
655
|
+
self-targeting method is covered without registering it. The keeper, its
|
|
656
|
+
daemon token and the admin account stay shared (one keeper per fixture, and
|
|
657
|
+
admin methods with generated nil ids target no existing account); a
|
|
658
|
+
credential they lose still fails on the 401 check.
|
|
659
|
+
`src/test/testing/rpc_round_trip.test.ts` pins both against a fake backend
|
|
660
|
+
whose side-effecting method ends the caller's session.
|
|
661
|
+
|
|
662
|
+
`success_fixtures` (`RpcSuccessFixtures`, method → `(fixture) =>
|
|
663
|
+
Promise<RpcSuccessFixtureCase>`) drives a **success** response for a method
|
|
664
|
+
whose generated nil ids can only refuse: the factory builds the row, then
|
|
665
|
+
returns `{params, headers?}` — `headers` replaces the picked identity, for a
|
|
666
|
+
row only its owner may act on. The response must be `ok`, not an internal
|
|
667
|
+
failure, and parse against `output`. `cross_backend/rpc_success_fixtures.ts`
|
|
668
|
+
— `create_spine_rpc_success_fixtures({rpc_path?})` builds the map for the
|
|
669
|
+
standard spine methods that act on an existing row (`account_token_revoke`,
|
|
670
|
+
`account_session_revoke`, `invite_delete`, `role_grant_revoke`,
|
|
671
|
+
`role_grant_offer_accept` / `_decline` / `_retract`) from standard RPC
|
|
672
|
+
actions and the builtin `ROLE_ADMIN` only, each factory on fresh accounts
|
|
673
|
+
and calling as one of them, so any consumer mounting
|
|
674
|
+
`create_standard_rpc_actions` can pass it. The fixtures run last on the
|
|
675
|
+
describe's fixture, since they mutate.
|
|
676
|
+
fuz_app wires it through the standard bundle and in the in-process
|
|
677
|
+
`cross_backend/rpc_round_trip_spine.db.test.ts`.
|
|
527
678
|
|
|
528
679
|
### `sse_round_trip.ts` — `describe_sse_route_tests`
|
|
529
680
|
|
|
@@ -666,7 +817,12 @@ lifecycle (via `role_grant_offer_create` + `role_grant_offer_accept` +
|
|
|
666
817
|
`role_grant_revoke` RPC flows — **not** REST, **not** direct
|
|
667
818
|
`query_accept_offer`; see `auth/CLAUDE.md` for
|
|
668
819
|
`role_grant_offer_action_specs.ts` + `role_grant_offer_actions.ts`),
|
|
669
|
-
session / token management, audit log reads (RPC
|
|
820
|
+
session / token management, audit log reads (RPC — every row read is
|
|
821
|
+
parsed by `assert_audit_event_metadata`, closed to undeclared keys when the
|
|
822
|
+
fixture asks, and the completeness test drives login, logout, the offer flow,
|
|
823
|
+
grant + revoke, token create, password change, and — each where the surface
|
|
824
|
+
mounts its actions — token revoke, session revoke, invite create + delete and
|
|
825
|
+
`app_settings_update`), admin-to-admin
|
|
670
826
|
isolation, error coverage, response schema validation.
|
|
671
827
|
|
|
672
828
|
The shared `role_grant_offer_and_accept` helper (`role_grant_helpers.ts`)
|
|
@@ -679,6 +835,12 @@ from `db_entities.ts`.
|
|
|
679
835
|
|
|
680
836
|
Required options: `{setup_test, surface_source, session_options, rpc_endpoints: RpcEndpointsSuiteOption, roles: RoleSchemaResult, admin_prefix?}`.
|
|
681
837
|
|
|
838
|
+
fuz_app runs it cross-process through the standard bundle
|
|
839
|
+
and in-process as a consumer wires it
|
|
840
|
+
(`cross_backend/admin_integration_spine.db.test.ts`) — the in-process fixture's
|
|
841
|
+
`create_account` inserts rows directly, writing no `invite_create` or `signup`
|
|
842
|
+
audit row, so the suite expects neither from it.
|
|
843
|
+
|
|
682
844
|
`rpc_endpoints` is `Array<RpcEndpointSpec> | ((ctx: AppServerContext) => Array<RpcEndpointSpec>)` —
|
|
683
845
|
the same `RpcEndpointsSuiteOption` union every DB-backed suite accepts
|
|
684
846
|
(`integration`, `admin_integration`, `audit_completeness`, `rate_limiting`,
|
|
@@ -763,6 +925,12 @@ invokes the right per-test fixture. Consumers wiring the standard surface
|
|
|
763
925
|
call once instead of seven times; forgetting a suite no longer silently
|
|
764
926
|
loses coverage.
|
|
765
927
|
|
|
928
|
+
`rpc_success_fixtures`, `rpc_skip_methods` and `rpc_input_overrides`
|
|
929
|
+
forward to the RPC round-trip's `success_fixtures`, `skip_methods` and
|
|
930
|
+
`input_overrides` (same options on `describe_standard_cross_process_tests`)
|
|
931
|
+
— `rpc_skip_methods` is the consumer escape hatch for a method the generic
|
|
932
|
+
round-trip can't drive in its test wiring.
|
|
933
|
+
|
|
766
934
|
`StandardTestOptions` requires `create_route_specs` (for rate_limiting) and
|
|
767
935
|
`rpc_endpoints` (for admin/audit_completeness/rpc_round_trip); the admin
|
|
768
936
|
suite's requirement is enforced at the type level so a missing
|
|
@@ -810,6 +978,7 @@ One-shot transport:
|
|
|
810
978
|
- `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
979
|
- `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
980
|
- `RpcCallArgs` — `{app, path, method, params?, headers?, id?, verb?}`. `verb` defaults to `'POST'`; use `'GET'` for `side_effects: false` methods.
|
|
981
|
+
- `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
982
|
- `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
983
|
- `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
984
|
- `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 +991,7 @@ Registry lookups:
|
|
|
822
991
|
- `RpcEndpointsSuiteOption` — union `Array<RpcEndpointSpec> | ((ctx: AppServerContext) => Array<RpcEndpointSpec>)` accepted by every DB-backed suite's `rpc_endpoints` field.
|
|
823
992
|
- `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
993
|
|
|
825
|
-
### `rpc_attack_surface.ts` — `describe_rpc_attack_surface_tests`
|
|
994
|
+
### `rpc_attack_surface.ts` + `rpc_envelope_cases.ts` — `describe_rpc_attack_surface_tests`
|
|
826
995
|
|
|
827
996
|
3 test groups for JSON-RPC endpoints:
|
|
828
997
|
|
|
@@ -833,8 +1002,8 @@ Registry lookups:
|
|
|
833
1002
|
- **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
1003
|
- correct auth passes (not 401/403)
|
|
835
1004
|
- GET unauthenticated for `side_effects: false` reads
|
|
836
|
-
2. **RPC adversarial envelopes** — fixed set exercising
|
|
837
|
-
3. **RPC adversarial params** —
|
|
1005
|
+
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.
|
|
1006
|
+
3. **RPC adversarial params** — `generate_rpc_params_test_cases(input)` (`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). Every case expects 400 `invalid_params`. Shared with the cross-process suite.
|
|
838
1007
|
|
|
839
1008
|
Skips silently when `surface.rpc_endpoints` is empty. Uses stub deps — no
|
|
840
1009
|
DB needed.
|
|
@@ -874,10 +1043,17 @@ test-server core below (fuz_app's own domain-free `testing_spine_server`, run
|
|
|
874
1043
|
on Node + Deno + Bun). In-process is the fast feedback path; cross-process is the
|
|
875
1044
|
source of truth for wire-shape conformance.
|
|
876
1045
|
|
|
1046
|
+
fuz_app's own spines run every single-backend suite below through one
|
|
1047
|
+
`describe_spine_cross_suites` call, `src/test/cross_backend/spine_suites.cross.test.ts`
|
|
1048
|
+
(§`cross_backend/spine_suites.ts`), so a suite's section names the suite, not a
|
|
1049
|
+
file; the dual-spawn and limiter-enabled suites name their own files.
|
|
1050
|
+
|
|
877
1051
|
### Fixture protocol + capabilities
|
|
878
1052
|
|
|
879
1053
|
- `testing/cross_backend/setup.ts` — `SetupTest` / `TestFixture` /
|
|
880
|
-
`
|
|
1054
|
+
`CreateTestAccountOptions` types (`fixture.create_account` and
|
|
1055
|
+
`fixture.extra_accounts` hand out `TestAccount`s from `app_server.ts`, built
|
|
1056
|
+
by `to_test_account`),
|
|
881
1057
|
`default_in_process_setup(options)` (wraps `create_test_app`; pass
|
|
882
1058
|
`migration_namespaces` for suites needing tables beyond the auth-only
|
|
883
1059
|
default — the cell parity suite passes `[CELL_MIGRATION_NS]`, and
|
|
@@ -913,7 +1089,11 @@ source of truth for wire-shape conformance.
|
|
|
913
1089
|
lands with an email set — the hook the username-or-email login parity suite
|
|
914
1090
|
uses; the in-process path sets it directly on the inserted row. Must be a
|
|
915
1091
|
valid, per-test-unique `Email` (the `LOWER(email)` partial-unique index
|
|
916
|
-
rejects collisions).
|
|
1092
|
+
rejects collisions). The signup and login it drives go to the setup's
|
|
1093
|
+
`signup_path` / `login_path` (default `SPINE_ACCOUNT_SIGNUP_PATH` /
|
|
1094
|
+
`SPINE_ACCOUNT_LOGIN_PATH`, from `spine_surface_constants.ts`, which also
|
|
1095
|
+
carries the other account routes, `SPINE_WS_PATH`, and the prefix).
|
|
1096
|
+
Caller-supplied `username`s pass through _as-is_ now that the DB
|
|
917
1097
|
wipes between tests — hardcoded names like `'user_two'` work and the
|
|
918
1098
|
earlier uniquification prefix is gone. Every `TestFixture` also exposes
|
|
919
1099
|
`fresh_transport({origin?: string | null})` — cookie-jar-free probe;
|
|
@@ -930,22 +1110,31 @@ source of truth for wire-shape conformance.
|
|
|
930
1110
|
|
|
931
1111
|
- `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
|
|
932
1112
|
(the **gating** flags `ws` / `sse` / `cell_crud` / `cell_relations` /
|
|
933
|
-
`cell_gated_create` / `account_lifecycle` / `fact_serving` /
|
|
1113
|
+
`cell_gated_create` / `account_lifecycle` / `fact_serving` /
|
|
1114
|
+
`fact_serving_admin_hash` / `ready` /
|
|
934
1115
|
`account_status` / `peer_request` /
|
|
935
1116
|
`ws_handshake_pipelining` / `ws_account_actions` /
|
|
936
1117
|
`ws_role_gated` / `ws_refusal_body` — each has a `test_if`
|
|
937
1118
|
reader (`ws_refusal_body` picks how the front-door table reads a WS
|
|
938
1119
|
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
|
-
|
|
1120
|
+
`peer/ping` round-trip) is `true` on fuz_app's Rust and TS spine presets
|
|
1121
|
+
(`BackendWebsocketTransport.request_connection`); a backend states it
|
|
1122
|
+
`true` once it wires the `peer/ping` HTTP + WS mount), `test_if(cond, name, fn)`
|
|
1123
|
+
for capability-gated cases, and `in_process_capabilities` preset. Only
|
|
1124
|
+
flags a suite reads are capabilities — a wiring fact that gates nothing is
|
|
1125
|
+
not recorded. **Mount flags have no family default**: `MountFlag` names the
|
|
1126
|
+
flags that describe a backend's own mounts (`ws`, `sse`, `cell_crud`,
|
|
1127
|
+
`cell_relations`, `cell_gated_create`, `fact_serving`,
|
|
1128
|
+
`fact_serving_admin_hash`, `ready`,
|
|
1129
|
+
`peer_request`, `ws_account_actions`, `ws_role_gated`), and the family
|
|
1130
|
+
defaults (`ts_default_capabilities` / `rust_default_capabilities`, typed
|
|
1131
|
+
`FamilyDefaultCapabilities`) omit them, with `capabilities` required on
|
|
1132
|
+
`make_default_{ts,rust}_backend_config` — so a config that leaves one out
|
|
1133
|
+
fails typecheck naming it, rather than silently skipping its suites. The
|
|
1134
|
+
family defaults carry the declared surface's mounts (`account_lifecycle`,
|
|
1135
|
+
`account_status`) and the runtime-behavior flags (`ws_handshake_pipelining`,
|
|
1136
|
+
`ws_refusal_body`) of the family's reference runtime — Node for TS, hyper
|
|
1137
|
+
for Rust; a backend on another runtime (Deno, Bun) overrides them. `cell_crud`
|
|
949
1138
|
gates the CRUD parity suite, `cell_relations` the relation / ACL / audit
|
|
950
1139
|
parity suite — both `true` on every backend that live-mounts the full cell
|
|
951
1140
|
surface (TS spine binary, in-process app, Rust stub). A backend mounting only
|
|
@@ -965,15 +1154,19 @@ source of truth for wire-shape conformance.
|
|
|
965
1154
|
auto-enumerated for shape/auth) — the flag gates the behavioral parity the
|
|
966
1155
|
generic round-trip can't drive (it can't delete the subject). `true` on every
|
|
967
1156
|
spine. `fact_serving`
|
|
968
|
-
gates `describe_fact_serving_cross_tests` (the cell-scoped per-reference
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1157
|
+
gates `describe_fact_serving_cross_tests` (the cell-scoped per-reference
|
|
1158
|
+
fact-serving parity suite, incl. the served fact's hardening headers and
|
|
1159
|
+
bodyless masked 404s); like cells it stays off the declared surface and is
|
|
1160
|
+
`true` on every spine that mounts the cell-gated serve route +
|
|
972
1161
|
the `_testing_put_fact` seeder (whose optional `holder` records a
|
|
973
1162
|
`fact_holder` row, so a non-admin can reference what it seeds, and whose
|
|
974
1163
|
optional `external_url` writes a bare external row naming that URL, so the
|
|
975
1164
|
suite can pin that a URL naming another hash's file is refused) — it also
|
|
976
|
-
gates `describe_cell_ref_admission_cross_tests`. `
|
|
1165
|
+
gates `describe_cell_ref_admission_cross_tests`. `fact_serving_admin_hash`
|
|
1166
|
+
is the admin-only bare-hash route (`GET /api/facts/:hash`), its own flag
|
|
1167
|
+
because a backend can serve the cell-gated route alone: it gates the
|
|
1168
|
+
fact-serving suite's bare-hash cases (with `fact_serving`) and the bare-hash
|
|
1169
|
+
cases of the query-shape and token-scope-surface suites. `ready` gates `describe_ready_cross_tests`
|
|
977
1170
|
(anonymous `GET /ready` → `200 {ready: true}` on a clean spine bootstrap);
|
|
978
1171
|
like cells/sse the `/ready` deploy gate stays off the declared surface, `true`
|
|
979
1172
|
on every spine that live-mounts it over the shared `expected_schema.json`.
|
|
@@ -993,7 +1186,7 @@ source of truth for wire-shape conformance.
|
|
|
993
1186
|
as it does behind auth, and `false` in-process, where there is no socket to
|
|
994
1187
|
write raw bytes to).
|
|
995
1188
|
`ws_account_actions` (the WS endpoint mounts the account actions — `true` on
|
|
996
|
-
fuz_app's own spine presets
|
|
1189
|
+
fuz_app's own spine presets; a consumer states it like every mount flag) gates the WS
|
|
997
1190
|
round-trip suite's self-revocation case (which requires the reply ahead of
|
|
998
1191
|
the close on every backend) and, with `rpc_path`, its WS-mutation audit case
|
|
999
1192
|
(which calls `_testing_drain_effects`).
|
|
@@ -1026,6 +1219,129 @@ consumer needs partial opt-out, add the knob then.
|
|
|
1026
1219
|
`bootstrap`, `rate_limiting_app_options`, `bootstrap_token`) — those drive
|
|
1027
1220
|
the omitted suites.
|
|
1028
1221
|
|
|
1222
|
+
### `cross_backend/spine_suites.ts` — `describe_spine_cross_suites`
|
|
1223
|
+
|
|
1224
|
+
One call that runs every shipped single-backend cross suite against one
|
|
1225
|
+
spawned backend: `describe_spine_cross_suites(handle, {surface_source,
|
|
1226
|
+
session_options, rpc_endpoints, live_rpc_endpoints?, roles?, setup?, exclude?,
|
|
1227
|
+
overrides?, rpc_success_fixtures?, rpc_skip_methods?, rpc_input_overrides?,
|
|
1228
|
+
rpc_max_body_bytes?, ws_max_message_bytes?})`.
|
|
1229
|
+
`live_rpc_endpoints` is the backend's whole live mount; given, `rpc_adversarial`
|
|
1230
|
+
also walks the methods it keeps off the declared surface. Without it a
|
|
1231
|
+
consumer writes a file per suite, and a suite added here never reaches it.
|
|
1232
|
+
|
|
1233
|
+
- **The registry.** `SUITE_NAMES` / `SuiteName` — the closed set of suites the
|
|
1234
|
+
call runs, each named by its module (`ws_round_trip` is
|
|
1235
|
+
`describe_cross_process_ws_tests`, `standard` the standard bundle).
|
|
1236
|
+
`STANDALONE_CROSS_SUITES` (keyed by the closed `StandaloneSuiteName`) names
|
|
1237
|
+
the modules left out, each with why: the
|
|
1238
|
+
`differential` (two handles), `login_security` and `ws_action_rate_limit`
|
|
1239
|
+
(a backend spawned with live limiters, which the standard suites would
|
|
1240
|
+
exhaust), and the `conformance_table` runner (the consumer's own cases).
|
|
1241
|
+
`src/test/testing/cross_backend/spine_suites.test.ts` imports every module
|
|
1242
|
+
under `cross_backend/` and holds the two lists to the ones exporting a
|
|
1243
|
+
`describe_*` function, whatever the export form (the Bun and Deno adapters,
|
|
1244
|
+
which load only on their own runtime, are held to naming no `describe_` at
|
|
1245
|
+
all), so a new suite fails until it is registered or stated standalone; the
|
|
1246
|
+
dispatch table
|
|
1247
|
+
is a `Record<SuiteName, …>`, so a registered name without an entry fails
|
|
1248
|
+
typecheck.
|
|
1249
|
+
- **Options from the handle.** Paths, cookie name and capabilities come from
|
|
1250
|
+
`handle.config` — `to_cross_suite_options(handle)`, exported for a suite
|
|
1251
|
+
called on its own — the daemon token from the handle, and each suite's
|
|
1252
|
+
`setup_test` from `default_cross_process_setup` over the `setup` options
|
|
1253
|
+
plus the seeding the suite needs (concatenated): a keeper-only
|
|
1254
|
+
`non_admin_keeper` for the standard bundle's keeper ≠ admin probe, the
|
|
1255
|
+
multi-actor keepers of `fact_serving` and `transport_composition`, the
|
|
1256
|
+
offer grantor of `role_grant_offer_enumeration`, the `cell_editor` holder of
|
|
1257
|
+
`cell_grant_role`. Suites are registered without a wrapping `describe`, so
|
|
1258
|
+
test names match a standalone call.
|
|
1259
|
+
- **`ws_required_roles`** — the roles the backend's WS upgrade on `ws_path`
|
|
1260
|
+
requires (an admin-gated `/api/ws`: `[ROLE_ADMIN]`). The suites that open
|
|
1261
|
+
sockets for accounts they create (`credential_deadline`, `session_cap`,
|
|
1262
|
+
`ws_round_trip`) create them holding these (`ws_account_roles`), and
|
|
1263
|
+
`role_grant_offer_notification_ws` expects revoking one to close the socket
|
|
1264
|
+
(its `ws_required_roles`) — one option for every socket a role gates.
|
|
1265
|
+
- **`rpc_max_body_bytes`** / **`ws_max_message_bytes`** — the backend's
|
|
1266
|
+
request-body cap on `rpc_path` and its inbound WS message cap, when they
|
|
1267
|
+
aren't the spine defaults (`SPINE_BODY_LIMIT_DEFAULT_BYTES`,
|
|
1268
|
+
`DEFAULT_WS_MAX_MESSAGE_BYTES`). The first reaches `body_size`,
|
|
1269
|
+
`body_size_smuggling` and `transport_composition`'s RPC over-cap row (each
|
|
1270
|
+
takes it as `rpc_max_body_bytes`), the second `ws_round_trip`'s
|
|
1271
|
+
`max_message_bytes` — one value per limit for every suite probing it.
|
|
1272
|
+
- **`exclude`** — `{[suite]: reason}`; the reason is required and shows in the
|
|
1273
|
+
run as a skipped test. An empty reason, an unknown suite name, or a suite
|
|
1274
|
+
both excluded and overridden throws before anything registers.
|
|
1275
|
+
- **`overrides`** — `SuiteOverrides`, per suite a `Partial` of its own options
|
|
1276
|
+
(`SuiteOptionsMap`), shallow-merged over the derived ones: a role list, an
|
|
1277
|
+
`xfail`, a narrower capability set, `ws: undefined` to drop optional socket
|
|
1278
|
+
cases (`session_cap` and `testing_backdoor` take no `capabilities`, so `ws` is
|
|
1279
|
+
their only socket switch). An entry can instead be a function of the derived
|
|
1280
|
+
options returning the patch (`SuiteOverride`), for a change built on a
|
|
1281
|
+
derived value — an `xfail` added to the derived multi-actor setup — so the
|
|
1282
|
+
seeding isn't restated. After the merge, every option the suite's type
|
|
1283
|
+
requires (each entry's `required` set, exhaustive by type) must be neither
|
|
1284
|
+
`undefined` nor `''`, or the call throws at collection naming suite and key.
|
|
1285
|
+
- **Inputs the handle doesn't carry.** The declared surface is a required
|
|
1286
|
+
option. Paths and roles that name the reference spines' mounts (the
|
|
1287
|
+
role-gated WS endpoint and its roles) default to the `SPINE_*` constants —
|
|
1288
|
+
a suite whose flag is off never reads them, and a backend mounting its own
|
|
1289
|
+
overrides them. Anything else comes from the config when it can
|
|
1290
|
+
(`session_cookie_codec`'s `cookie_keys` from `SECRET_FUZ_COOKIE_KEYS`), else
|
|
1291
|
+
from `overrides`; with neither, the call throws naming the suite and the
|
|
1292
|
+
field — provide it or exclude the suite.
|
|
1293
|
+
|
|
1294
|
+
Each suite reads its own capability flags, so a backend's truthful
|
|
1295
|
+
`BackendConfig.capabilities` decides what runs. The role-shaped suites default
|
|
1296
|
+
to the spine's own roles — `cell_grant_role`'s `cell_editor`, the
|
|
1297
|
+
`participant` `role_grant_offer_lifecycle` offers and
|
|
1298
|
+
`role_grant_participation` confers — and take another through `suite_roles`
|
|
1299
|
+
(`SpineSuiteRoles`: `cell_grant`, `conferral`), which the call also seeds on
|
|
1300
|
+
the holder each needs (set it there, not in `overrides`, which would leave the
|
|
1301
|
+
seed on the default). A backend with only the builtin roles passes
|
|
1302
|
+
`{cell_grant: ROLE_KEEPER, conferral: ROLE_ADMIN}`: keeper reach is not admin
|
|
1303
|
+
reach, so every `cell_grant_role` arm still turns on the grant, while an admin
|
|
1304
|
+
views and edits every cell — `describe_cell_grant_role_cross_tests` refuses
|
|
1305
|
+
`ROLE_ADMIN` — and the scoped-assign case of `role_grant_participation` skips
|
|
1306
|
+
for a builtin. fuz_app's own wiring is
|
|
1307
|
+
`src/test/cross_backend/spine_suites.cross.test.ts`.
|
|
1308
|
+
|
|
1309
|
+
### `cross_backend/rust_spine_consumer_surface.ts` — `create_rust_spine_consumer_surface_spec`
|
|
1310
|
+
|
|
1311
|
+
The declared surface of a consumer served by the Rust spine, for its cross
|
|
1312
|
+
suites: `create_rust_spine_consumer_surface_spec({session_options,
|
|
1313
|
+
domain_action_specs, extra_rpc_actions?, roles?, exclude_methods?,
|
|
1314
|
+
rpc_path?, host_gate?})`, built on `create_test_app_surface_spec` from the hono-free route
|
|
1315
|
+
shapes. It declares `/health`, the `ACCOUNT_ROUTER_PATHS` under
|
|
1316
|
+
`/api/account` (`/status`, `/login`, `/logout`, `/password` — what
|
|
1317
|
+
`fuz_auth::account_router` mounts, so the TS-only `/verify` stays off), signup,
|
|
1318
|
+
bootstrap as a `surface_only` shape, the daemon-token layer the consumer test
|
|
1319
|
+
binaries wire, a `host_validation` stub layer at the head of the middleware
|
|
1320
|
+
list when `host_gate` says the server turns on the spine's opt-in host gate
|
|
1321
|
+
(default off), and one RPC endpoint: the standard actions (over `roles`), the
|
|
1322
|
+
`extra_rpc_actions` (opt-in families built against the stub context), then the
|
|
1323
|
+
request-response, non-protocol `domain_action_specs` minus `exclude_methods`
|
|
1324
|
+
(methods the spec-derived suites can't drive; an entry naming no domain spec
|
|
1325
|
+
throws). `create_rust_spine_consumer_rpc_endpoints(options)` is that endpoint
|
|
1326
|
+
alone, for `describe_spine_cross_suites`'s `rpc_endpoints`. Handlers are
|
|
1327
|
+
no-ops — live dispatch happens in the spawned binary. A server mounting no
|
|
1328
|
+
signup router (a single-keeper daemon) is not this shape.
|
|
1329
|
+
|
|
1330
|
+
### `cross_backend/provided_context.ts` — `inject_backend_handle`
|
|
1331
|
+
|
|
1332
|
+
Augments vitest's `ProvidedContext` with the keys the shipped global setups
|
|
1333
|
+
provide — `backend_handle` (`create_cross_backend_global_setup`) and
|
|
1334
|
+
`parity_handle_a` / `parity_handle_b` (`create_dual_spawn_global_setup`'s
|
|
1335
|
+
default keys) — and reads them: `inject_backend_handle()` /
|
|
1336
|
+
`inject_parity_handles()` return the handles rebuilt by
|
|
1337
|
+
`reconstruct_bootstrapped_handle`. The augmentation ships in the module's
|
|
1338
|
+
`.d.ts`, so a test file importing either helper gets the typed `inject` with
|
|
1339
|
+
no augmentation of its own — a type-only import of the module carries it too,
|
|
1340
|
+
and a global setup built on the shipped makers needs none. A project providing other
|
|
1341
|
+
keys — a dual spawn under its own `provide_keys`, a probe result — augments
|
|
1342
|
+
`ProvidedContext` for those itself; fuz_app's are in
|
|
1343
|
+
`src/test/cross_backend/cross_test_types.ts`.
|
|
1344
|
+
|
|
1029
1345
|
### `cross_backend/conformance_table.ts` + `conformance_case.ts` + `xfail.ts` — declarative behavioral/security cases
|
|
1030
1346
|
|
|
1031
1347
|
The opinionated behavioral/security layer on top of the spec-derived
|
|
@@ -1036,7 +1352,7 @@ conformance cases assert _expected behavior_ — the security negatives
|
|
|
1036
1352
|
wire-shape check passes green on even when behavior is wrong.
|
|
1037
1353
|
|
|
1038
1354
|
- `conformance_case.ts` — `ConformanceCase` Zod schema:
|
|
1039
|
-
`{name, request: {method, params?, as, verb?}, expect: {status,
|
|
1355
|
+
`{name, request: {method, params?, as, acting?, verb?}, expect: {status,
|
|
1040
1356
|
error_reason?, fields?, headers?, equivalence_group?}, note?, xfail?}`. A case is **data** — `method`
|
|
1041
1357
|
resolves its `input`/`output` from the live registry (RPC) or `RouteSpec`
|
|
1042
1358
|
(the 6 REST auth routes), so the case never carries a schema. `as` is the
|
|
@@ -1069,10 +1385,17 @@ error_reason?, fields?, headers?, equivalence_group?}, note?, xfail?}`. A case i
|
|
|
1069
1385
|
`expired_session` is the keeper behind an expired server-side session
|
|
1070
1386
|
(`fixture.mint_expired_session()`: a backdated `auth_session` row behind a
|
|
1071
1387
|
still-valid signed cookie, so the DB-row expiry gate is what refuses it).
|
|
1388
|
+
`acting` (`ConformanceActingBinding`) has the runner fill `params.acting`
|
|
1389
|
+
with a real actor id: `principal` (the actor of the account the
|
|
1390
|
+
principal's credential names), `extra_actor` (`fixture.extra_actors[0]`),
|
|
1391
|
+
`keeper_actor` (`fixture.actor`, foreign to every non-keeper principal);
|
|
1392
|
+
params must then be an object or omitted, without an `acting` of their own,
|
|
1393
|
+
and a principal presenting no account throws.
|
|
1072
1394
|
`error_reason` is the imported
|
|
1073
1395
|
`ERROR_*` constant (asserted against the RPC `error.data.reason` or the
|
|
1074
|
-
REST flat-body `error`; the
|
|
1075
|
-
|
|
1396
|
+
REST flat-body `error`; the dispatcher's 401 carries
|
|
1397
|
+
`authentication_required`, and a denial with no reason — a handler's bare
|
|
1398
|
+
`forbidden` — is pinned by `status`). `equivalence_group` is the
|
|
1076
1399
|
**negative-space** axis: every case tagged with the same group string must
|
|
1077
1400
|
produce a **byte-identical** normalized response (`{status, body}`),
|
|
1078
1401
|
asserted per impl after the case loop. It promotes a masked pair
|
|
@@ -1091,14 +1414,76 @@ error_reason?, fields?, headers?, equivalence_group?}, note?, xfail?}`. A case i
|
|
|
1091
1414
|
backend-fingerprinting oracle). Headers are deliberately kept out of the
|
|
1092
1415
|
`equivalence_group` `{status, body}` comparison (`Set-Cookie` / `Date` vary).
|
|
1093
1416
|
- `conformance_table.ts` — `describe_conformance_table_tests({cases,
|
|
1094
|
-
setup_test, surface_source, capabilities, rpc_endpoints,
|
|
1095
|
-
principals?, suite_name?})`.
|
|
1417
|
+
setup_test, surface_source, capabilities, rpc_endpoints, live_rpc_endpoints?,
|
|
1418
|
+
session_options, principals?, ws?, suite_name?})`. A case's RPC method
|
|
1419
|
+
resolves from `rpc_endpoints` first, then from `live_rpc_endpoints` — the
|
|
1420
|
+
endpoints the backend live-mounts, for methods kept off the declared surface
|
|
1421
|
+
(cell verbs, opt-in resolvers, `_testing_*` backdoors); a backend's whole
|
|
1422
|
+
live mount is a fine value, since a declared method still resolves from the
|
|
1423
|
+
declared surface. `resolve_conformance_rpc_endpoints({rpc_endpoints,
|
|
1424
|
+
live_rpc_endpoints?, session_options})` (exported) builds that lookup list,
|
|
1425
|
+
for the table and the differential alike. fuz_app's spine value is
|
|
1426
|
+
`create_spine_live_rpc_endpoints()` (`full_spine_mount.ts`), the binary's
|
|
1427
|
+
whole mount built on stub deps for resolution only. Same `{setup_test, surface_source,
|
|
1096
1428
|
capabilities}` protocol every Tier 1 suite uses, so **one case array runs
|
|
1097
1429
|
both transports** — in-process (`gro test`) and cross-process (the gate,
|
|
1098
|
-
each backend's real auth resolution). `
|
|
1430
|
+
each backend's real auth resolution). `resolve_conformance_principal` (exported) maps the five
|
|
1099
1431
|
always-available principals to fixture accessors; `role_holder` /
|
|
1100
1432
|
`wrong_role` read a seeded `extra_accounts` username named via
|
|
1101
|
-
`options.principals`.
|
|
1433
|
+
`options.principals`. `send_conformance_case(c, {fixture, surface_source, principals?,
|
|
1434
|
+
resolved_rpc_endpoints})` (exported) is the send half — principal resolved,
|
|
1435
|
+
request sent, the raw `{target, status, headers, body}` back unasserted; the
|
|
1436
|
+
table asserts on it, and the cross-backend differential compares two.
|
|
1437
|
+
**WS leg:** `ws: WsEndpointTarget` (`{base_url, ws_path, origin?}`) on
|
|
1438
|
+
either sends each case as one frame on an admitted socket the principal
|
|
1439
|
+
opens — its headers on the upgrade, no `Origin` where the HTTP leg
|
|
1440
|
+
suppresses it, its own actor as `?acting=`. Only the TS spine resolves an
|
|
1441
|
+
actor at the upgrade, refusing a multi-actor account's upgrade without
|
|
1442
|
+
`?acting=` (400 `actor_required`); the Rust spine's upgrade resolves the
|
|
1443
|
+
account and ignores it. Per-frame dispatch resolves each frame's `acting` on
|
|
1444
|
+
both. The leg always sends `?acting=`, so it does not cover that
|
|
1445
|
+
upgrade-level difference; the front-door table's multi-actor row does. The frame is the envelope the HTTP
|
|
1446
|
+
body is (`perform_action_result_to_envelope` on TS), so the response reads
|
|
1447
|
+
`{status, headers: {}, body: frame}` with `status` the code's
|
|
1448
|
+
`jsonrpc_error_code_to_http_status` (200 for a result) and every row holds
|
|
1449
|
+
unchanged. What WS loses is headers — the TS DEV-only `X-Fuz-Auth-Debug`
|
|
1450
|
+
among them. `is_ws_conformance_case(c)` (exported) is the filter: RPC, sent
|
|
1451
|
+
`POST`, no header value pinned, a principal that can hold a socket — not
|
|
1452
|
+
`anonymous` / `bearer_browser` / `malformed_bearer` / `expired_session`,
|
|
1453
|
+
refused at the upgrade, nor `scoped_token`, refused `surface:ws_upgrade`
|
|
1454
|
+
there (a narrowed bearer can never send a frame, so no per-frame scope row
|
|
1455
|
+
exists; the front-door table pins the upgrade refusal). An equivalence
|
|
1456
|
+
group left with one member on the WS leg is dropped. Cross-process only.
|
|
1457
|
+
- `conformance_rpc_auth.ts` — `generate_rpc_auth_conformance_cases({surface_source,
|
|
1458
|
+
live_rpc_endpoints?, skip_methods?})` → `ConformanceCase[]` from each declared RPC method's
|
|
1459
|
+
`RouteAuth`, then each live-mounted one the declared surface lacks
|
|
1460
|
+
(`live_rpc_endpoints`, resolved — the rows then need the same endpoints on
|
|
1461
|
+
the runner's `live_rpc_endpoints`): `anonymous` → 401 `authentication_required` for every method
|
|
1462
|
+
needing an account or actor (all in the
|
|
1463
|
+
`RPC_AUTH_ANONYMOUS_EQUIVALENCE_GROUP`, since the 401 must not vary by
|
|
1464
|
+
method); for a `credential_types` gate, each builtin channel it refuses
|
|
1465
|
+
(`keeper` = session, `token` = api_token, `daemon` = daemon_token, each the
|
|
1466
|
+
keeper) → 403 `credential_type_required` with `required_credential_types`;
|
|
1467
|
+
for `roles` on a method admitting a session, `fresh_non_admin` → 403
|
|
1468
|
+
`insufficient_permissions` with `required_roles`; public methods get
|
|
1469
|
+
nothing. Params are `generate_valid_body` minus its generated `acting`
|
|
1470
|
+
(read before validation, so the generator's id would answer
|
|
1471
|
+
`actor_not_on_account`), so a missing gate reaches the handler rather than
|
|
1472
|
+
a 400. On an actor-bound method the credential and role rows bind
|
|
1473
|
+
`acting: 'principal'` instead — both spines resolve the acting actor before
|
|
1474
|
+
the credential gate, so a keeper with a second actor (`extra_actors`) would
|
|
1475
|
+
otherwise answer `actor_required` — and the rows hold for a single-actor and
|
|
1476
|
+
a multi-actor keeper alike. The rows pin each
|
|
1477
|
+
gate, not its order against validation (the security slate's
|
|
1478
|
+
401-before-400 rows do). Skips `PROTOCOL_ACTION_METHODS` (fixed shared specs,
|
|
1479
|
+
WS-scoped — `cancel` has no reply, `peer/ping` is public — and covered by
|
|
1480
|
+
the WS suites). Given the live mount, the `_testing_*` backdoors get rows too:
|
|
1481
|
+
each is daemon-token-gated, so anonymous → 401 and the keeper's session and
|
|
1482
|
+
API token → 403 `credential_type_required`. Exists
|
|
1483
|
+
because the action-manifest parity gate compares declarations, not the
|
|
1484
|
+
live gates, and the Rust spine dispatches its own. Input-dependent handler
|
|
1485
|
+
checks stay hand-written rows. fuz_app spreads it into both conformance
|
|
1486
|
+
legs.
|
|
1102
1487
|
- `xfail.ts` — `xfail_until(tracking_id, reason, name, fn)`, a thin
|
|
1103
1488
|
`test.fails` wrapper for deferred-by-design rows (visible + self-cleaning:
|
|
1104
1489
|
turns red when the gap closes, forcing marker removal). In-scope gaps fail
|
|
@@ -1108,7 +1493,36 @@ capabilities}` protocol every Tier 1 suite uses, so **one case array runs
|
|
|
1108
1493
|
Wire from a `.db.test.ts` (in-process) and a `.cross.test.ts`
|
|
1109
1494
|
(cross-process) with the same case array — fuz_app's own runner-proof is
|
|
1110
1495
|
`../../test/cross_backend/conformance.{db,cross}.test.ts` sharing
|
|
1111
|
-
`conformance_proof_cases.ts
|
|
1496
|
+
`conformance_proof_cases.ts`, the generated auth rows, and
|
|
1497
|
+
`conformance_admin_cases.ts` (offer list/history across accounts, invite
|
|
1498
|
+
errors, pagination bounds on the admin lists); two checks the table can't
|
|
1499
|
+
express ride beside it on both legs: the duplicate-invite refusal (two
|
|
1500
|
+
requests, `describe_invite_duplicate_tests` in `invite_duplicate.ts`) and the
|
|
1501
|
+
offer reads' cross-account refusal being identical for the keeper's real id
|
|
1502
|
+
and the nil id (`describe_offer_cross_account_tests` in
|
|
1503
|
+
`offer_cross_account.ts` — the existing id is known only to the fixture).
|
|
1504
|
+
The cross legs pass the spine's live mount, so the table also runs
|
|
1505
|
+
`conformance_actor_cases.ts` (`actor_lookup`'s empty-`ids` bound, the
|
|
1506
|
+
`actor_search` scope gate) and `conformance_cell_cases.ts` (the cell refusals
|
|
1507
|
+
one request reaches with no cell in place: `kind` inside `data`, `cell_get`
|
|
1508
|
+
naming neither `id` nor `path`, the relation lists naming neither or both
|
|
1509
|
+
endpoints — each `invalid_params` with its declared reason — a grant miss
|
|
1510
|
+
`cell_grant_not_found`, the `cell_list` pagination bounds and enum-valued
|
|
1511
|
+
filters, and the schema-first rows where a bad page or filter value must answer
|
|
1512
|
+
before a handler's reason; `conformance_cell_cases.test.ts` requires a `{}` row
|
|
1513
|
+
with a reason for every cell action whose input is marked
|
|
1514
|
+
`empty_input_handler_guarded`), with the
|
|
1515
|
+
generated rows over the off-surface methods; the in-process leg for those is
|
|
1516
|
+
`conformance_off_surface.db.test.ts`, over an app mounting the cell surface
|
|
1517
|
+
and the two resolvers. The cross leg runs the same cases
|
|
1518
|
+
over WS too, and both legs run `conformance_acting_cases.ts` with the
|
|
1519
|
+
generated rows on a keeper seeded with `extra_actors`: no `acting` or a
|
|
1520
|
+
malformed one → 400 `actor_required`; the nil id, an unused id and another
|
|
1521
|
+
account's real actor → one identical 400 `actor_not_on_account`; acting as
|
|
1522
|
+
the extra actor (no grants) → 403 `insufficient_permissions` on an admin
|
|
1523
|
+
method over a session and a bearer, and on the keeper-gated `account_purge`
|
|
1524
|
+
over the daemon token; acting as the bootstrap actor passes the same gates.
|
|
1525
|
+
The role gate reads the resolved actor's grants on both spines.
|
|
1112
1526
|
|
|
1113
1527
|
### `cross_backend/ws_round_trip.ts` — `describe_cross_process_ws_tests`
|
|
1114
1528
|
|
|
@@ -1119,7 +1533,7 @@ needs raw `base_url` / `ws_path` the standard bundle doesn't carry, mirroring
|
|
|
1119
1533
|
how `describe_ws_round_trip_tests` sits beside `describe_standard_tests`
|
|
1120
1534
|
in-process. `describe_cross_process_ws_tests({setup_test, capabilities,
|
|
1121
1535
|
base_url, ws_path, origin?, rpc_path?, max_message_bytes?, logout_path?,
|
|
1122
|
-
password_path?, max_tokens?})` opens a live
|
|
1536
|
+
password_path?, max_tokens?, ws_account_roles?})` opens a live
|
|
1123
1537
|
`WebSocket` via `create_ws_transport` (the `ws` npm package) and asserts
|
|
1124
1538
|
cases against the upgrade stack `register_ws_endpoint` wires (origin →
|
|
1125
1539
|
`require_auth` → dispatch): authed upgrade round-trips `heartbeat`,
|
|
@@ -1149,7 +1563,9 @@ bearer socket (a socket on its other token stays open);
|
|
|
1149
1563
|
bearer socket of the token the cap evicts. The account-wide rows come from
|
|
1150
1564
|
`cross_backend/revocation_drivers.ts` (`create_account_revocations`, shared
|
|
1151
1565
|
with the SSE suite), and a case that opens several sockets closes whichever
|
|
1152
|
-
opened when a later open fails.
|
|
1566
|
+
opened when a later open fails. The second accounts are created holding
|
|
1567
|
+
`ws_account_roles`, for a backend whose WS upgrade requires a role (an
|
|
1568
|
+
admin-gated `/api/ws` refuses a roleless account). Per-connection auth is enforced **at upgrade
|
|
1153
1569
|
time**, so the negative upgrade cases assert the upgrade itself rejects, not
|
|
1154
1570
|
a per-message error; the close cases prove a revocation closes an
|
|
1155
1571
|
already-open socket, since per-message dispatch never re-checks credential
|
|
@@ -1168,7 +1584,7 @@ so it validates the transport without touching domain WS methods. Gated on
|
|
|
1168
1584
|
`capabilities.ws`; cross-process only (needs a real bound socket — wire from
|
|
1169
1585
|
a `*.cross.test.ts`, never an in-process setup). Authed cookies come from the
|
|
1170
1586
|
fresh-per-test keeper via `fixture.transport.cookies()`, not the stale
|
|
1171
|
-
globalSetup handle.
|
|
1587
|
+
globalSetup handle.
|
|
1172
1588
|
|
|
1173
1589
|
### `cross_backend/role_grant_offer_notification_ws.ts` — `describe_role_grant_offer_notification_ws_tests`
|
|
1174
1590
|
|
|
@@ -1185,7 +1601,23 @@ lifecycle RPC over `fixture.transport`, then
|
|
|
1185
1601
|
strict-parses the delivered frame against its canonical params schema from
|
|
1186
1602
|
`auth/role_grant_offer_notifications.ts` (the guard against serialization drift —
|
|
1187
1603
|
field / null / datetime / the flat revoke shape / the supersede `reason` +
|
|
1188
|
-
`cause_id`).
|
|
1604
|
+
`cause_id`). Every case also opens a **bystander** — a third account's
|
|
1605
|
+
admitted socket, party to nothing the case drives — and once the targeted
|
|
1606
|
+
frame has landed sends a `heartbeat` on it and asserts it received nothing
|
|
1607
|
+
but the answers to its own requests. The barrier is what makes the silence
|
|
1608
|
+
mean something: both spines queue a fan-out to every recipient in one pass
|
|
1609
|
+
with no await in it, and neither writes a response ahead of a notification
|
|
1610
|
+
already queued for the same connection — the TS transport sends both through
|
|
1611
|
+
one outbound queue, and the Rust loop writes a connection's queued frames
|
|
1612
|
+
before a response (`flush_queued_frames`). So a frame the pass addressed to
|
|
1613
|
+
the bystander reaches it ahead of the barrier's response. That is strict on TS,
|
|
1614
|
+
which sends a fan-out in one synchronous closure. On Rust, `deliver_all` pushes
|
|
1615
|
+
to recipients one after another on one worker while the target's connection
|
|
1616
|
+
loop runs on another, so the target's frame can be written while the pass is
|
|
1617
|
+
still pushing — a window the barrier's round trip outlasts in practice. The
|
|
1618
|
+
check covers every pass of the drain: the Rust `PostCommitEffects::drain_all`
|
|
1619
|
+
joins effects whose notify futures push synchronously, so every pass
|
|
1620
|
+
completes in the drain's first poll. Unlike `describe_cross_process_ws_tests` (consumer-agnostic,
|
|
1189
1621
|
`heartbeat`-only), this drives a real **domain** notification family — but one
|
|
1190
1622
|
built from spine primitives only (accounts / role-grants / offers / WS), zero
|
|
1191
1623
|
consumer domain, so it lives here and runs against any backend that wires the
|
|
@@ -1204,8 +1636,7 @@ grant and expects its socket to close with `WS_CLOSE_PERMISSION_REVOKED` (the
|
|
|
1204
1636
|
notification may or may not land first, and is held to the flat shape when it
|
|
1205
1637
|
does), where an ungated socket expects the frame. The offer/accept fixtures are `create_pending_role_grant_offer` /
|
|
1206
1638
|
`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
|
|
1639
|
+
Gated on `capabilities.ws`; cross-process only. fuz_app's
|
|
1209
1640
|
`role_grant_offer_notification_ws_gated.cross.test.ts` runs it again over the
|
|
1210
1641
|
spine binaries' role-gated endpoint with `ws_required_roles` declared.
|
|
1211
1642
|
|
|
@@ -1227,7 +1658,6 @@ must hold `ROLE_ADMIN` and `scoped_role` (a role an admin can grant scoped).
|
|
|
1227
1658
|
fuz_app's spine binaries mount `SPINE_ROLE_GATED_WS_PATH` gated on
|
|
1228
1659
|
`SPINE_ROLE_GATED_WS_ROLES` (`admin` + `participant`). Gated on
|
|
1229
1660
|
`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
1661
|
|
|
1232
1662
|
### `cross_backend/peer_ping_ws.ts` — `describe_peer_ping_ws_tests`
|
|
1233
1663
|
|
|
@@ -1251,8 +1681,7 @@ the HTTP no-transport path. Gated on `capabilities.peer_request` — `true` on
|
|
|
1251
1681
|
both the Rust spine and the TS spine (the `BackendWebsocketTransport.request_connection`
|
|
1252
1682
|
path + `register_action_ws` response correlation); `peer/ping` is a protocol
|
|
1253
1683
|
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`.
|
|
1684
|
+
gate. Cross-process only.
|
|
1256
1685
|
|
|
1257
1686
|
### `cross_backend/ws_inbound_dispatch.ts` — `describe_ws_inbound_dispatch_cross_tests`
|
|
1258
1687
|
|
|
@@ -1267,8 +1696,7 @@ client's responder withholds the server's ping echo until the test releases
|
|
|
1267
1696
|
it — so the suite gates on `capabilities.peer_request`.
|
|
1268
1697
|
`describe_ws_inbound_dispatch_cross_tests({setup_test, capabilities, base_url,
|
|
1269
1698
|
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`.
|
|
1699
|
+
constants, so a value changed on one spine fails here. Cross-process only.
|
|
1272
1700
|
|
|
1273
1701
|
### `cross_backend/ws_outbound_overflow.ts` — `describe_ws_outbound_overflow_cross_tests`
|
|
1274
1702
|
|
|
@@ -1297,8 +1725,7 @@ flood action is daemon-token-gated, so the flooding socket upgrades with the
|
|
|
1297
1725
|
daemon token and no `Origin` (`create_ws_transport`'s `origin: null`). The wire
|
|
1298
1726
|
literals are independent copies of the producers' constants. Gated on
|
|
1299
1727
|
`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`.
|
|
1728
|
+
flood action on its WS endpoint.
|
|
1302
1729
|
|
|
1303
1730
|
### `cross_backend/ws_connection_cap.ts` — `describe_ws_connection_cap_cross_tests`
|
|
1304
1731
|
|
|
@@ -1326,8 +1753,7 @@ overridden; pass the consumer's value when it sets one, `null` (cap disabled)
|
|
|
1326
1753
|
to skip. **Consumer-agnostic** like the WS round-trip suite (`heartbeat` only).
|
|
1327
1754
|
A separate call rather than more cases on that suite because each case opens
|
|
1328
1755
|
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`.
|
|
1756
|
+
cross-process only.
|
|
1331
1757
|
|
|
1332
1758
|
### `cross_backend/session_cap.ts` — `describe_session_cap_cross_tests`
|
|
1333
1759
|
|
|
@@ -1335,12 +1761,13 @@ Parity for the per-account session cap over real HTTP: logging in past
|
|
|
1335
1761
|
`max_sessions` (default `DEFAULT_MAX_SESSIONS`) never refuses a login and
|
|
1336
1762
|
evicts the oldest session.
|
|
1337
1763
|
`describe_session_cap_cross_tests({setup_test, max_sessions?, login_path?,
|
|
1338
|
-
status_path?, ws?})` — pass `ws` (`{base_url, ws_path, origin?}`) to also
|
|
1764
|
+
status_path?, ws?, ws_account_roles?})` — pass `ws` (`{base_url, ws_path, origin?}`) to also
|
|
1339
1765
|
assert that the evicted session's socket is closed with
|
|
1340
1766
|
`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
|
-
|
|
1767
|
+
is skipped, so a caller passes it only for a backend with `capabilities.ws`;
|
|
1768
|
+
`ws_account_roles` gives that case's account roles, for a backend whose WS
|
|
1769
|
+
upgrade requires one.
|
|
1770
|
+
Cross-process only.
|
|
1344
1771
|
|
|
1345
1772
|
### `cross_backend/sse_round_trip.ts` — `describe_cross_process_sse_tests`
|
|
1346
1773
|
|
|
@@ -1370,8 +1797,7 @@ admin each drop its stream — the rows `create_account_revocations`
|
|
|
1370
1797
|
session closes the oldest and leaves the rest open. The data-frame + close
|
|
1371
1798
|
cases gate on `rpc_path` (they drive the standard account/admin actions); all
|
|
1372
1799
|
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
|
|
1800
|
+
`*.cross.test.ts`. It runs on every fuz_app backend — the
|
|
1375
1801
|
TS spines wire `audit_log_sse` and the Rust `spine_stub` serves the same
|
|
1376
1802
|
stream.
|
|
1377
1803
|
|
|
@@ -1389,7 +1815,7 @@ covered on WS and SSE at once. The drivers stand alone too: `cross_post_as`
|
|
|
1389
1815
|
`cross_admin_token_revoke_all`, and `cross_account_purge` (over the daemon
|
|
1390
1816
|
token, no `Origin`).
|
|
1391
1817
|
|
|
1392
|
-
### `cross_backend/cell_crud.ts` + `cell_relations.ts` + `cell_gated_create.ts` — cell parity suites
|
|
1818
|
+
### `cross_backend/cell_crud.ts` + `cell_relations.ts` + `cell_gated_create.ts` + `cell_moderate.ts` — cell parity suites
|
|
1393
1819
|
|
|
1394
1820
|
The cell-layer parity coverage is split across two sibling suites. Cells
|
|
1395
1821
|
can't ride the generic `describe_rpc_round_trip_tests` (stateful verbs need a
|
|
@@ -1400,7 +1826,19 @@ cell validators (`describe_standard_cross_process_tests`' generic round-trip
|
|
|
1400
1826
|
never sees them). Both parse every success response against the verb's Zod
|
|
1401
1827
|
**output** schema, so a TS↔Rust envelope drift fails the suite — not just a
|
|
1402
1828
|
payload-field drift. Call-site primitives (`cross_rpc_call` / `error_reason` /
|
|
1403
|
-
`expect_output`) live in `cross_backend/
|
|
1829
|
+
`expect_output`) live in `cross_backend/cross_rpc.ts`, shared by every
|
|
1830
|
+
stateful cross suite (a reply that is no JSON object throws naming the method,
|
|
1831
|
+
the HTTP status and the body's head), and every
|
|
1832
|
+
suite that reads audit rows goes through `cross_backend/audit_cross_helpers.ts`
|
|
1833
|
+
— `list_audit_events(fixture, params, rpc_path?)` drains
|
|
1834
|
+
(`cross_drain_effects`, the `_testing_drain_effects` barrier) before it lists and
|
|
1835
|
+
parses, so no reader skips the barrier, and parses every row's metadata
|
|
1836
|
+
(`assert_audit_event_metadata`, closed to undeclared keys when the fixture's
|
|
1837
|
+
setup asked), so every reader holds the rows it reads to their event's
|
|
1838
|
+
schema. A suite pinning a row's values uses `assert_audit_metadata_fields(fixture,
|
|
1839
|
+
metadata, expected, absent?)` — each field compared on its own so a consumer's
|
|
1840
|
+
added key passes, the `absent` keys refused, and the whole object compared only
|
|
1841
|
+
when the fixture closed the world; the shared
|
|
1404
1842
|
options shape is `RpcPathCapabilityGatedCrossSuiteOptions` from
|
|
1405
1843
|
`cross_backend/setup.ts`. `cross_backend/wire_shapes.ts` carries
|
|
1406
1844
|
`assert_iso8601_seconds` / `assert_iso8601_seconds_nullable` — the byte-level
|
|
@@ -1413,7 +1851,7 @@ an audit row, an admin account listing, and an offer's `created_at` /
|
|
|
1413
1851
|
imperative cross suite aliases: `CrossSuiteOptions` (`setup_test` alone),
|
|
1414
1852
|
`CapabilityGatedCrossSuiteOptions` (+ `capabilities` — `ready`),
|
|
1415
1853
|
`RpcPathCrossSuiteOptions` (+ `rpc_path?` — origin, body-size (which adds `base_url`), actor
|
|
1416
|
-
lookup
|
|
1854
|
+
lookup, app settings, testing backdoor), and the intersection
|
|
1417
1855
|
`RpcPathCapabilityGatedCrossSuiteOptions` (every `cell_*` suite, query-shape,
|
|
1418
1856
|
token-scope surface, fact-serving, account lifecycle). A suite takes the
|
|
1419
1857
|
capability-carrying variant only when it actually gates cases on a flag, so
|
|
@@ -1426,20 +1864,28 @@ the option type states whether the bundle is read.
|
|
|
1426
1864
|
`path` write by non-admin → 403 on create + update; `cell_get` with no
|
|
1427
1865
|
id/path → `invalid_params`; null-auth `cell_list` `created_by` →
|
|
1428
1866
|
`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
|
|
1867
|
+
UTF-16 code unit past it → `invalid_params` on create and as a list filter;
|
|
1868
|
+
`cell_list` pages honoring `limit` + `offset`, a page holding visible rows
|
|
1869
|
+
only and filling to `limit` past a newer run of rows the caller can't see —
|
|
1870
|
+
two owners' private cells, read by a non-owner and by anon — and an admin's
|
|
1871
|
+
pages tiling every row).
|
|
1430
1872
|
- **`describe_cell_relations_cross_tests`** (gates on
|
|
1431
1873
|
`capabilities.cell_relations`) — the verbs beyond CRUD: grant lifecycle
|
|
1432
1874
|
(actor-shaped editor grant enables edit, manage-tier `cell_grant_list`,
|
|
1433
1875
|
revoke), the now-reachable `cell_visibility_manage_only` 403 (editor-grant
|
|
1434
1876
|
holder can't flip visibility), field set / forward+reverse list / idempotent
|
|
1435
1877
|
delete, item insert / ordered forward+reverse list / move / idempotent
|
|
1436
|
-
delete,
|
|
1878
|
+
delete, an insert or move onto a taken position → `cell_item_position_taken`
|
|
1879
|
+
with the rows unchanged, clone shallow (shares edges) vs deep (clones children), and
|
|
1437
1880
|
manage-tier `cell_audit_list` (owner reads the timeline; a viewer-grant
|
|
1438
1881
|
holder who can `cell_get` still gets the IDOR 404). Only **actor-shaped**
|
|
1439
1882
|
grants are exercised — role-shaped principals need a closed role registry the
|
|
1440
1883
|
Rust spine deliberately lacks.
|
|
1441
|
-
- **`describe_cell_gated_create_cross_tests`** +
|
|
1442
|
-
(
|
|
1884
|
+
- **`describe_cell_gated_create_cross_tests`** (`cell_gated_create.ts`) +
|
|
1885
|
+
**`describe_cell_moderate_cross_tests`** (`cell_moderate.ts`)
|
|
1886
|
+
(gate on `capabilities.cell_gated_create`, **cross-only**; both build their
|
|
1887
|
+
spaces on `TEST_SPACE_POLICY`, exported beside the authorizer in
|
|
1888
|
+
`test_cell_gated_create_authorize.ts`) — the parent-aware
|
|
1443
1889
|
cell-creation authorizer (`CellCreateAuthorize`, **directory model**) +
|
|
1444
1890
|
the `cell_moderate` verb parity proofs. Both spines mount the same
|
|
1445
1891
|
`test_cell_gated_create_authorize` policy (its admin bypass and `min_role`
|
|
@@ -1453,7 +1899,9 @@ moderation_required}`, with `moderation` set per the verdict (`pending`+private
|
|
|
1453
1899
|
**visible** one **403**s; plain parentless creates stay open. The moderate
|
|
1454
1900
|
suite then proves the `pending → approved | rejected` transition: an admin
|
|
1455
1901
|
(root manager) approves → public, the **author** self-approve → 403
|
|
1456
|
-
(anti-self-approval), a non-viewer → 404, reject → stays private
|
|
1902
|
+
(anti-self-approval), a non-viewer → 404, reject → stays private, and a cell
|
|
1903
|
+
with no governing root → 400 `cell_not_a_contribution` for any viewer, the
|
|
1904
|
+
404 mask first for a non-viewer. It also
|
|
1457
1905
|
pins the moderation read gate (a pending or rejected post reaches no
|
|
1458
1906
|
non-steward reader past its author and admins on `cell_get`, `cell_list`,
|
|
1459
1907
|
`cell_item_list`, and fact serving, even flipped public or granted while
|
|
@@ -1469,8 +1917,7 @@ moderation_required}`, with `moderation` set per the verdict (`pending`+private
|
|
|
1469
1917
|
|
|
1470
1918
|
`cell_crud` / `cell_relations` gate `true` on TS + Rust (cells run on both, no
|
|
1471
1919
|
`.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
|
|
1920
|
+
fixture). The in-process legs (plain `gro test`) for crud/relations
|
|
1474
1921
|
are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
1475
1922
|
|
|
1476
1923
|
- `cell_relations_parity.db.test.ts`, sharing the full-surface
|
|
@@ -1486,11 +1933,59 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
|
1486
1933
|
`rust_spine_stub_backend_config()` as a convenience preset for the non-domain
|
|
1487
1934
|
third spine consumer, but otherwise backend-specific paths and env are a
|
|
1488
1935
|
consumer concern.
|
|
1936
|
+
- `testing/cross_backend/default_backend_configs.ts` —
|
|
1937
|
+
`make_default_ts_backend_config` / `make_default_rust_backend_config`, the
|
|
1938
|
+
family builders a consumer config composes: the env baseline (`HOST` bound
|
|
1939
|
+
to `DEFAULT_TEST_BACKEND_HOST`, `127.0.0.1`; the port under
|
|
1940
|
+
`port_env_var`; cookie keys; allowed origins; the bootstrap token path; the
|
|
1941
|
+
Rust startup self-wipe), the default paths, and the required `capabilities`.
|
|
1942
|
+
`daemon_token_dir_env` names the env var the binary reads its daemon-token
|
|
1943
|
+
directory from, set to the `{dir}` whose `{dir}/run/daemon_token` is the
|
|
1944
|
+
bootstrap's `daemon_token_path` (`to_daemon_token_dir`, which throws for a
|
|
1945
|
+
path in another layout).
|
|
1946
|
+
- `testing/cross_backend/create_cross_backend_global_setup.ts` —
|
|
1947
|
+
`create_cross_backend_global_setup({configs, derive_name?, provide_key?,
|
|
1948
|
+
ensure_databases?, before_spawn?})`, the single-backend `globalSetup` maker:
|
|
1949
|
+
the backend name derived from the vitest project name selects a config,
|
|
1950
|
+
which is spawned, bootstrapped and provided. `before_spawn` (by backend
|
|
1951
|
+
name; a key naming no config, an inherited one included, throws at
|
|
1952
|
+
construction) runs first for the selected backend — `prepare_cargo_binary`
|
|
1953
|
+
for a Rust one — then its config factory, then `ensure_databases: true`
|
|
1954
|
+
creates the database the config's `postgres:` `env.DATABASE_URL` names
|
|
1955
|
+
(`ensure_database_for_url`), then the spawn. A second argument
|
|
1956
|
+
(`CrossBackendGlobalSetupDeps`) swaps the database creator and the spawn,
|
|
1957
|
+
for testing the setup itself.
|
|
1958
|
+
- `testing/cross_backend/cargo_binary.ts` — `prepare_cargo_binary({workspace_dir,
|
|
1959
|
+
package, bin?, profile?})` builds a Cargo binary (`cargo build -p --bin
|
|
1960
|
+
--profile`, output inherited, a failed build throws) unless
|
|
1961
|
+
`NO_REBUILD_ENV` (`FUZ_TESTING_NO_REBUILD`, `is_no_rebuild_requested`) asks
|
|
1962
|
+
to skip it, and returns its path; `resolve_cargo_binary_path(options)` is the
|
|
1963
|
+
path alone, for a config's `start_command` — under the `target_directory`
|
|
1964
|
+
`cargo metadata` reports (memoized per process per workspace), in the
|
|
1965
|
+
profile's directory (`to_cargo_profile_dir`: `dev` → `debug`;
|
|
1966
|
+
`to_cargo_binary_path` reads it from the metadata JSON). Run as
|
|
1967
|
+
`create_cross_backend_global_setup`'s `before_spawn` hook, the build happens
|
|
1968
|
+
before the spawn's startup window, with its output shown. fuz_app's own stub
|
|
1969
|
+
rebuild (`rebuild_rust_spine_stub`) is built on it.
|
|
1970
|
+
- `testing/cross_backend/postgres_database.ts` — `ensure_database(database,
|
|
1971
|
+
server_url?, deps?)` (idempotent `createdb` on the server the URL names, its
|
|
1972
|
+
password passed in the environment; `createdb`'s already-exists refusal is
|
|
1973
|
+
the quiet success case, `is_createdb_already_exists`, and any other failure
|
|
1974
|
+
is reported through `deps.warn` — `console.warn` by default — with
|
|
1975
|
+
`createdb`'s stderr, never thrown), `ensure_database_for_url(url, deps?)` (the
|
|
1976
|
+
database a `postgres:` URL names; anything else left alone),
|
|
1977
|
+
`with_database_name`, `to_maintenance_database_url`, `redact_database_url`
|
|
1978
|
+
(a URL fit to print), `is_postgres_url`. The harness creates a database only
|
|
1979
|
+
when a setup asks, so a test role needs no `CREATEDB` grant otherwise.
|
|
1489
1980
|
- `testing/cross_backend/spawn_backend.ts` — `spawn_backend(config) => BackendHandle`.
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1981
|
+
Refuses a port that already accepts connections, writes the bootstrap
|
|
1982
|
+
token, deletes a stale daemon-token file, spawns `detached: true` in its
|
|
1983
|
+
own process group (so SIGTERM to the negative pid tears down descendants),
|
|
1984
|
+
polls health (each probe request timed out), reads the deterministic daemon
|
|
1985
|
+
token from the binary-written file. Keeps a bounded stderr tail
|
|
1986
|
+
(`create_output_tail`) for an early exit's error — with its exit code or
|
|
1987
|
+
signal — and for the log line an unexpected exit after startup writes.
|
|
1988
|
+
Registers exit-time + signal cleanup so vitest worker death or
|
|
1494
1989
|
Ctrl+C kills children before they strand ports.
|
|
1495
1990
|
- `testing/transports/fetch_transport.ts` — cookie-threading HTTP transport
|
|
1496
1991
|
satisfying `RpcTestTransport`. Carries a name-keyed cookie jar that
|
|
@@ -1530,6 +2025,18 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
|
1530
2025
|
opens and round-trips `heartbeat` (response id `WS_ADMISSION_PROBE_ID`) so the
|
|
1531
2026
|
connection is admitted when it resolves; suites that wait for a push, or need
|
|
1532
2027
|
connections registered in a known order, open through it.
|
|
2028
|
+
- `testing/cross_backend/ws_endpoint.ts` — `WsEndpointTarget`
|
|
2029
|
+
(`{base_url, ws_path, origin?}`), the endpoint shape the socket-driving
|
|
2030
|
+
cross suites take (flat, or as an optional `ws` on
|
|
2031
|
+
`describe_session_cap_cross_tests`, `describe_testing_backdoor_cross_tests`
|
|
2032
|
+
and the conformance table; `describe_transport_composition_cross_tests`
|
|
2033
|
+
keeps its own nullable `base_url` and defaulted `ws_path`), and
|
|
2034
|
+
`open_session_ws(target, account, {admitted, on_request?, headers?,
|
|
2035
|
+
default_timeout_ms?})`, which opens a socket on an account's session cookie
|
|
2036
|
+
(`to_session_cookie`, asserted present) with `target.origin` or `base_url`
|
|
2037
|
+
as the `Origin` — through `create_admitted_ws_transport` when `admitted`,
|
|
2038
|
+
`create_ws_transport` otherwise. The cookie is the account's static one, so
|
|
2039
|
+
a session rotated through `fixture.transport` upgrades stale.
|
|
1533
2040
|
- `testing/transports/ws_raw_client.ts` — `connect_raw_ws({port, hostname?, path, headers?, pipelined?})`
|
|
1534
2041
|
→ `RawWsClient` (`{frames, send_text, wait_for, destroy}`), a WebSocket client
|
|
1535
2042
|
over a raw `node:net` socket for the two things a client library can't do:
|
|
@@ -1551,8 +2058,12 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
|
1551
2058
|
`raw_http_parse_response` (the first response, `null` until its head and
|
|
1552
2059
|
framed body are whole), `raw_http_count_responses` (unanchored status-line
|
|
1553
2060
|
count, so a response concatenated after a body is seen),
|
|
1554
|
-
`raw_http_first_status
|
|
1555
|
-
|
|
2061
|
+
`raw_http_first_status`, `raw_http_header_lines` (headers as CRLF lines),
|
|
2062
|
+
`raw_http_parse_json` (a body's JSON, `undefined` when it isn't), and
|
|
2063
|
+
`RAW_HTTP_READ_TIMEOUT_MS`, the suites' default read window. Shared by the
|
|
2064
|
+
body-size, body-size smuggling, credential-header, origin-scope and
|
|
2065
|
+
transport-composition suites, which take the 1 MiB cap they probe from
|
|
2066
|
+
`SPINE_BODY_LIMIT_DEFAULT_BYTES` (`spine_surface_constants.ts`).
|
|
1556
2067
|
- `testing/transports/sse_frame_reader.ts` — `create_sse_frame_reader(reader, default_timeout_ms?)`,
|
|
1557
2068
|
the transport-agnostic SSE framing core over a
|
|
1558
2069
|
`ReadableStreamDefaultReader<Uint8Array>`: `\n\n` framing, per-read timeout,
|
|
@@ -1690,10 +2201,52 @@ refs); and a hidden cell's update, a create under a hidden parent, and a clone
|
|
|
1690
2201
|
of a hidden source answer `404` whatever `data` names. Facts are put through
|
|
1691
2202
|
`cross_put_fact` (exported by `fact_serving.ts`), whose `holder` maps onto
|
|
1692
2203
|
`_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
|
|
2204
|
+
`capabilities.fact_serving`; cross-process only. The in-process
|
|
1695
2205
|
cases are `src/test/auth/cell_ref_admission{,.fact_less}.db.test.ts`.
|
|
1696
2206
|
|
|
2207
|
+
### Malformed-envelope and invalid-params parity — `cross_backend/rpc_adversarial.ts`
|
|
2208
|
+
|
|
2209
|
+
`describe_rpc_adversarial_cross_tests({setup_test, surface_source, rpc_path?,
|
|
2210
|
+
live_rpc_endpoints?, skip_methods?})` — the envelope and params groups of
|
|
2211
|
+
`describe_rpc_attack_surface_tests`, sent over `fixture.transport` to a
|
|
2212
|
+
spawned backend. The in-process groups run on stub apps, while the Rust spine
|
|
2213
|
+
parses envelopes and params by hand and the action-manifest parity gate
|
|
2214
|
+
compares no param schemas, so this is what holds the spines to one answer.
|
|
2215
|
+
|
|
2216
|
+
- **envelopes** — every `create_rpc_envelope_cases` case, one test each, on a
|
|
2217
|
+
fresh fixture; status, code, echoed `id`, and `data.reason` per case, and a
|
|
2218
|
+
guard test that the endpoint has both a read and an authenticated mutation to
|
|
2219
|
+
carry them.
|
|
2220
|
+
- **params** — one test per declared method, sending every
|
|
2221
|
+
`generate_rpc_params_test_cases` case and failing with the list of cases not
|
|
2222
|
+
answered 400 `invalid_params` with no `data.reason`. `xfail_params`
|
|
2223
|
+
(`RpcParamsXfail` — `{method, labels, tracking_id, reason}`) moves the named
|
|
2224
|
+
cases of a method into one `xfail_until` test each, for a known divergence on
|
|
2225
|
+
one backend; the method's other cases keep their test, and an entry naming no
|
|
2226
|
+
generated case fails. Pass it only for the backend that diverges — fuz_app
|
|
2227
|
+
keys it on `RUST_SPINE_STUB_DEFAULT_NAME`.
|
|
2228
|
+
|
|
2229
|
+
The authorized principal is the keeper on the first channel the method's auth
|
|
2230
|
+
admits — its session, else its daemon token or API token over a no-`Origin`
|
|
2231
|
+
transport — resolved through `resolve_conformance_principal`.
|
|
2232
|
+
**Preconditions:** the keeper holds every role a declared method gates on
|
|
2233
|
+
(`default_cross_process_setup` seeds `keeper` and `admin`; `extra_keeper_roles`
|
|
2234
|
+
for others) and has a single actor. No generated body carries a valid `acting`,
|
|
2235
|
+
and a malformed one reads as omitted ahead of validation on both spines, so a
|
|
2236
|
+
single-actor keeper resolves its actor and validation refuses; a keeper with a
|
|
2237
|
+
second actor answers `actor_required` first, so such a suite lists the
|
|
2238
|
+
actor-required methods in `skip_methods`.
|
|
2239
|
+
|
|
2240
|
+
**Off the declared surface.** `live_rpc_endpoints` (resolved) adds the methods
|
|
2241
|
+
the endpoint at `rpc_path` live-mounts beyond the declared surface — cell
|
|
2242
|
+
verbs, opt-in resolvers, `_testing_*` backdoors — to both groups: their
|
|
2243
|
+
envelope cases in a group of their own, and a params test per method,
|
|
2244
|
+
`_testing_*` included (a daemon-only backdoor goes with the keeper's daemon
|
|
2245
|
+
token). fuz_app's spine self-test passes `create_spine_live_rpc_endpoints()`.
|
|
2246
|
+
Skips `_testing_*` on the declared surface and `PROTOCOL_ACTION_METHODS`, which
|
|
2247
|
+
`generate_rpc_auth_conformance_cases` skips too.
|
|
2248
|
+
Ungated; cross-process only.
|
|
2249
|
+
|
|
1697
2250
|
### Token-scope surface parity — `cross_backend/token_scope_surface.ts`
|
|
1698
2251
|
|
|
1699
2252
|
`describe_token_scope_surface_cross_tests({setup_test, capabilities, rpc_path?,
|
|
@@ -1708,7 +2261,7 @@ the spec-derived suites can't see it (REST routes, off the declared RPC surface)
|
|
|
1708
2261
|
Mints a narrowed bearer through the production `account_token_create` path and
|
|
1709
2262
|
fires it at the two surfaces whose denial **body** is observable over plain
|
|
1710
2263
|
HTTP — the audit SSE stream (`capabilities.sse`) and the admin-only bare-hash
|
|
1711
|
-
fact read (`capabilities.
|
|
2264
|
+
fact read (`capabilities.fact_serving_admin_hash`) — asserting the exact flat
|
|
1712
2265
|
`{error: 'token_scope_required', required_scope: 'surface:<name>'}` both spines
|
|
1713
2266
|
contract on (`token_scope_denied_body` ↔
|
|
1714
2267
|
`fuz_auth::token_scope_surface_denied_response`).
|
|
@@ -1733,20 +2286,21 @@ _does_ name, and a full bearer must get past the scope gate on the same route.
|
|
|
1733
2286
|
The WS upgrade is deliberately omitted — `create_ws_transport` threads cookies,
|
|
1734
2287
|
not bearer headers, and a refused upgrade's body isn't readable, so it can't
|
|
1735
2288
|
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`.
|
|
2289
|
+
censuses). Cross-process only.
|
|
1738
2290
|
|
|
1739
2291
|
### Front-door contract — `cross_backend/transport_composition.ts`
|
|
1740
2292
|
|
|
1741
2293
|
`describe_transport_composition_cross_tests({setup_test, capabilities, base_url,
|
|
1742
|
-
rpc_path?, origin?, rest_path?, password_path?, ws_path?, sse_path
|
|
2294
|
+
rpc_path?, origin?, rest_path?, password_path?, ws_path?, sse_path?,
|
|
2295
|
+
multi_actor?, rpc_max_body_bytes?})` — the per-transport
|
|
1743
2296
|
composition table: what each transport answers before per-action dispatch,
|
|
1744
2297
|
when one request trips several refusals. Rows are REST `POST` (session-only
|
|
1745
2298
|
`/api/account/logout`), RPC `POST`, RPC `GET`, the WS upgrade (gated on
|
|
1746
2299
|
`capabilities.ws`) and the audit stream (`capabilities.sse`); columns are a
|
|
1747
2300
|
foreign `Origin` with the keeper's session (→ 403 `forbidden_origin`), an
|
|
1748
2301
|
over-cap declared length with a foreign `Origin` and no credential (→ 413
|
|
1749
|
-
`payload_too_large`, body-carrying rows only
|
|
2302
|
+
`payload_too_large`, body-carrying rows only; one byte past the spine default,
|
|
2303
|
+
or past `rpc_max_body_bytes` on the RPC row), no credential (→ 401
|
|
1750
2304
|
`authentication_required`), and a narrowed bearer with no `Origin` (→ 403
|
|
1751
2305
|
`token_scope_required` naming `rpc:<method>` on RPC and `surface:ws_upgrade` on
|
|
1752
2306
|
the upgrade; 403 `credential_type_required` on the session-only REST route and
|
|
@@ -1756,12 +2310,17 @@ envelope's `error.data`. A WS refusal's body is required where the backend
|
|
|
1756
2310
|
declares `capabilities.ws_refusal_body`; where it doesn't (Node's
|
|
1757
2311
|
`@hono/node-ws` drops the body), an empty body passes on its status and a sent
|
|
1758
2312
|
one is still asserted. The table
|
|
1759
|
-
is the exported `transport_composition_cases` data.
|
|
2313
|
+
is the exported `transport_composition_cases` data. `multi_actor:
|
|
2314
|
+
{setup_test, xfail?}` adds `transport_composition_multi_actor_cases`, run on
|
|
2315
|
+
a fixture whose keeper has `extra_actors`: the keeper's session upgrading with
|
|
2316
|
+
no `?acting=` → 400 `actor_required` (flat body), the TS upgrade's actor
|
|
2317
|
+
resolution. The Rust spine's upgrade resolves no actor and admits the socket,
|
|
2318
|
+
so fuz_app passes `xfail` (tracking id `rust-ws-upgrade-acting-fail-open`) for
|
|
2319
|
+
the Rust stub only. Given `base_url`, every
|
|
1760
2320
|
probe is written to the socket (`raw_http_exchange`) and an over-cap request
|
|
1761
2321
|
sends its head alone; with `base_url: null` the probes go through the
|
|
1762
2322
|
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`.
|
|
2323
|
+
in-process leg is `src/test/auth/transport_composition_parity.db.test.ts`.
|
|
1765
2324
|
|
|
1766
2325
|
`cross_backend/narrowed_api_token.ts` — `mint_narrowed_api_token({app,
|
|
1767
2326
|
rpc_path, session_headers, methods, name?})` mints a method-scoped api token
|
|
@@ -1773,7 +2332,7 @@ principal.
|
|
|
1773
2332
|
### Credential deadline parity — `cross_backend/credential_deadline.ts`
|
|
1774
2333
|
|
|
1775
2334
|
`describe_credential_deadline_cross_tests({setup_test, capabilities, base_url,
|
|
1776
|
-
ws_path, sse_path?, rpc_path?})` — a connection closes when the credential it
|
|
2335
|
+
ws_path, sse_path?, rpc_path?, ws_account_roles?})` — a connection closes when the credential it
|
|
1777
2336
|
opened on expires: each case pulls a fresh account's credential expiries in to
|
|
1778
2337
|
two seconds with `_testing_shorten_credential_expiry` before the open, then
|
|
1779
2338
|
checks the connection still dispatches short of the deadline and closes at it.
|
|
@@ -1783,17 +2342,24 @@ stream on a session (`capabilities.sse`) ends; and a second seam call asking
|
|
|
1783
2342
|
for a later expiry moves nothing (the seam only shortens). API token × audit
|
|
1784
2343
|
stream is not reachable over the wire — the stock stream is session-only — so
|
|
1785
2344
|
the Rust spine pins it on a widened gate and the TS twin shares one deadline
|
|
1786
|
-
wait across both transports.
|
|
1787
|
-
`
|
|
2345
|
+
wait across both transports. The cases run concurrently (`describe.concurrent`)
|
|
2346
|
+
under one `setup_test()` for the suite, each on its own account, so a leg waits
|
|
2347
|
+
out one deadline rather than one per case. `ws_account_roles` gives the
|
|
2348
|
+
socket cases' accounts roles, for a backend whose WS upgrade requires one.
|
|
2349
|
+
Cross-process only.
|
|
1788
2350
|
|
|
1789
2351
|
### Origin check scope parity — `cross_backend/origin_scope.ts`
|
|
1790
2352
|
|
|
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`.
|
|
2353
|
+
`describe_origin_scope_cross_tests({setup_test, capabilities, probe_path?,
|
|
2354
|
+
base_url?, unrouted_status?})` — the origin check covers paths outside `/api`:
|
|
2355
|
+
on `GET /ready` (`probe_path`), a foreign `Origin` → 403 `forbidden_origin`, an
|
|
2356
|
+
absent or allowed one → 200, gated on `capabilities.ready`. On an unrouted
|
|
2357
|
+
path, ungated: a foreign `Origin` → 403, an absent one passes to the fallback
|
|
2358
|
+
(`unrouted_status`, default 404 — a backend whose fallback serves an SPA shell
|
|
2359
|
+
passes 200; the in-process leg runs that shape too), and an over-cap `POST`
|
|
2360
|
+
with a foreign `Origin` → 413. Runs both legs (the in-process
|
|
1795
2361
|
`cross_backend/origin_scope_parity.db.test.ts` + the cross-process
|
|
1796
|
-
|
|
2362
|
+
one call).
|
|
1797
2363
|
|
|
1798
2364
|
### Origin verification parity — `cross_backend/origin.ts`
|
|
1799
2365
|
|
|
@@ -1803,7 +2369,7 @@ imperative Origin-verification suite: absent `Origin` → request passes
|
|
|
1803
2369
|
table's. Imperative (not a conformance-table row) because absent-Origin needs
|
|
1804
2370
|
`fresh_transport({origin: null})`. Runs both legs (the
|
|
1805
2371
|
in-process `auth/origin_parity.db.test.ts` + the cross-process
|
|
1806
|
-
|
|
2372
|
+
one call). The promotion surfaced a twin-impl divergence — the
|
|
1807
2373
|
Rust spine returned a plain-text body — now converged to the canonical TS
|
|
1808
2374
|
`{error: "forbidden_origin"}` via `fuz_http::forbidden_origin_response()`.
|
|
1809
2375
|
|
|
@@ -1823,7 +2389,6 @@ keeper's daemon-token channel; the TS unit leg runs the same rows through
|
|
|
1823
2389
|
`run_testing_origin_check` (`src/test/http/origin.conformance.test.ts`), so the
|
|
1824
2390
|
Rust spine is held to the table without reading the file. Ungated; edit the
|
|
1825
2391
|
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
2392
|
|
|
1828
2393
|
### CORS simple-request parity — `cross_backend/simple_request.ts`
|
|
1829
2394
|
|
|
@@ -1839,7 +2404,7 @@ and login, form-encoded login, a forged `text/plain` logout with a session
|
|
|
1839
2404
|
`application/json` with a charset is dispatched. The TS spine checks per route
|
|
1840
2405
|
(`apply_route_specs`), the Rust spine in each JSON handler. Runs both legs:
|
|
1841
2406
|
the in-process `auth/simple_request_parity.db.test.ts` and the cross-process
|
|
1842
|
-
|
|
2407
|
+
one call.
|
|
1843
2408
|
|
|
1844
2409
|
### Query-string shape parity — `cross_backend/query_shape.ts`
|
|
1845
2410
|
|
|
@@ -1850,7 +2415,7 @@ suite over the three spine surfaces that read a query:
|
|
|
1850
2415
|
unknown keys ignored, anonymous junk query → 401 never a query 400), the
|
|
1851
2416
|
JSON-RPC GET endpoint (`method` / `id` / `params` first-wins — a last-wins
|
|
1852
2417
|
reader dispatches a different method per backend on the same request), and
|
|
1853
|
-
`GET /api/facts/:hash` (gated on `capabilities.
|
|
2418
|
+
`GET /api/facts/:hash` (gated on `capabilities.fact_serving_admin_hash`; strict schema —
|
|
1854
2419
|
unknown key / malformed `acting` → 400 `invalid_query_params` **before** the
|
|
1855
2420
|
401 guard, duplicated `acting` first-wins). Imperative because duplicated
|
|
1856
2421
|
query keys can't be expressed through the structured RPC helpers. Pinned
|
|
@@ -1859,8 +2424,7 @@ after the Rust spine's derived-serde `Query` extractors were converged onto
|
|
|
1859
2424
|
"duplicate field" 400 is an anonymously observable backend fingerprint).
|
|
1860
2425
|
Cross-process only — the per-surface in-process TS pins live beside each
|
|
1861
2426
|
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`.
|
|
2427
|
+
`serve_fact_route.db.test.ts`);
|
|
1864
2428
|
|
|
1865
2429
|
### Grant-verb role grammar parity — `cross_backend/role_grant_role_grammar.ts`
|
|
1866
2430
|
|
|
@@ -1875,8 +2439,7 @@ vacuous, and the same refusal for a role exactly at the length cap pins that
|
|
|
1875
2439
|
boundary. Exists because the action-manifest parity gate captures no param
|
|
1876
2440
|
schemas while the Rust spine parses params by hand, and the grant verbs write
|
|
1877
2441
|
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`.
|
|
2442
|
+
reads the audit log behind `_testing_drain_effects`).
|
|
1880
2443
|
|
|
1881
2444
|
### API-token lifetime parity — `cross_backend/token_lifetime.ts`
|
|
1882
2445
|
|
|
@@ -1891,7 +2454,7 @@ schemas while the Rust spine hand-parses params — a spine that silently
|
|
|
1891
2454
|
dropped the field would answer the same mint with a 200 and an eternal
|
|
1892
2455
|
token; only the stored-state read-back catches it. Ungated (mint + list are
|
|
1893
2456
|
on every spine's standard surface). Runs both legs
|
|
1894
|
-
(`token_lifetime_parity.db.test.ts` +
|
|
2457
|
+
(`token_lifetime_parity.db.test.ts` + the cross-process one call).
|
|
1895
2458
|
|
|
1896
2459
|
### Readiness probe parity — `cross_backend/ready.ts`
|
|
1897
2460
|
|
|
@@ -1906,7 +2469,7 @@ success-path gate. Gated on `capabilities.ready`. Imperative (not a
|
|
|
1906
2469
|
JSON-RPC envelope, and the probe needs `fresh_transport({origin: null})` —
|
|
1907
2470
|
the same reasons the sibling `cross_backend/origin.ts` suite is imperative.
|
|
1908
2471
|
Runs both legs (`cross_backend/ready_parity.db.test.ts` +
|
|
1909
|
-
|
|
2472
|
+
the cross-process one call).
|
|
1910
2473
|
|
|
1911
2474
|
Both backends read the **same** committed
|
|
1912
2475
|
`testing/cross_backend/expected_schema.json` (column-presence is engine-portable,
|
|
@@ -1931,8 +2494,7 @@ same MAC under `atob`), an unpadded signature, and correctly signed
|
|
|
1931
2494
|
with `authentication_required`. Controls: re-signing the keeper cookie's
|
|
1932
2495
|
payload reproduces it byte for byte and it authenticates, and a leading-zero
|
|
1933
2496
|
`0N` authenticates (the `u64` bound is on the value, not the digit count).
|
|
1934
|
-
Ungated; cross-process only.
|
|
1935
|
-
`src/test/cross_backend/session_cookie_codec.cross.test.ts`.
|
|
2497
|
+
Ungated; cross-process only.
|
|
1936
2498
|
|
|
1937
2499
|
### Testing-backdoor credential gate — `cross_backend/testing_backdoor.ts`
|
|
1938
2500
|
|
|
@@ -1954,11 +2516,14 @@ runs. Cited property: `docs/security.md`
|
|
|
1954
2516
|
§Test Backdoor Actions. Cross-process only (the `_testing_*` actions are
|
|
1955
2517
|
mounted on the spawned binary, not the in-process app — like the ws/sse
|
|
1956
2518
|
suites); ungated, since every cross backend that uses
|
|
1957
|
-
`default_cross_process_setup` mounts them.
|
|
1958
|
-
`src/test/cross_backend/testing_backdoor.cross.test.ts`. The complement is the
|
|
2519
|
+
`default_cross_process_setup` mounts them. The complement is the
|
|
1959
2520
|
in-process spec-level gate test (`src/test/testing/testing_actions_auth.test.ts`,
|
|
1960
2521
|
asserting each spec declares `credential_types: ['daemon_token']`) + the
|
|
1961
|
-
`assert_no_testing_methods` surface invariant.
|
|
2522
|
+
`assert_no_testing_methods` surface invariant. fuz_app's conformance table
|
|
2523
|
+
also covers the same gates, through the auth rows it generates over the live
|
|
2524
|
+
mount (`live_rpc_endpoints`), which the differential compares across spines;
|
|
2525
|
+
this suite stays for consumers whose backends mount the `_testing_*` actions
|
|
2526
|
+
without a conformance table over their live mount.
|
|
1962
2527
|
|
|
1963
2528
|
### Identity-primitive parity — `cross_backend/identity_parity.ts`
|
|
1964
2529
|
|
|
@@ -2015,7 +2580,7 @@ USERNAME_LENGTH_MAX]` = `[3, 39]` boundary edges (just-outside → 400,
|
|
|
2015
2580
|
|
|
2016
2581
|
Ungated (login + signup are on every spine). Runs both legs via the shared
|
|
2017
2582
|
`{setup_test}` protocol: in-process (`identity_parity.db.test.ts`, plain
|
|
2018
|
-
`gro test`) and cross-process (
|
|
2583
|
+
`gro test`) and cross-process (the one call).
|
|
2019
2584
|
The accepted-case 403s rely on `create_spine_route_specs` zeroing
|
|
2020
2585
|
`signup_fail_floor_ms` (mirroring `login_fail_floor_ms: 0`) so they don't each
|
|
2021
2586
|
pay the 250ms denial floor.
|
|
@@ -2146,6 +2711,15 @@ need a real recipient): admin **assigns** `participant` to a fresh account
|
|
|
2146
2711
|
`participant` → recipient **accepts** → a role_grant lands. The `role_holder`
|
|
2147
2712
|
principal is seeded via `extra_accounts: [{username, roles: ['participant']}]`
|
|
2148
2713
|
(bootstrap-cradle direct grant) and named through `principals.role_holder`.
|
|
2714
|
+
The replay cases count audit rows behind `_testing_drain_effects`, so the
|
|
2715
|
+
backend must mount it — every spine binary does; an in-process leg mounts
|
|
2716
|
+
`create_testing_drain_effects_action` over an emitter built with
|
|
2717
|
+
`track_inflight: true` (`conformance_participation.db.test.ts`).
|
|
2718
|
+
`role` (default `SPINE_PARTICIPANT_ROLE`) names the role conferred, which must
|
|
2719
|
+
be admin-grantable in the backend's registry; `scope_kind` (default `'space'`,
|
|
2720
|
+
or `null` for a builtin role) is the scope the scoped-assign case uses, and
|
|
2721
|
+
`null` skips it. A backend with only the builtin roles runs the suite with
|
|
2722
|
+
`ROLE_ADMIN` — the scoped case skips, as a builtin is global-only.
|
|
2149
2723
|
|
|
2150
2724
|
Both legs run in-process (`conformance_participation.db.test.ts`, plain
|
|
2151
2725
|
`gro test`) and cross-process (`conformance_participation.cross.test.ts`, TS
|
|
@@ -2154,6 +2728,38 @@ the `cross_backend_parity` action-manifest gate green (it was Rust-only before).
|
|
|
2154
2728
|
Cited property: `docs/identity.md` + the role-gated-participation design
|
|
2155
2729
|
(admin-only conferral, immediate assign).
|
|
2156
2730
|
|
|
2731
|
+
### Offer decline + retract refusals — `cross_backend/role_grant_offer_lifecycle.ts`
|
|
2732
|
+
|
|
2733
|
+
`describe_role_grant_offer_lifecycle_cross_tests({setup_test, rpc_path?, role?})` —
|
|
2734
|
+
the error paths of the two verbs that end a pending offer without a grant,
|
|
2735
|
+
which the WS notification suite drives only on the happy path. Canonical
|
|
2736
|
+
answers, from the handlers and the queries behind them
|
|
2737
|
+
(`auth/role_grant_offer_actions.ts`, `auth/role_grant_offer_queries.ts`):
|
|
2738
|
+
|
|
2739
|
+
- **not a party → 404** `not_found` with reason `role_grant_offer_not_found`.
|
|
2740
|
+
Decline is scoped to the recipient account and retract to the grantor actor,
|
|
2741
|
+
so a stranger, the grantor declining, or the recipient retracting hears what
|
|
2742
|
+
a missing offer answers — the case compares the two refusals whole — and the
|
|
2743
|
+
offer stays pending (`docs/security.md` §"404-over-403 is the general mask").
|
|
2744
|
+
The scope check runs before the terminal check, so a stranger hears 404 on
|
|
2745
|
+
a decided offer too.
|
|
2746
|
+
- **decided → 400** `invalid_request` with reason `role_grant_offer_terminal`:
|
|
2747
|
+
a decline or retract after an accept, a second decline, a second retract,
|
|
2748
|
+
and either after the other.
|
|
2749
|
+
- **audit**: a decline writes one `role_grant_offer_decline` row
|
|
2750
|
+
(`account_id` / `actor_id` the recipient, `target_*` the grantor, metadata
|
|
2751
|
+
`{offer_id, role, scope_id}` plus `reason` only when one was given), a
|
|
2752
|
+
retract one `role_grant_offer_retract` row (the grantor, targeting the
|
|
2753
|
+
recipient, no target actor on an account-grain offer); refusals write none.
|
|
2754
|
+
Read through `list_audit_events`.
|
|
2755
|
+
|
|
2756
|
+
The single-request rows — decline and retract of an id naming no offer → 404
|
|
2757
|
+
— are in the conformance security slate, so the differential sends them to
|
|
2758
|
+
both spines. The keeper is the grantor and offers `role` — an admin-grantable
|
|
2759
|
+
role, `SPINE_PARTICIPANT_ROLE` by default; `ROLE_ADMIN` serves a backend with
|
|
2760
|
+
only the builtin roles. Ungated. The
|
|
2761
|
+
in-process leg is on the drained setup of `conformance_participation.db.test.ts`.
|
|
2762
|
+
|
|
2157
2763
|
### Building a TS test-server binary — `testing_server_core.ts` + adapters
|
|
2158
2764
|
|
|
2159
2765
|
The reusable shape for standing up a **spawnable TS** cross-process test
|
|
@@ -2165,7 +2771,7 @@ re-roll the serve / daemon-info / WS-attach / drain boilerplate:
|
|
|
2165
2771
|
- `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
2772
|
- `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
2773
|
- `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.
|
|
2774
|
+
- `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
2775
|
- `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
2776
|
|
|
2171
2777
|
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.
|