@fuzdev/fuz_app 0.147.0 → 0.149.0

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