@fuzdev/fuz_app 0.148.0 → 0.150.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.
Files changed (210) hide show
  1. package/dist/actions/CLAUDE.md +11 -9
  2. package/dist/actions/action_bridge.d.ts +7 -0
  3. package/dist/actions/action_bridge.d.ts.map +1 -1
  4. package/dist/actions/action_bridge.js +10 -0
  5. package/dist/actions/action_event.d.ts.map +1 -1
  6. package/dist/actions/action_event.js +14 -1
  7. package/dist/actions/action_event_types.d.ts +1 -1
  8. package/dist/actions/action_event_types.d.ts.map +1 -1
  9. package/dist/actions/perform_action.d.ts +17 -1
  10. package/dist/actions/perform_action.d.ts.map +1 -1
  11. package/dist/actions/perform_action.js +30 -7
  12. package/dist/actions/register_action_ws.d.ts +8 -2
  13. package/dist/actions/register_action_ws.d.ts.map +1 -1
  14. package/dist/actions/register_action_ws.js +24 -4
  15. package/dist/actions/transports_ws_auth_guard.d.ts +6 -4
  16. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  17. package/dist/actions/transports_ws_auth_guard.js +9 -6
  18. package/dist/actions/ws_endpoint_spec.d.ts +1 -1
  19. package/dist/auth/CLAUDE.md +72 -33
  20. package/dist/auth/account_action_specs.d.ts +12 -0
  21. package/dist/auth/account_action_specs.d.ts.map +1 -1
  22. package/dist/auth/account_action_specs.js +16 -2
  23. package/dist/auth/account_actions.d.ts.map +1 -1
  24. package/dist/auth/account_actions.js +7 -4
  25. package/dist/auth/actor_lookup_queries.d.ts +3 -2
  26. package/dist/auth/actor_lookup_queries.d.ts.map +1 -1
  27. package/dist/auth/actor_lookup_queries.js +6 -3
  28. package/dist/auth/actor_search_queries.d.ts +5 -2
  29. package/dist/auth/actor_search_queries.d.ts.map +1 -1
  30. package/dist/auth/actor_search_queries.js +11 -4
  31. package/dist/auth/admin_action_specs.d.ts +4 -4
  32. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  33. package/dist/auth/admin_action_specs.js +33 -12
  34. package/dist/auth/admin_actions.d.ts.map +1 -1
  35. package/dist/auth/admin_actions.js +35 -6
  36. package/dist/auth/all_cell_actions.d.ts +11 -2
  37. package/dist/auth/all_cell_actions.d.ts.map +1 -1
  38. package/dist/auth/all_cell_actions.js +2 -1
  39. package/dist/auth/audit_emitter.d.ts +23 -3
  40. package/dist/auth/audit_emitter.d.ts.map +1 -1
  41. package/dist/auth/audit_emitter.js +14 -4
  42. package/dist/auth/audit_log_schema.d.ts +1 -0
  43. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  44. package/dist/auth/audit_log_schema.js +9 -5
  45. package/dist/auth/bootstrap_account.d.ts +25 -1
  46. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  47. package/dist/auth/bootstrap_account.js +37 -4
  48. package/dist/auth/bootstrap_routes.d.ts +3 -2
  49. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  50. package/dist/auth/bootstrap_routes.js +5 -4
  51. package/dist/auth/cell_action_specs.d.ts +215 -15
  52. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  53. package/dist/auth/cell_action_specs.js +103 -16
  54. package/dist/auth/cell_actions.d.ts +40 -18
  55. package/dist/auth/cell_actions.d.ts.map +1 -1
  56. package/dist/auth/cell_actions.js +126 -19
  57. package/dist/auth/cell_audit_events.d.ts +1 -1
  58. package/dist/auth/cell_audit_events.d.ts.map +1 -1
  59. package/dist/auth/cell_audit_events.js +3 -2
  60. package/dist/auth/cell_audit_metadata.d.ts +11 -0
  61. package/dist/auth/cell_audit_metadata.d.ts.map +1 -1
  62. package/dist/auth/cell_audit_metadata.js +10 -0
  63. package/dist/auth/cell_grant_action_specs.d.ts +7 -0
  64. package/dist/auth/cell_grant_action_specs.d.ts.map +1 -1
  65. package/dist/auth/cell_grant_action_specs.js +7 -0
  66. package/dist/auth/cell_grant_actions.d.ts +36 -4
  67. package/dist/auth/cell_grant_actions.d.ts.map +1 -1
  68. package/dist/auth/cell_grant_actions.js +20 -3
  69. package/dist/auth/credential_type_schema.d.ts +3 -2
  70. package/dist/auth/credential_type_schema.d.ts.map +1 -1
  71. package/dist/auth/credential_type_schema.js +3 -2
  72. package/dist/auth/role_grant_offer_action_specs.d.ts +18 -2
  73. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  74. package/dist/auth/role_grant_offer_action_specs.js +30 -6
  75. package/dist/auth/role_grant_offer_actions.d.ts +9 -4
  76. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  77. package/dist/auth/role_grant_offer_actions.js +58 -7
  78. package/dist/auth/role_grant_offer_notifications.d.ts +3 -2
  79. package/dist/auth/role_grant_offer_notifications.d.ts.map +1 -1
  80. package/dist/auth/role_grant_offer_notifications.js +3 -2
  81. package/dist/auth/role_grant_offer_queries.d.ts +20 -0
  82. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  83. package/dist/auth/role_grant_offer_queries.js +35 -0
  84. package/dist/auth/role_grant_queries.d.ts +27 -9
  85. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  86. package/dist/auth/role_grant_queries.js +40 -13
  87. package/dist/auth/role_schema.d.ts +14 -8
  88. package/dist/auth/role_schema.d.ts.map +1 -1
  89. package/dist/auth/role_schema.js +6 -3
  90. package/dist/auth/self_service_role_actions.d.ts.map +1 -1
  91. package/dist/auth/self_service_role_actions.js +20 -0
  92. package/dist/db/CLAUDE.md +9 -2
  93. package/dist/db/cell_ddl.d.ts +9 -1
  94. package/dist/db/cell_ddl.d.ts.map +1 -1
  95. package/dist/db/cell_ddl.js +11 -1
  96. package/dist/db/cell_queries.d.ts +53 -7
  97. package/dist/db/cell_queries.d.ts.map +1 -1
  98. package/dist/db/cell_queries.js +61 -7
  99. package/dist/http/CLAUDE.md +14 -2
  100. package/dist/http/body_limit.d.ts +3 -1
  101. package/dist/http/body_limit.d.ts.map +1 -1
  102. package/dist/http/body_limit.js +6 -1
  103. package/dist/http/error_schemas.d.ts +8 -0
  104. package/dist/http/error_schemas.d.ts.map +1 -1
  105. package/dist/http/error_schemas.js +12 -0
  106. package/dist/http/jsonrpc.d.ts +9 -0
  107. package/dist/http/jsonrpc.d.ts.map +1 -1
  108. package/dist/http/jsonrpc.js +17 -0
  109. package/dist/http/jsonrpc_errors.d.ts +40 -15
  110. package/dist/http/jsonrpc_errors.d.ts.map +1 -1
  111. package/dist/http/jsonrpc_errors.js +99 -35
  112. package/dist/http/proxy.d.ts +4 -2
  113. package/dist/http/proxy.d.ts.map +1 -1
  114. package/dist/http/proxy.js +7 -2
  115. package/dist/http/surface.d.ts +3 -1
  116. package/dist/http/surface.d.ts.map +1 -1
  117. package/dist/http/surface.js +7 -4
  118. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  119. package/dist/realtime/sse_auth_guard.js +3 -2
  120. package/dist/schema_meta.d.ts +92 -1
  121. package/dist/schema_meta.d.ts.map +1 -1
  122. package/dist/schema_meta.js +117 -0
  123. package/dist/server/app_server.d.ts +13 -5
  124. package/dist/server/app_server.d.ts.map +1 -1
  125. package/dist/server/app_server.js +32 -8
  126. package/dist/testing/CLAUDE.md +196 -82
  127. package/dist/testing/adversarial_input_cases.d.ts +67 -7
  128. package/dist/testing/adversarial_input_cases.d.ts.map +1 -1
  129. package/dist/testing/adversarial_input_cases.js +210 -165
  130. package/dist/testing/app_server.d.ts +3 -1
  131. package/dist/testing/app_server.d.ts.map +1 -1
  132. package/dist/testing/audit_drift_guard.d.ts +3 -2
  133. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  134. package/dist/testing/audit_drift_guard.js +5 -3
  135. package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
  136. package/dist/testing/cross_backend/actor_lookup.js +27 -0
  137. package/dist/testing/cross_backend/audit_cross_helpers.d.ts +6 -0
  138. package/dist/testing/cross_backend/audit_cross_helpers.d.ts.map +1 -1
  139. package/dist/testing/cross_backend/audit_cross_helpers.js +11 -0
  140. package/dist/testing/cross_backend/capabilities.d.ts +29 -5
  141. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  142. package/dist/testing/cross_backend/capabilities.js +10 -6
  143. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  144. package/dist/testing/cross_backend/cell_crud.js +117 -2
  145. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  146. package/dist/testing/cross_backend/cell_gated_create.js +109 -13
  147. package/dist/testing/cross_backend/cell_moderate.d.ts +7 -1
  148. package/dist/testing/cross_backend/cell_moderate.d.ts.map +1 -1
  149. package/dist/testing/cross_backend/cell_moderate.js +68 -4
  150. package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -1
  151. package/dist/testing/cross_backend/cell_ref_admission.js +18 -9
  152. package/dist/testing/cross_backend/conformance_table.d.ts +5 -0
  153. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  154. package/dist/testing/cross_backend/conformance_table.js +12 -10
  155. package/dist/testing/cross_backend/cross_rpc.d.ts +3 -1
  156. package/dist/testing/cross_backend/cross_rpc.d.ts.map +1 -1
  157. package/dist/testing/cross_backend/cross_rpc.js +2 -2
  158. package/dist/testing/cross_backend/default_backend_configs.d.ts +9 -7
  159. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  160. package/dist/testing/cross_backend/default_backend_configs.js +3 -3
  161. package/dist/testing/cross_backend/differential.d.ts +7 -0
  162. package/dist/testing/cross_backend/differential.d.ts.map +1 -1
  163. package/dist/testing/cross_backend/differential.js +17 -0
  164. package/dist/testing/cross_backend/expected_schema.json +1 -0
  165. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  166. package/dist/testing/cross_backend/full_spine_mount.js +5 -4
  167. package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts.map +1 -1
  168. package/dist/testing/cross_backend/role_grant_offer_lifecycle.js +74 -2
  169. package/dist/testing/cross_backend/rpc_adversarial.d.ts +9 -0
  170. package/dist/testing/cross_backend/rpc_adversarial.d.ts.map +1 -1
  171. package/dist/testing/cross_backend/rpc_adversarial.js +16 -6
  172. package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts +1 -1
  173. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  174. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -3
  175. package/dist/testing/cross_backend/setup.d.ts +2 -2
  176. package/dist/testing/cross_backend/spine_suites.d.ts +85 -5
  177. package/dist/testing/cross_backend/spine_suites.d.ts.map +1 -1
  178. package/dist/testing/cross_backend/spine_suites.js +291 -20
  179. package/dist/testing/cross_backend/spine_surface_constants.d.ts +15 -0
  180. package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
  181. package/dist/testing/cross_backend/spine_surface_constants.js +24 -0
  182. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +18 -19
  183. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
  184. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +66 -42
  185. package/dist/testing/cross_backend/testing_backdoor.d.ts +27 -4
  186. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  187. package/dist/testing/cross_backend/testing_backdoor.js +136 -41
  188. package/dist/testing/cross_backend/testing_reset_actions.d.ts +30 -15
  189. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  190. package/dist/testing/cross_backend/testing_reset_actions.js +52 -35
  191. package/dist/testing/cross_backend/transport_composition.js +3 -3
  192. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  193. package/dist/testing/cross_backend/ts_spine_backend_config.js +6 -3
  194. package/dist/testing/rpc_attack_surface.d.ts +10 -0
  195. package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
  196. package/dist/testing/rpc_attack_surface.js +15 -3
  197. package/dist/testing/rpc_envelope_cases.d.ts +5 -2
  198. package/dist/testing/rpc_envelope_cases.d.ts.map +1 -1
  199. package/dist/testing/rpc_envelope_cases.js +4 -1
  200. package/dist/testing/rpc_round_trip.js +3 -3
  201. package/dist/testing/schema_generators.d.ts +99 -2
  202. package/dist/testing/schema_generators.d.ts.map +1 -1
  203. package/dist/testing/schema_generators.js +244 -76
  204. package/dist/testing/stubs.d.ts +4 -3
  205. package/dist/testing/stubs.d.ts.map +1 -1
  206. package/dist/testing/stubs.js +6 -4
  207. package/dist/zod.d.ts +9 -0
  208. package/dist/zod.d.ts.map +1 -1
  209. package/dist/zod.js +20 -0
  210. package/package.json +1 -1
@@ -32,7 +32,7 @@ time (never runtime), where a throwing guard would break `vite build`.
32
32
  - `stub_handler()` — fresh `Response('stub')`.
33
33
  - `stub_mw` — pass-through middleware (`async (_c, next) => next()`).
34
34
  - `create_stub_app_deps()` — factory: fresh `AppDeps` with no-op keyring/password/`delete_file`, a `read_secure_file` that throws ENOENT (the no-token-file state), a `create_noop_stub` DB, silent `Logger`, no-op `audit`, an empty `connection_closer`.
35
- - `create_test_audit_emitter()` — no-op `AuditEmitter`; `emit` / `emit_role_grant_target` no-op, `emit_pool` resolves immediately, `notify` no-op, `add_listener` throws (use `create_recording_audit_emitter` for a listener-accepting emitter), `listener_count` returns 0, `drain_inflight` resolves immediately.
35
+ - `create_test_audit_emitter()` — no-op `AuditEmitter`; `emit` / `emit_role_grant_target` no-op, `emit_pool` resolves immediately, `notify` no-op, `add_listener` throws (use `create_recording_audit_emitter` for a listener-accepting emitter), `listener_count` returns 0, `drain_inflight` resolves immediately, `is_tracking_inflight` returns `false`.
36
36
  - `create_stub_audit_sse()` — no-op `AuditLogSse` for surface-test wiring without booting real SSE. `on_audit_event` no-op (nothing is broadcast); `required_role` is `admin`; `registry` is a fresh `SubscriberRegistry` (live `.count` / `.close_*` for registry-state tests, isolated per call). For real SSE plumbing build via `create_audit_log_sse` against `create_test_app`.
37
37
  - `create_stub_api_middleware({include_daemon_token?, origin_carve_outs?})` — stub `MiddlewareSpec[]` matching `create_auth_middleware_specs`'s output (origin/session/request_context/bearer_auth, optional daemon_token) for surface generation without booting real auth — the same names, paths, order, and declared `errors` (`origin` declares a 403; `bearer_auth` and `daemon_token` declare none, since both soft-fail through `next()`), pinned against the real factory by `auth/middleware.mounted.test.ts`. The surface merges middleware errors into every route under the path, so a stub declaring more would widen the test surface with statuses the live server never returns. Each spec gets its own pass-through handler (not the shared `stub_mw`), so `assert_middleware_stack_mounted` tells the layers apart. See `auth/CLAUDE.md` §Middleware for the real stack.
38
38
  - `create_stub_app_backend(options?)` — stub `AppBackend` with no database: `create_stub_app_deps()` (or `options.deps`), no migration results, a `close` that does nothing (or `options.close`). For assembling a server in tests that never reach the database.
@@ -345,7 +345,11 @@ field a loose output schema passes on one spine, shows up as a difference.
345
345
  one alone, registration throws. Per case, each backend's own `setup_test`
346
346
  (separate databases and keepers), the principal resolved and the request
347
347
  sent through `send_conformance_case` (`conformance_table.ts`), both answers
348
- normalized and diffed. The per-case `expect` is not asserted —
348
+ normalized and diffed. Every HTTP request carries
349
+ `DIFFERENTIAL_REQUEST_HEADERS` (`accept-encoding: identity`), so a runtime's
350
+ own compression (`Deno.serve` gzips for a request accepting it, as Node's
351
+ `fetch` sends by default) never reads as a `content-encoding` / `vary`
352
+ difference. The per-case `expect` is not asserted —
349
353
  each backend's conformance leg holds it. `KnownDivergence` is the ledger
350
354
  row: `{case_name | method, tracking_id, reason, what_differs}`, the names a
351
355
  string or a list, `what_differs` the diff paths covered (a path covers
@@ -474,11 +478,15 @@ Support functions:
474
478
 
475
479
  Walks Zod schemas to generate valid values for adversarial/round-trip tests.
476
480
 
477
- - `detect_format(field_schema)` — reads `format` / `pattern` from the JSON Schema representation.
478
- - `generate_valid_value(field, field_schema)` — base-type switch producing a valid sample (UUIDs → nil UUID, strings → `'xxxxxxxxxx'`, numbers → `1`, objects → recurse, enums → first entry, etc.). For branded-string refinements, walks a fallback chain synthesized from the `pattern` string the JSON Schema representation exposes: fixed-length hex (`^[0-9a-f]{N}$` — blake3 / sha256 / md5 digests; `0`.repeat(N)), prefix-lengthed slug (`^<prefix>_[A-Za-z0-9_-]{N}$` — `ApiTokenId`-style ids; `<prefix>_` + `x`.repeat(N)), absolute path prefix, URL prefix. First candidate that `safeParse` accepts is used.
481
+ - `to_field_json_schema(field_schema)` — the field's JSON Schema rendered with `io: 'input'`, so a `.transform()` or pipe field (`Username`) yields its input-side bounds and pattern instead of throwing; a `.nullable()` / `.nullish()` field's one non-null `anyOf` branch is returned; `null` for a type JSON Schema can't represent. A pipe yields its input stage only, and a `.refine()` never appears (a code-point length bound generates nothing). Every JSON Schema read in the generators goes through it.
482
+ - `detect_format(field_schema)` — `detect_json_schema_format` over `to_field_json_schema`: the `format`, `'pattern'` for a regex, or `null`.
483
+ - `generate_valid_value(field, field_schema)` — base-type switch producing a valid sample (UUIDs → nil UUID, strings → `'xxxxxxxxxx'`, numbers → `1`, objects → `generate_valid_object` against the field schema, so a nested refinement is satisfied too, enums → first entry, etc.). For branded-string refinements, walks a fallback chain synthesized from the `pattern` string the JSON Schema representation exposes: fixed-length hex (`^[0-9a-f]{N}$` — blake3 / sha256 / md5 digests; `0`.repeat(N)), prefix-lengthed slug (`^<prefix>_[A-Za-z0-9_-]{N}$` — `ApiTokenId`-style ids; `<prefix>_` + `x`.repeat(N)), absolute path prefix, URL prefix, two `/`-joined segments (an `owner/name` slug). First candidate that `safeParse` accepts is used.
484
+ - `generate_valid_string_param(field, field_schema)` — a string a URL-sourced field (route param, query value) accepts: the nil UUID for a UUID field, else the first of `test_value`, the generated valid value as a string, `1`, `0`, `true`, `false`, the field's JSON Schema `default` or lower bound as a string (a preprocessed integer whose minimum is above one) and a synthesized string the field accepts; `null` when none does. The params and query case generators build their base from it.
479
485
  - `resolve_valid_path(path, params_schema?)` — swaps `:param` for a value the params schema accepts. Prefers `test_<name>` wherever the field schema admits it (the param name keeps failure messages readable); otherwise a UUID param takes the nil UUID and anything else is synthesized from the schema, percent-encoded. A synthesized value containing `/` is refused (it would route elsewhere) and falls back to `test_<name>`.
480
486
  - `to_route_content_type(route)` — the `Content-Type` a well-formed request to the route sends: `application/json`, or `application/octet-stream` for a `raw_body` route. Every `POST` route refuses anything else with 415 before its other gates, so every probe in the harnesses (auth, params and query fuzz, 404, data exposure, round trip) sends it, body or not.
481
- - `generate_valid_body(input_schema) => Record<string, unknown> | undefined` — builds a body satisfying the input schema. Throws with Zod `issues` if the generated body fails validation — surfaces broken generation logic with a descriptive error rather than a confusing 400 downstream.
487
+ - `generate_valid_object(object_schema, schema) => {value, result}` — required and defaulted fields; when `schema` (refinements included) refuses those, each optional field alone in declaration order, the first that parses; failing that, the optional fields added cumulatively until it parses. Covers a `.refine()` over optional fields (at least one of, exactly one of, both of) at any depth, and, where one optional field suffices, keeps the others (an `acting` declared first) out. Returns the last `safeParse` result and leaves the throw to the caller. `format_zod_issues(error)` renders the issues for those errors.
488
+ - `generate_valid_object_with_field(object_schema, schema, base, field)` — a valid object carrying `field`, built from the valid `base`: `base` itself when it carries the field; else the field joins `base`, with the optional fields `base` omits filled as `generate_valid_object` fills them (a pair rule: `scope_kind` needs `scope_id`); failing that, the field replaces `base`'s optional fields, refilled around it (exactly one of: adding `slug` drops `id`). `null` when no object the generator builds carries the field. The body a single-fault case on a field the base omits changes.
489
+ - `generate_valid_body(input_schema) => Record<string, unknown> | undefined` — builds a body satisfying the input schema (`generate_valid_object`). Throws with Zod `issues` if the generated body fails validation — surfaces broken generation logic with a descriptive error rather than a confusing 400 downstream.
482
490
 
483
491
  ### `integration_helpers.ts` — route lookup + body checks
484
492
 
@@ -534,9 +542,12 @@ All cases expect 400 with one of `ERROR_INVALID_REQUEST_BODY` /
534
542
  The generators live in the hono-free `adversarial_input_cases.ts`, so the
535
543
  cross-process suites reuse them:
536
544
 
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.
545
+ - `generate_input_test_cases(input_schema, options?)` → `InputTestCase[]` — whole-body structural (non-object, extra key when `strictObject`), missing required fields, one wrong-type per field, null for required non-nullable, one format violation per constrained field, and boundary cases read from the field's input-side JSON Schema (`to_field_json_schema` — through a transform or pipe, and under a nullable's `anyOf`): the empty string, one under `minLength` (above one; a prefix of the valid value) and one over `maxLength`; below `minimum`, above `maximum`, at an exclusive bound, zero and negative for a positive-only field, a non-integer for an integer field and a value off a `multipleOf` grid (each half a step from the valid value, inside the range); the empty array, one under `minItems` (above one; the valid value's own items) and one over `maxItems`. A per-field case is emitted only when the field refuses its value (a `z.coerce` field given a wrong type, a pattern admitting the violation string, a preprocess that repairs the value: dropped), so no generated case is accidentally valid. Each per-field case is single-fault — valid but for the field it breaks: it changes one field of a valid base (`generate_valid_object`'s, or `options.base` (`InputTestCaseOptions`) for an input whose valid shape the generator can't reach), or, for a field that base omits, of the body `generate_valid_object_with_field` builds around it, so a case can't also break an object-level rule. A field no valid body carries keeps its wrong-type and null cases on the base and gets no format or boundary cases. Either base failing the schema throws, naming `options.label` (the method or route) when given. A per-field case names the field it breaks (`InputTestCase.field`; absent on the whole-body cases).
546
+ - `expected_invalid_reason(input_schema, test_case)` → `string | undefined` — the `data.reason` an RPC refusal of the case owes: the broken field's `invalid_reason` (`schema_meta.ts`), resolved by the same `resolve_invalid_reason` the dispatcher resolves through, or none for an undeclared field or a whole-body case.
547
+ - `generate_params_test_cases(params_schema)` → `ParamsTestCase[]` — format violations only (unconstrained string params accept anything), each one the field refuses.
548
+ - `generate_query_test_cases(query_schema)` → `QueryTestCase[]` — missing required fields the field refuses to go without (a preprocessed field with a default accepts its absence) + format violations the field refuses.
549
+
550
+ Both build their base from `generate_valid_string_param` and check it against the whole schema, throwing when a field has no string it accepts or the record fails, so each case breaks one field of a valid record (a URL-sourced integer such as `db_routes`' `offset` / `limit` gets `'1'`, not a `test_value` it refuses).
540
551
 
541
552
  GET-with-input routes hit the RPC `?params=` query convention; invalid-JSON
542
553
  arrays there collapse to `ERROR_INVALID_REQUEST_BODY` (schema failure)
@@ -626,8 +637,8 @@ envelope — and a 401 to a non-public call (`assert_rpc_authenticated`): the
626
637
  call carried a credential its auth admits, so a 401 means it stopped working.
627
638
  A public method may still answer 401 (a wrong-password login), and so may a
628
639
  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
640
+ (`service_unavailable` 503, `timeout` 504, a consumer code declared with
641
+ `register_jsonrpc_error_code`) is a server condition and passes like a
631
642
  4xx. The failure names the method, status, code and message; a non-JSON body
632
643
  is reported as text. Options: `{setup_test, surface_source, session_options,
633
644
  rpc_endpoints, skip_methods?, input_overrides?, success_fixtures?}`.
@@ -927,9 +938,10 @@ loses coverage.
927
938
 
928
939
  `rpc_success_fixtures`, `rpc_skip_methods` and `rpc_input_overrides`
929
940
  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.
941
+ `input_overrides` (same options on `describe_standard_cross_process_tests`;
942
+ `describe_spine_cross_suites` also forwards `rpc_input_overrides` to
943
+ `rpc_adversarial`) — `rpc_skip_methods` is the consumer escape hatch for a
944
+ method the generic round-trip can't drive in its test wiring.
933
945
 
934
946
  `StandardTestOptions` requires `create_route_specs` (for rate_limiting) and
935
947
  `rpc_endpoints` (for admin/audit_completeness/rpc_round_trip); the admin
@@ -1003,12 +1015,12 @@ Registry lookups:
1003
1015
  - correct auth passes (not 401/403)
1004
1016
  - GET unauthenticated for `side_effects: false` reads
1005
1017
  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.
1018
+ 3. **RPC adversarial params** — `generate_rpc_params_test_cases(input, options?)` (`rpc_envelope_cases.ts`): `generate_input_test_cases` minus its structural cases (a non-object `params` is refused at the envelope, not at params validation), taking the same `{base?, label?}`. Every case expects 400 `invalid_params` with `data.reason` the broken field's declared `invalid_reason` (`expected_invalid_reason`), absent for an undeclared field. Shared with the cross-process suite. `input_overrides` (method name → valid params) seeds a method's cases from that base; an override its schema refuses throws at collection, naming the method.
1007
1019
 
1008
1020
  Skips silently when `surface.rpc_endpoints` is empty. Uses stub deps — no
1009
1021
  DB needed.
1010
1022
 
1011
- Options: `{build: () => AppSurfaceSpec, roles: Array<string>}`.
1023
+ Options: `{build: () => AppSurfaceSpec, roles: Array<string>, input_overrides?}`.
1012
1024
 
1013
1025
  **Opt-in bundles need their own per-bundle suite file.** Action bundles not
1014
1026
  folded into `create_standard_rpc_actions` (today `self_service_role_actions`,
@@ -1128,7 +1140,10 @@ file; the dual-spawn and limiter-enabled suites name their own files.
1128
1140
  `fact_serving_admin_hash`, `ready`,
1129
1141
  `peer_request`, `ws_account_actions`, `ws_role_gated`), and the family
1130
1142
  defaults (`ts_default_capabilities` / `rust_default_capabilities`, typed
1131
- `FamilyDefaultCapabilities`) omit them, with `capabilities` required on
1143
+ `FamilyDefaultCapabilities`) omit them and the `testing_backdoors` list (the
1144
+ `_testing_*` backdoors the backend mounts, from `TESTING_BACKDOOR_METHODS` —
1145
+ §Testing-backdoor declaration below; `[]` in-process, every one on fuz_app's
1146
+ spine presets), with `capabilities` required on
1132
1147
  `make_default_{ts,rust}_backend_config` — so a config that leaves one out
1133
1148
  fails typecheck naming it, rather than silently skipping its suites. The
1134
1149
  family defaults carry the declared surface's mounts (`account_lifecycle`,
@@ -1162,7 +1177,8 @@ file; the dual-spawn and limiter-enabled suites name their own files.
1162
1177
  `fact_holder` row, so a non-admin can reference what it seeds, and whose
1163
1178
  optional `external_url` writes a bare external row naming that URL, so the
1164
1179
  suite can pin that a URL naming another hash's file is refused) — it also
1165
- gates `describe_cell_ref_admission_cross_tests`. `fact_serving_admin_hash`
1180
+ gates `describe_cell_ref_admission_cross_tests` (its grant, item, and clone
1181
+ cases with `cell_relations`). `fact_serving_admin_hash`
1166
1182
  is the admin-only bare-hash route (`GET /api/facts/:hash`), its own flag
1167
1183
  because a backend can serve the cell-gated route alone: it gates the
1168
1184
  fact-serving suite's bare-hash cases (with `fact_serving`) and the bare-hash
@@ -1224,11 +1240,15 @@ the omitted suites.
1224
1240
  One call that runs every shipped single-backend cross suite against one
1225
1241
  spawned backend: `describe_spine_cross_suites(handle, {surface_source,
1226
1242
  session_options, rpc_endpoints, live_rpc_endpoints?, roles?, setup?, exclude?,
1227
- overrides?, rpc_success_fixtures?, rpc_skip_methods?, rpc_input_overrides?,
1243
+ exclude_cases?, overrides?, rpc_success_fixtures?, rpc_skip_methods?, rpc_input_overrides?,
1228
1244
  rpc_max_body_bytes?, ws_max_message_bytes?})`.
1229
1245
  `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.
1246
+ also walks the methods it keeps off the declared surface. `rpc_input_overrides`
1247
+ reaches the standard bundle's RPC round-trip and `rpc_adversarial`'s
1248
+ `input_overrides`, so one valid body per method serves both; a key naming a
1249
+ method on neither the declared surface nor `live_rpc_endpoints` throws at
1250
+ collection. Without it a consumer writes a file per suite, and a suite added
1251
+ here never reaches it.
1232
1252
 
1233
1253
  - **The registry.** `SUITE_NAMES` / `SuiteName` — the closed set of suites the
1234
1254
  call runs, each named by its module (`ws_round_trip` is
@@ -1270,13 +1290,51 @@ consumer writes a file per suite, and a suite added here never reaches it.
1270
1290
  takes it as `rpc_max_body_bytes`), the second `ws_round_trip`'s
1271
1291
  `max_message_bytes` — one value per limit for every suite probing it.
1272
1292
  - **`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.
1293
+ run as a skipped test. An empty reason, an unknown suite name, a suite
1294
+ both excluded and overridden, `testing_backdoor` (it checks the backdoor
1295
+ declaration the gating reads), or a suite already skipping itself for an
1296
+ undeclared backdoor throws before anything registers.
1297
+ - **Backdoors.** `capabilities.testing_backdoors` must declare
1298
+ `SPINE_SUITES_REQUIRED_BACKDOORS` (`_testing_reset`, every setup's per-test
1299
+ reset, and `_testing_drain_effects`, the audit-read barrier), and
1300
+ `_testing_put_fact` when `fact_serving` is declared; the call throws
1301
+ otherwise. A suite every case of which drives another backdoor
1302
+ (`SuiteEntry.backdoors`: `credential_deadline` →
1303
+ `_testing_shorten_credential_expiry`, `origin_conformance` →
1304
+ `_testing_origin_check`, `ws_outbound_overflow` →
1305
+ `_testing_flood_notifications`) skips itself on a backend not declaring it,
1306
+ as a skipped test beside the excludes whose reason names the backdoor; an
1307
+ exclude, override or `exclude_cases` entry for it throws as dead.
1308
+ `spine_suites.test.ts` holds the backdoors each suite module names — quoted
1309
+ `_testing_*` names, `testing_*_action_spec` references, and the
1310
+ `cross_put_fact` / `cross_drain_effects` seeders — to its entry's
1311
+ `backdoors`, the required pair, or `_testing_put_fact` under
1312
+ `capabilities.fact_serving`; a helper module the suite calls isn't read.
1313
+ - **`exclude_cases`** — `SuiteCaseExclusions`, `{[suite]: {[name path]:
1314
+ reason}}`: single tests of a suite that runs, each by its name path (the
1315
+ describe titles and the test name joined with `' > '` — what the reporter
1316
+ prints after the file and any `describe` wrapping the call; a full path, not
1317
+ a leaf name, so a title repeated under two describes names one test). One
1318
+ `beforeEach` on the calling suite (`register_spine_case_exclusions`) skips
1319
+ them at run time with `ctx.skip(reason)`, so the suite files stay as they
1320
+ are and each shows as skipped with its reason; it finds a test's suite
1321
+ from the top-level task ranges each suite registered (read through
1322
+ `TestRunner.getCurrentSuite()` at collection), so an entry matches only
1323
+ below its own suite. A reasonless entry, an empty suite map, or an entry for
1324
+ an unknown, excluded or self-skipping suite, or for `testing_backdoor`,
1325
+ throws at collection; an entry
1326
+ naming no test its suite collected fails the closing test
1327
+ (`check_spine_case_exclusions`, which reads the collected paths, so a
1328
+ filtered run still checks them) — the dead-name check, as the known-divergence
1329
+ ledger has.
1275
1330
  - **`overrides`** — `SuiteOverrides`, per suite a `Partial` of its own options
1276
1331
  (`SuiteOptionsMap`), shallow-merged over the derived ones: a role list, an
1277
1332
  `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
1333
+ cases (`session_cap` and `testing_backdoor` gate no case on `capabilities.ws`,
1334
+ so `ws` is their only socket switch), `extra_methods` to fire a consumer's own
1335
+ daemon-token seams through `testing_backdoor` (whose
1336
+ `capabilities.testing_backdoors` an override can't change — the call throws
1337
+ unless the merge keeps the backend's list). An entry can instead be a function of the derived
1280
1338
  options returning the patch (`SuiteOverride`), for a change built on a
1281
1339
  derived value — an `xfail` added to the derived multi-actor setup — so the
1282
1340
  seeding isn't restated. After the merge, every option the suite's type
@@ -1431,18 +1489,19 @@ capabilities}` protocol every Tier 1 suite uses, so **one case array runs
1431
1489
  always-available principals to fixture accessors; `role_holder` /
1432
1490
  `wrong_role` read a seeded `extra_accounts` username named via
1433
1491
  `options.principals`. `send_conformance_case(c, {fixture, surface_source, principals?,
1434
- resolved_rpc_endpoints})` (exported) is the send half — principal resolved,
1492
+ resolved_rpc_endpoints, request_headers?})` (exported) is the send half — principal resolved,
1435
1493
  request sent, the raw `{target, status, headers, body}` back unasserted; the
1436
1494
  table asserts on it, and the cross-backend differential compares two.
1495
+ `request_headers` go on the HTTP request beneath the principal's (the
1496
+ differential's identity encoding); the WS leg sends none.
1437
1497
  **WS leg:** `ws: WsEndpointTarget` (`{base_url, ws_path, origin?}`) on
1438
1498
  either sends each case as one frame on an admitted socket the principal
1439
1499
  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
1500
+ suppresses it, its own actor as `?acting=`. Both spines resolve an actor at
1501
+ the upgrade, refusing a multi-actor account's upgrade without `?acting=`
1502
+ (400 `actor_required`), and per-frame dispatch resolves each frame's
1503
+ `acting` on both. The leg always sends `?acting=`, so it does not cover that
1504
+ upgrade refusal; the front-door table's multi-actor row does. The frame is the envelope the HTTP
1446
1505
  body is (`perform_action_result_to_envelope` on TS), so the response reads
1447
1506
  `{status, headers: {}, body: frame}` with `status` the code's
1448
1507
  `jsonrpc_error_code_to_http_status` (200 for a result) and every row holds
@@ -1827,12 +1886,14 @@ never sees them). Both parse every success response against the verb's Zod
1827
1886
  **output** schema, so a TS↔Rust envelope drift fails the suite — not just a
1828
1887
  payload-field drift. Call-site primitives (`cross_rpc_call` / `error_reason` /
1829
1888
  `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,
1889
+ stateful cross suite (`RpcResult` carries the HTTP `status` beside the
1890
+ envelope; a reply that is no JSON object throws naming the method,
1831
1891
  the HTTP status and the body's head), and every
1832
1892
  suite that reads audit rows goes through `cross_backend/audit_cross_helpers.ts`
1833
1893
  — `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
1894
+ (`cross_drain_effects`, the `_testing_drain_effects` barrier, refusing an answer
1895
+ without `tracked: true`) before it lists and parses, so no reader skips the
1896
+ barrier and every read checks the barrier is live, and parses every row's metadata
1836
1897
  (`assert_audit_event_metadata`, closed to undeclared keys when the fixture's
1837
1898
  setup asked), so every reader holds the rows it reads to their event's
1838
1899
  schema. A suite pinning a row's values uses `assert_audit_metadata_fields(fixture,
@@ -1851,9 +1912,9 @@ an audit row, an admin account listing, and an offer's `created_at` /
1851
1912
  imperative cross suite aliases: `CrossSuiteOptions` (`setup_test` alone),
1852
1913
  `CapabilityGatedCrossSuiteOptions` (+ `capabilities` — `ready`),
1853
1914
  `RpcPathCrossSuiteOptions` (+ `rpc_path?` — origin, body-size (which adds `base_url`), actor
1854
- lookup, app settings, testing backdoor), and the intersection
1915
+ lookup, app settings), and the intersection
1855
1916
  `RpcPathCapabilityGatedCrossSuiteOptions` (every `cell_*` suite, query-shape,
1856
- token-scope surface, fact-serving, account lifecycle). A suite takes the
1917
+ token-scope surface, fact-serving, account lifecycle, testing backdoor). A suite takes the
1857
1918
  capability-carrying variant only when it actually gates cases on a flag, so
1858
1919
  the option type states whether the bundle is read.
1859
1920
 
@@ -1868,7 +1929,15 @@ the option type states whether the bundle is read.
1868
1929
  `cell_list` pages honoring `limit` + `offset`, a page holding visible rows
1869
1930
  only and filling to `limit` past a newer run of rows the caller can't see —
1870
1931
  two owners' private cells, read by a non-owner and by anon — and an admin's
1871
- pages tiling every row).
1932
+ pages tiling every row; a `data` that is no `CellData` — a non-object, or a
1933
+ non-string `label` / `summary` — refused 400 `invalid_params` with the
1934
+ generic message and no reason (no `data` beyond a TS spine's DEV-only
1935
+ `issues`, an array of Zod issues) on `cell_create`, on `cell_update` of a
1936
+ missing and of a hidden cell, and, with `cell_relations`, on `cell_clone`'s
1937
+ `with_data_patch` — ahead of the 404 mask the same hidden-cell call answers
1938
+ when well-typed, with the keeper's cell count unchanged; those cases run as
1939
+ the keeper and plain accounts only, so a backend that refuses a non-admin
1940
+ root cell can keep them through `exclude_cases` while excluding the rest).
1872
1941
  - **`describe_cell_relations_cross_tests`** (gates on
1873
1942
  `capabilities.cell_relations`) — the verbs beyond CRUD: grant lifecycle
1874
1943
  (actor-shaped editor grant enables edit, manage-tier `cell_grant_list`,
@@ -1893,15 +1962,23 @@ the option type states whether the bundle is read.
1893
1962
  admin-only (a non-admin → **403** `cell_create_forbidden`, and the same for a
1894
1963
  non-admin `cell_clone` of a viewable space — clone runs the authorizer for
1895
1964
  every cell it writes); a **contribution**
1896
- under a space is gated by the root's `data.policy[kind] = {min_role,
1897
- moderation_required}`, with `moderation` set per the verdict (`pending`+private
1898
- vs `approved`+author-visibility); a **hidden** parent **404**-masks vs a
1899
- **visible** one **403**s; plain parentless creates stay open. The moderate
1900
- suite then proves the `pending → approved | rejected` transition: an admin
1901
- (root manager) approves → public, the **author** self-approve → 403
1902
- (anti-self-approval), a non-viewer → 404, reject → stays private, and a cell
1965
+ under a space is gated by the root's `policy[kind] = {min_role,
1966
+ moderation_required}` (the root's manage-tier `policy` column), with
1967
+ `moderation` set per the verdict (`pending`+private vs
1968
+ `approved`+author-visibility); a **hidden** parent **404**-masks vs a
1969
+ **visible** one **403**s; plain parentless creates stay open. The root policy
1970
+ itself: `cell_policy_set` writes it (audited old and new), answers an
1971
+ editor-grant holder 403 `cell_policy_manage_only` and a non-viewer the 404,
1972
+ refuses a contribution `cell_policy_root_only`, and runs each policy through
1973
+ the authorizer's `validate_policy` (`cell_policy_invalid`); an editor
1974
+ rewriting the root's `data.policy` leaves moderation as the column says. The moderate
1975
+ suite then proves the `pending → approved | rejected` transition and the
1976
+ `approved → rejected` takedown: an admin (root manager) approves → public,
1977
+ the **author** self-approve → 403 (anti-self-approval), a non-viewer → 404, reject → stays private, and a cell
1903
1978
  with no governing root → 400 `cell_not_a_contribution` for any viewer, the
1904
- 404 mask first for a non-viewer. It also
1979
+ 404 mask first for a non-viewer; approval needs a pending post and rejection
1980
+ a pending or approved one (the takedown), else 400 `cell_not_pending` — so a
1981
+ root owner shown a private born-approved contribution can't publish it. It also
1905
1982
  pins the moderation read gate (a pending or rejected post reaches no
1906
1983
  non-steward reader past its author and admins on `cell_get`, `cell_list`,
1907
1984
  `cell_item_list`, and fact serving, even flipped public or granted while
@@ -2087,22 +2164,25 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
2087
2164
  `create_testing_actions(deps, options)` factory returning the
2088
2165
  `_testing_reset` RPC action. Test binaries register it on their RPC
2089
2166
  endpoint; `default_cross_process_setup` fires it unconditionally per
2090
- test. Handler DELETEs every auth-namespace row (no keeper-preserve
2091
- filter), resets `app_settings` to production defaults, flips
2092
- `bootstrap_lock.bootstrapped = true`, inline-seeds a fresh keeper via
2093
- `create_test_account_with_credentials` (same primitive in-process uses,
2094
- keeping write semantics in parity), seeds any `extra_accounts` at the
2095
- same bootstrap-equivalent step (the only path for roles like
2096
- `ROLE_KEEPER` whose `grant_paths` is bootstrap-only), queues the refresh
2167
+ test. Handler first fires the consumer-supplied `reset_state(db)`
2168
+ callback for domain-state reset — before the auth wipe, because domain
2169
+ rows depend on auth rows (a domain table with a restricting FK to `actor`
2170
+ must empty before the actor delete) — then DELETEs every auth-namespace
2171
+ row (no keeper-preserve filter), resets `app_settings` to production
2172
+ defaults, flips `bootstrap_lock.bootstrapped = true`, inline-seeds a fresh
2173
+ keeper via `create_test_account_with_credentials` (same primitive
2174
+ in-process uses, keeping write semantics in parity), queues the refresh
2097
2175
  of `DaemonTokenState.keeper_account_id` to the new row as a post-commit
2098
- effect (so a reset that rolls back — a throwing `reset_state`, a failed
2099
- commit — leaves the cache on the keeper still in the database, and the
2100
- next reset can still authenticate), then fires the
2101
- consumer-supplied `reset_state(db)` callback for domain-state reset —
2102
- passed the **transactional** `Db` the auth wipe ran on, so DB-domain
2103
- consumers (e.g. fuz_forge truncating its cell / fact / file tables) reset
2104
- on the same connection rather than a separately-pooled one that would
2105
- deadlock against this open transaction under the in-memory drivers. Auth
2176
+ effect as soon as the keeper exists (so a reset that rolls back — a
2177
+ failing extra seed, a failed commit — leaves the cache on the keeper
2178
+ still in the database, and the next reset can still authenticate), then
2179
+ seeds any `extra_accounts` at the same bootstrap-equivalent step (the
2180
+ only path for roles like `ROLE_KEEPER` whose `grant_paths` is
2181
+ bootstrap-only). `reset_state` is passed the **transactional** `Db` the
2182
+ auth wipe then runs on, so DB-domain consumers (e.g. fuz_forge truncating
2183
+ its cell / fact / file tables) reset on the same connection rather than a
2184
+ separately-pooled one that would deadlock against this open transaction
2185
+ under the in-memory drivers. Auth
2106
2186
  gates on `credential_types: ['daemon_token']` — effectively keeper-only
2107
2187
  without forcing the `actor: 'required'` ⟺ `acting?: ActingActor`
2108
2188
  biconditional. No free-form runtime grant action exists — see the
@@ -2117,6 +2197,10 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
2117
2197
  emitter must be built with `create_audit_emitter({track_inflight: true})`
2118
2198
  (the spine binary's `audit_factory` does — pinned by
2119
2199
  `cross_backend/testing_spine_server.audit.test.ts`), or the drain resolves at once.
2200
+ The action answers `{ok: true, tracked}`, `tracked` read from
2201
+ `audit.is_tracking_inflight()`, and `cross_drain_effects` refuses anything but
2202
+ `tracked: true` — so a binary built over an untracked emitter fails its first
2203
+ audit read rather than racing it. The Rust twin answers the same shape.
2120
2204
  The barrier is needed whatever the transport: HTTP on a binary running
2121
2205
  `await_pending_effects: true` settles a mutation's writes before
2122
2206
  responding, but WS replies before its effects flush, so a WS mutation's
@@ -2207,10 +2291,11 @@ cases are `src/test/auth/cell_ref_admission{,.fact_less}.db.test.ts`.
2207
2291
  ### Malformed-envelope and invalid-params parity — `cross_backend/rpc_adversarial.ts`
2208
2292
 
2209
2293
  `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
2294
+ live_rpc_endpoints?, skip_methods?, input_overrides?, xfail_params?})` — the
2295
+ envelope and params groups of `describe_rpc_attack_surface_tests`, sent over
2296
+ `fixture.transport` to a spawned backend. The in-process groups run on stub
2297
+ apps, while the Rust spine parses envelopes and params by hand and the
2298
+ action-manifest parity gate
2214
2299
  compares no param schemas, so this is what holds the spines to one answer.
2215
2300
 
2216
2301
  - **envelopes** — every `create_rpc_envelope_cases` case, one test each, on a
@@ -2218,8 +2303,12 @@ compares no param schemas, so this is what holds the spines to one answer.
2218
2303
  guard test that the endpoint has both a read and an authenticated mutation to
2219
2304
  carry them.
2220
2305
  - **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`
2306
+ `generate_rpc_params_test_cases` case (seeded from the method's
2307
+ `input_overrides` entry when it has one — the cases go as a single-actor
2308
+ principal, so an override omits `acting`) and failing with the list of
2309
+ cases not answered 400 `invalid_params` with the broken field's declared
2310
+ `invalid_reason` as `data.reason` (`expected_invalid_reason`; none for an
2311
+ undeclared field or a whole-body case). `xfail_params`
2223
2312
  (`RpcParamsXfail` — `{method, labels, tracking_id, reason}`) moves the named
2224
2313
  cases of a method into one `xfail_until` test each, for a known divergence on
2225
2314
  one backend; the method's other cases keep their test, and an entry naming no
@@ -2313,10 +2402,9 @@ one is still asserted. The table
2313
2402
  is the exported `transport_composition_cases` data. `multi_actor:
2314
2403
  {setup_test, xfail?}` adds `transport_composition_multi_actor_cases`, run on
2315
2404
  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
2405
+ no `?acting=` → 400 `actor_required` (flat body), the upgrade's actor
2406
+ resolution on both spines. `xfail` is for a backend known to admit the
2407
+ socket; fuz_app passes none. Given `base_url`, every
2320
2408
  probe is written to the socket (`raw_http_exchange`) and an over-cap request
2321
2409
  sends its head alone; with `base_url: null` the probes go through the
2322
2410
  fixture's transport and the WS rows skip. Not in the standard bundle. fuz_app's
@@ -2496,12 +2584,33 @@ payload reproduces it byte for byte and it authenticates, and a leading-zero
2496
2584
  `0N` authenticates (the `u64` bound is on the value, not the digit count).
2497
2585
  Ungated; cross-process only.
2498
2586
 
2499
- ### Testing-backdoor credential gate — `cross_backend/testing_backdoor.ts`
2500
-
2501
- `describe_testing_backdoor_cross_tests({setup_test, rpc_path?, ws?})` —
2502
- the negative-credential parity suite for the `_testing_*` backdoor actions.
2503
- For each `_testing_*` action (the privileged writes, the audit barrier, the
2504
- connection flood, and the introspection reads) it fires three principals over real HTTP: **anonymous** →
2587
+ ### Testing-backdoor declaration + credential gate — `cross_backend/testing_backdoor.ts`
2588
+
2589
+ `describe_testing_backdoor_cross_tests({setup_test, capabilities, rpc_path?,
2590
+ ws?, extra_methods?})` — the backend's backdoor declaration, then the
2591
+ negative-credential parity suite for the `_testing_*` backdoor actions.
2592
+
2593
+ **Declaration.** `capabilities.testing_backdoors` lists the backdoors the
2594
+ backend mounts, from `TESTING_BACKDOOR_METHODS` (`spine_surface_constants.ts`,
2595
+ the one list; `spine_surface_constants.testing_backdoors.test.ts` holds it to
2596
+ the `_testing_*` actions fuz_app's spine binary mounts). Each known backdoor
2597
+ the list leaves out is sent anonymously and must answer `method_not_found`
2598
+ (404): both spines look the method up before they authenticate (the TS
2599
+ dispatcher's lookup step, the Rust `perform_action`'s), so an unmounted method
2600
+ answers that whatever the credential, and a mounted one a 401. Each declared
2601
+ one gets the rows below, whose anonymous row fails as "declared but not
2602
+ mounted" on a `method_not_found`. So the declaration can't lie in either
2603
+ direction, and the suites gated on it skip only what the backend lacks. A
2604
+ declared name outside the known list, or a duplicate, throws at collection.
2605
+ The suite runs on every backend, one declaring only the required backdoors
2606
+ included — `describe_spine_cross_suites` refuses to exclude it, skip any of
2607
+ its cases, or hand it other `capabilities` through `overrides`.
2608
+
2609
+ **Credential gate.** For each declared backdoor (the privileged writes, the
2610
+ audit barrier, the connection flood, and the introspection reads), and each
2611
+ `extra_methods` entry (`TestingBackdoorProbe` `{method, params?}` — a
2612
+ consumer's own daemon-token seam, which must start with `_testing_` and not be
2613
+ a known backdoor), it fires three principals over real HTTP: **anonymous** →
2505
2614
  401 `unauthenticated`, **session** → 403 `forbidden` `credential_type_required`,
2506
2615
  **bearer** → 403 `forbidden` `credential_type_required` — and, when `ws`
2507
2616
  (`{base_url, ws_path, origin?}`) is passed, sends each on a socket opened with
@@ -2512,18 +2621,23 @@ on the real dispatcher (the spec-derived `describe_rpc_attack_surface_tests`
2512
2621
  never enumerates them because they're off the declared surface). Each method is
2513
2622
  sent with **valid** params so each case varies only the credential (validation
2514
2623
  runs after the gates — order is 401 → authz → 403 → 429 → 400); the handler never
2515
- runs. Cited property: `docs/security.md`
2516
- §Test Backdoor Actions. Cross-process only (the `_testing_*` actions are
2517
- mounted on the spawned binary, not the in-process app — like the ws/sse
2518
- suites); ungated, since every cross backend that uses
2519
- `default_cross_process_setup` mounts them. The complement is the
2624
+ runs. The known backdoors' params are a `Record<TestingBackdoorMethod, …>`, so a
2625
+ backdoor added to the list fails typecheck until it has them. Cited property:
2626
+ `docs/security.md` §Test Backdoor Actions. The credential rows run
2627
+ cross-process only (the `_testing_*` actions are mounted on the spawned binary,
2628
+ not the in-process app — like the ws/sse suites); in-process, an app mounting
2629
+ none declares none and gets the declaration rows alone, one per known backdoor
2630
+ — fuz_app's in-process leg is `cross_backend/testing_backdoor_parity.db.test.ts`,
2631
+ while its cross legs declare every backdoor, so they run no declaration row.
2632
+ The complement is the
2520
2633
  in-process spec-level gate test (`src/test/testing/testing_actions_auth.test.ts`,
2521
2634
  asserting each spec declares `credential_types: ['daemon_token']`) + the
2522
2635
  `assert_no_testing_methods` surface invariant. fuz_app's conformance table
2523
2636
  also covers the same gates, through the auth rows it generates over the live
2524
2637
  mount (`live_rpc_endpoints`), which the differential compares across spines;
2525
2638
  this suite stays for consumers whose backends mount the `_testing_*` actions
2526
- without a conformance table over their live mount.
2639
+ without a conformance table over their live mount, and for the declaration
2640
+ check.
2527
2641
 
2528
2642
  ### Identity-primitive parity — `cross_backend/identity_parity.ts`
2529
2643
 
@@ -18,6 +18,12 @@ import { ERROR_INVALID_REQUEST_BODY, ERROR_INVALID_JSON_BODY } from '../http/err
18
18
  export interface InputTestCase {
19
19
  label: string;
20
20
  body: unknown;
21
+ /**
22
+ * The one top-level field the case breaks; absent on a whole-body case (a
23
+ * non-object body, an extra unknown key). An RPC refusal of the case owes
24
+ * the field's declared `invalid_reason` (`expected_invalid_reason`).
25
+ */
26
+ field?: string;
21
27
  expected_error: typeof ERROR_INVALID_REQUEST_BODY | typeof ERROR_INVALID_JSON_BODY;
22
28
  }
23
29
  /** One adversarial params set for a route's params schema. */
@@ -30,6 +36,19 @@ export interface QueryTestCase {
30
36
  label: string;
31
37
  query: Record<string, string>;
32
38
  }
39
+ /** Options for `generate_input_test_cases`. */
40
+ export interface InputTestCaseOptions {
41
+ /**
42
+ * A valid body to build the cases from, in place of the generated one —
43
+ * for an input whose valid shape the generator can't reach, or one whose
44
+ * generated body the handler can't run. Each case is this body with one
45
+ * field broken, so it must pass the input schema itself: one that doesn't
46
+ * throws.
47
+ */
48
+ base?: Record<string, unknown>;
49
+ /** Names the input in error messages — the RPC method or route it belongs to. */
50
+ label?: string;
51
+ }
33
52
  /**
34
53
  * Generate adversarial test cases for a route's input schema.
35
54
  *
@@ -39,26 +58,67 @@ export interface QueryTestCase {
39
58
  * - One wrong-type value per field
40
59
  * - Null for required non-nullable fields
41
60
  * - One format violation per constrained field
61
+ * - String length (empty, one under `minLength`, one over `maxLength`), numeric
62
+ * (out of range, non-integer, off a `multipleOf` grid) and array-length
63
+ * (empty, one under `minItems`, one over `maxItems`) boundaries
64
+ *
65
+ * Format and boundaries come from the field's input-side JSON Schema
66
+ * (`to_field_json_schema`), so a field carrying a `.transform()` or pipe and a
67
+ * `.nullable()` / `.nullish()` field get them too. A bound only a `.refine()`
68
+ * enforces generates nothing — JSON Schema can't express it.
69
+ *
70
+ * Each per-field case is single-fault: valid except for the field it breaks.
71
+ * It changes one field of a valid base body — `options.base` when given, else
72
+ * one built by `generate_valid_object` — or, for a field that base omits, of
73
+ * the valid body `generate_valid_object_with_field` builds around it, so a case
74
+ * can't also break an object-level rule (adding `scope_kind` without its
75
+ * paired `scope_id`, or `slug` beside an exactly-one-of `id`). A field no valid
76
+ * body carries keeps its wrong-type and null cases on the base and gets no
77
+ * format or boundary cases, which would test no bound. A per-field case is
78
+ * emitted only when the field refuses its value, so no case is accidentally
79
+ * valid.
42
80
  *
43
- * @throws Error if the seed body built by `generate_valid_value` fails
44
- * `input_schema.safeParse` — surfaces broken generation logic with the
45
- * Zod issues path rather than producing nonsense adversarial cases.
81
+ * @param input_schema - the input schema to attack
82
+ * @param options - a base body to seed the cases, and a label for errors
83
+ * @throws Error if the base body fails `input_schema.safeParse` — a seeded
84
+ * base the schema refuses, or a generated one that surfaces broken
85
+ * generation logic — naming the issues rather than producing nonsense
86
+ * adversarial cases.
46
87
  */
47
- export declare const generate_input_test_cases: (input_schema: z.ZodType) => Array<InputTestCase>;
88
+ export declare const generate_input_test_cases: (input_schema: z.ZodType, options?: InputTestCaseOptions) => Array<InputTestCase>;
89
+ /**
90
+ * The `error.data.reason` an RPC refusal of `test_case` owes: the declared
91
+ * `invalid_reason` of the field it breaks, resolved as the dispatcher resolves
92
+ * a failure on that field (`resolve_invalid_reason`), or `undefined` — no
93
+ * reason — for an undeclared field or a whole-body case.
94
+ *
95
+ * @param input_schema - the input schema the case was generated from
96
+ * @param test_case - a case `generate_input_test_cases` produced from it
97
+ * @returns the owed reason, or `undefined` when the refusal carries none
98
+ */
99
+ export declare const expected_invalid_reason: (input_schema: z.ZodType, test_case: InputTestCase) => string | undefined;
48
100
  /**
49
101
  * Generate adversarial test cases for a route's params schema.
50
102
  *
51
103
  * Params are always strings from URL segments. Only generates cases for
52
104
  * format-constrained fields (uuid, pattern) since unconstrained string
53
- * params accept any string value.
105
+ * params accept any string value, and only a violation the field refuses.
106
+ * Each case breaks one field of a valid params record.
107
+ *
108
+ * @throws Error if no valid params record can be generated
54
109
  */
55
110
  export declare const generate_params_test_cases: (params_schema: z.ZodObject) => Array<ParamsTestCase>;
56
111
  /**
57
112
  * Generate adversarial test cases for a route's query schema.
58
113
  *
59
114
  * Query params are always strings from the URL. Generates cases for:
60
- * - Missing required fields
61
- * - Format violations on constrained fields (uuid, pattern)
115
+ * - Missing required fields the field refuses to go without (a preprocessed
116
+ * field with a default accepts its absence)
117
+ * - Format violations on constrained fields (uuid, pattern) the field refuses
118
+ *
119
+ * Each case breaks one field of a valid query record.
120
+ *
121
+ * @throws Error if no valid query record can be generated
62
122
  */
63
123
  export declare const generate_query_test_cases: (query_schema: z.ZodObject) => Array<QueryTestCase>;
64
124
  //# sourceMappingURL=adversarial_input_cases.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adversarial_input_cases.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/adversarial_input_cases.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AA8C/F,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,cAAc,EAAE,OAAO,0BAA0B,GAAG,OAAO,uBAAuB,CAAC;CACnF;AAED,8DAA8D;AAC9D,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,wDAAwD;AACxD,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAID;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,KAAK,CAAC,aAAa,CAsMtF,CAAC;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GAAI,eAAe,CAAC,CAAC,SAAS,KAAG,KAAK,CAAC,cAAc,CA+B3F,CAAC;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GAAI,cAAc,CAAC,CAAC,SAAS,KAAG,KAAK,CAAC,aAAa,CA4CxF,CAAC"}
1
+ {"version":3,"file":"adversarial_input_cases.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/adversarial_input_cases.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAmE/F,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,OAAO,0BAA0B,GAAG,OAAO,uBAAuB,CAAC;CACnF;AAED,8DAA8D;AAC9D,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,wDAAwD;AACxD,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAID,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACpC;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,yBAAyB,GACrC,cAAc,CAAC,CAAC,OAAO,EACvB,UAAU,oBAAoB,KAC5B,KAAK,CAAC,aAAa,CAmOrB,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,GACnC,cAAc,CAAC,CAAC,OAAO,EACvB,WAAW,aAAa,KACtB,MAAM,GAAG,SAG+C,CAAC;AAkC5D;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B,GAAI,eAAe,CAAC,CAAC,SAAS,KAAG,KAAK,CAAC,cAAc,CAoB3F,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yBAAyB,GAAI,cAAc,CAAC,CAAC,SAAS,KAAG,KAAK,CAAC,aAAa,CAkCxF,CAAC"}