@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
@@ -14,7 +14,7 @@ import '../assert_dev_env.js';
14
14
  * `output` schema from the live spec registry (RPC) or `RouteSpec` (the 6
15
15
  * REST auth routes) — the row never carries a schema. The principal the
16
16
  * row runs `as` resolves to a `TestFixture` accessor via
17
- * `resolve_principal` — no inline credential minting.
17
+ * `resolve_conformance_principal` — no inline credential minting.
18
18
  *
19
19
  * Every response also passes an always-on **no-fingerprint** invariant
20
20
  * (`assert_no_fingerprint_headers` over `FINGERPRINT_HEADERS` — `Server` /
@@ -22,16 +22,78 @@ import '../assert_dev_env.js';
22
22
  * a row may pin further header expectations via `expect.headers`. Headers are
23
23
  * deliberately kept out of the equivalence-group `{status, body}` comparison.
24
24
  *
25
+ * Given `ws`, the runner sends each case as a frame on a WebSocket instead:
26
+ * the principal opens an admitted socket and the frame meets the per-message
27
+ * gates. The frame carries the same JSON-RPC envelope the HTTP body does, so
28
+ * the same rows hold, read through the status the HTTP binding maps the
29
+ * frame's error code to; the cases a socket can't carry are left out
30
+ * (`is_ws_conformance_case`).
31
+ *
32
+ * A case binding `request.acting` gets `params.acting` filled with a real
33
+ * actor id at run time (`ConformanceActingBinding`), so rows can act as a
34
+ * named actor of a multi-actor keeper without knowing its id.
35
+ *
36
+ * A case's RPC method resolves from the declared `rpc_endpoints` first, then
37
+ * from `live_rpc_endpoints` when given — the methods a backend live-mounts
38
+ * off the declared surface (cell verbs, opt-in resolvers, `_testing_*`
39
+ * backdoors), so rows can reach them too.
40
+ *
25
41
  * @module
26
42
  */
27
43
  import { assert, describe, test } from 'vitest';
28
44
  import { DAEMON_TOKEN_HEADER } from '../../auth/daemon_token.js';
29
45
  import { find_auth_route, rest_auth_route_suffixes } from '../integration_helpers.js';
30
- import { find_rpc_action, headers_to_record, rpc_call, resolve_rpc_endpoints_for_setup } from '../rpc_helpers.js';
46
+ import { find_rpc_action, headers_to_record, parse_rpc_call_result, rpc_call_raw, resolve_rpc_endpoints_for_setup } from '../rpc_helpers.js';
31
47
  import { account_token_create_action_spec } from '../../auth/account_action_specs.js';
48
+ import { jsonrpc_error_code_to_http_status } from '../../http/jsonrpc_errors.js';
49
+ import { create_admitted_ws_transport } from '../transports/ws_transport.js';
50
+ import { is_response_for } from '../transports/ws_client.js';
32
51
  import { SCOPED_TOKEN_ADMITTED_METHOD } from './conformance_case.js';
33
52
  import { mint_narrowed_api_token } from './narrowed_api_token.js';
34
53
  import { xfail_until } from './xfail.js';
54
+ /**
55
+ * Resolve the endpoints a conformance case's RPC method is looked up in: the
56
+ * declared `rpc_endpoints`, then `live_rpc_endpoints`. `find_rpc_action`
57
+ * takes the first match, so a declared method resolves from the declared
58
+ * surface even when the live mount repeats it.
59
+ *
60
+ * @param options - the declared and live endpoints, and the session options
61
+ * @returns the endpoints in lookup order
62
+ */
63
+ export const resolve_conformance_rpc_endpoints = (options) => [
64
+ ...resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options),
65
+ ...(options.live_rpc_endpoints
66
+ ? resolve_rpc_endpoints_for_setup(options.live_rpc_endpoints, options.session_options)
67
+ : [])
68
+ ];
69
+ /**
70
+ * Principals that cannot hold a socket: each presents no usable credential
71
+ * (`anonymous`, the browser-discarded and malformed bearers, the expired
72
+ * session) or one the upgrade refuses (`scoped_token` — a narrowed token is
73
+ * refused `surface:ws_upgrade`), so the upgrade answers before any frame.
74
+ * The front-door table (`describe_transport_composition_cross_tests`) pins
75
+ * those upgrade refusals.
76
+ */
77
+ const WS_UNAVAILABLE_PRINCIPALS = new Set([
78
+ 'anonymous',
79
+ 'bearer_browser',
80
+ 'malformed_bearer',
81
+ 'expired_session',
82
+ 'scoped_token'
83
+ ]);
84
+ /**
85
+ * Whether a case can run over a WebSocket: an RPC method (not a REST auth
86
+ * route), sent as `POST` (a `GET` row pins the HTTP query path), pinning no
87
+ * header value (a frame has no headers; an absence pin holds vacuously), as
88
+ * a principal that can hold a socket.
89
+ *
90
+ * @param c - the case
91
+ * @returns whether the WS leg runs it
92
+ */
93
+ export const is_ws_conformance_case = (c) => !c.request.method.startsWith('/') &&
94
+ (c.request.verb ?? 'POST') === 'POST' &&
95
+ !Object.values(c.expect.headers ?? {}).some((value) => value !== null) &&
96
+ !WS_UNAVAILABLE_PRINCIPALS.has(c.request.as);
35
97
  /**
36
98
  * A deliberately malformed `X-Daemon-Token` value for the `invalid_daemon`
37
99
  * principal. Fails the `DaemonToken` Zod schema (not 43 base64url chars), so
@@ -78,20 +140,33 @@ const mint_scoped_token = async (fixture, resolved_rpc_endpoints) => {
78
140
  * `role_holder` / `wrong_role` read a seeded `extra_accounts` entry named
79
141
  * via `options.principals` (throws a clear setup error when unconfigured);
80
142
  * `scoped_token` mints its narrowed credential over the production path.
143
+ * Also used outside the table by suites that send a principal's requests
144
+ * themselves (`describe_rpc_adversarial_cross_tests`).
145
+ *
146
+ * @param fixture - the per-test fixture
147
+ * @param as - the principal
148
+ * @param principals - usernames for `role_holder` / `wrong_role`
149
+ * @param resolved_rpc_endpoints - the endpoints `scoped_token` mints through
150
+ * @returns the transport and headers the principal sends with
81
151
  */
82
- const resolve_principal = async (fixture, as, principals, resolved_rpc_endpoints) => {
152
+ export const resolve_conformance_principal = async (fixture, as, principals, resolved_rpc_endpoints = []) => {
83
153
  switch (as) {
84
154
  case 'keeper':
85
155
  // Keeper carries its session cookie via `create_session_headers`
86
156
  // (in-process the transport is stateless; cross-process the jar
87
157
  // also holds it — the explicit header is the same value).
88
- return { transport: fixture.transport, headers: fixture.create_session_headers() };
158
+ return {
159
+ transport: fixture.transport,
160
+ headers: fixture.create_session_headers(),
161
+ actor_id: fixture.actor.id
162
+ };
89
163
  case 'daemon':
90
164
  // Daemon-token is a non-browser credential — empty jar + no Origin.
91
165
  return {
92
166
  transport: fixture.fresh_transport({ origin: null }),
93
167
  headers: fixture.create_daemon_token_headers(),
94
- suppress_default_origin: true
168
+ suppress_default_origin: true,
169
+ actor_id: fixture.actor.id
95
170
  };
96
171
  case 'invalid_daemon':
97
172
  // A malformed `X-Daemon-Token` carried ALONGSIDE the keeper's session
@@ -109,7 +184,8 @@ const resolve_principal = async (fixture, as, principals, resolved_rpc_endpoints
109
184
  headers: fixture.create_session_headers({
110
185
  [DAEMON_TOKEN_HEADER]: INVALID_DAEMON_TOKEN_VALUE
111
186
  }),
112
- suppress_default_origin: true
187
+ suppress_default_origin: true,
188
+ actor_id: fixture.actor.id
113
189
  };
114
190
  case 'daemon_browser':
115
191
  // A VALID `X-Daemon-Token` carried in a browser context (default Origin
@@ -124,14 +200,16 @@ const resolve_principal = async (fixture, as, principals, resolved_rpc_endpoints
124
200
  // guard), the 403 here proves the valid token was discarded, not honored.
125
201
  return {
126
202
  transport: fixture.transport,
127
- headers: fixture.create_session_headers(fixture.create_daemon_token_headers())
203
+ headers: fixture.create_session_headers(fixture.create_daemon_token_headers()),
204
+ actor_id: fixture.actor.id
128
205
  };
129
206
  case 'token':
130
207
  // Bearer is discarded in a browser context — empty jar + no Origin.
131
208
  return {
132
209
  transport: fixture.fresh_transport({ origin: null }),
133
210
  headers: fixture.create_bearer_headers(),
134
- suppress_default_origin: true
211
+ suppress_default_origin: true,
212
+ actor_id: fixture.actor.id
135
213
  };
136
214
  case 'bearer_browser':
137
215
  // A VALID bearer api-token in a browser context (default Origin
@@ -168,7 +246,8 @@ const resolve_principal = async (fixture, as, principals, resolved_rpc_endpoints
168
246
  return {
169
247
  transport: fixture.fresh_transport({ origin: null }),
170
248
  headers: { authorization: `Bearer ${token}` },
171
- suppress_default_origin: true
249
+ suppress_default_origin: true,
250
+ actor_id: fixture.actor.id
172
251
  };
173
252
  }
174
253
  case 'anonymous':
@@ -176,7 +255,11 @@ const resolve_principal = async (fixture, as, principals, resolved_rpc_endpoints
176
255
  return { transport: fixture.fresh_transport(), headers: {} };
177
256
  case 'fresh_non_admin': {
178
257
  const account = await fixture.create_account();
179
- return { transport: fixture.fresh_transport(), headers: account.create_session_headers() };
258
+ return {
259
+ transport: fixture.fresh_transport(),
260
+ headers: account.create_session_headers(),
261
+ actor_id: account.actor.id
262
+ };
180
263
  }
181
264
  case 'expired_session': {
182
265
  // The keeper presented via an expired server-side session — fresh
@@ -184,7 +267,11 @@ const resolve_principal = async (fixture, as, principals, resolved_rpc_endpoints
184
267
  // minted cookie payload is valid; the backdated `auth_session` row
185
268
  // is what the DB-row expiry gate refuses.
186
269
  const cookie = await fixture.mint_expired_session();
187
- return { transport: fixture.fresh_transport(), headers: { cookie } };
270
+ return {
271
+ transport: fixture.fresh_transport(),
272
+ headers: { cookie },
273
+ actor_id: fixture.actor.id
274
+ };
188
275
  }
189
276
  case 'role_holder':
190
277
  case 'wrong_role': {
@@ -198,7 +285,11 @@ const resolve_principal = async (fixture, as, principals, resolved_rpc_endpoints
198
285
  throw new Error(`conformance: extra_accounts['${username}'] not seeded for principal '${as}' — ` +
199
286
  `declare it in the suite's extra_accounts option.`);
200
287
  }
201
- return { transport: fixture.fresh_transport(), headers: extra.create_session_headers() };
288
+ return {
289
+ transport: fixture.fresh_transport(),
290
+ headers: extra.create_session_headers(),
291
+ actor_id: extra.actor.id
292
+ };
202
293
  }
203
294
  }
204
295
  };
@@ -286,45 +377,198 @@ const assert_response_headers = (headers, c) => {
286
377
  };
287
378
  const is_success_status = (status) => status >= 200 && status < 300;
288
379
  /**
289
- * Run one conformance case end-to-end: resolve the principal, dispatch the
290
- * request, and assert the expected status / reason / fields. Returns the
291
- * normalized response so the caller can feed an equivalence group.
380
+ * Resolve a case's principal on `fixture` and send its request, asserting
381
+ * nothing about the response.
382
+ *
383
+ * The send half of the table runner, shared with the cross-backend
384
+ * differential (`describe_differential_conformance_tests`), which sends one
385
+ * case to two backends and compares what came back.
386
+ *
387
+ * @param c - the case
388
+ * @param options - the fixture, surface and endpoints to send through
389
+ * @returns the resolved target and the raw response
390
+ * @throws Error when the method is on neither the RPC surface nor the REST
391
+ * auth routes
292
392
  */
293
- const run_case = async (c, options, resolved_rpc_endpoints) => {
294
- const fixture = await options.setup_test();
295
- const { transport, headers, suppress_default_origin } = await resolve_principal(fixture, c.request.as, options.principals, resolved_rpc_endpoints);
296
- const normalized = c.request.method.startsWith('/')
297
- ? await run_rest_case(c, options, transport, headers)
298
- : await run_rpc_case(c, transport, headers, suppress_default_origin, resolved_rpc_endpoints);
299
- // Negative-space field check, uniform across REST + RPC and success + error
300
- // bodies: a sensitive column (`password_hash`, `token_hash`) must not appear
301
- // anywhere in what the prober sees.
302
- if (c.expect.absent_fields) {
303
- assert_absent_fields(normalized.body, c.expect.absent_fields, c.name);
393
+ export const send_conformance_case = async (c, options) => {
394
+ const { fixture, surface_source, principals, resolved_rpc_endpoints, ws } = options;
395
+ if (ws && !is_ws_conformance_case(c)) {
396
+ throw new Error(`conformance: case '${c.name}' cannot run over a WebSocket (is_ws_conformance_case) — ` +
397
+ `filter the cases before sending them on the WS leg.`);
398
+ }
399
+ const principal = await resolve_conformance_principal(fixture, c.request.as, principals, resolved_rpc_endpoints);
400
+ const { transport, headers, suppress_default_origin } = principal;
401
+ const params = c.request.acting
402
+ ? bind_acting(c, resolve_acting_binding(c, c.request.acting, fixture, principal))
403
+ : c.request.params;
404
+ if (c.request.method.startsWith('/')) {
405
+ const suffix = c.request.method;
406
+ if (!rest_auth_route_suffixes.includes(suffix)) {
407
+ throw new Error(`conformance: REST method '${c.request.method}' is not a known auth-route suffix ` +
408
+ `(${rest_auth_route_suffixes.join(', ')}). Use an RPC method name for RPC actions.`);
409
+ }
410
+ const verb = c.request.verb ?? 'POST';
411
+ const route = find_auth_route(surface_source.route_specs, suffix, verb);
412
+ if (!route) {
413
+ throw new Error(`conformance: no REST route spec for ${verb} ${suffix} on the surface.`);
414
+ }
415
+ const init = {
416
+ method: verb,
417
+ headers: { 'Content-Type': 'application/json', ...headers },
418
+ ...(verb !== 'GET' && params !== undefined && { body: JSON.stringify(params) })
419
+ };
420
+ const response = await transport(route.path, init);
421
+ const body = await response.json().catch(() => undefined);
422
+ return {
423
+ target: { kind: 'rest', route },
424
+ status: response.status,
425
+ headers: headers_to_record(response.headers),
426
+ body
427
+ };
304
428
  }
305
- return normalized;
306
- };
307
- /** Dispatch + assert a case targeting an RPC method. */
308
- const run_rpc_case = async (c, transport, headers, suppress_default_origin, resolved_rpc_endpoints) => {
309
429
  const found = find_rpc_action(resolved_rpc_endpoints, c.request.method);
310
430
  if (!found) {
311
431
  throw new Error(`conformance: RPC method '${c.request.method}' not found on the surface — ` +
312
- `check the method name or that the action is registered on rpc_endpoints.`);
432
+ `check the method name or that the action is registered on rpc_endpoints ` +
433
+ `(or, for a method off the declared surface, on live_rpc_endpoints).`);
434
+ }
435
+ if (ws) {
436
+ return {
437
+ target: { kind: 'rpc', ...found },
438
+ ...(await send_ws_frame(c.request.method, params, principal, ws))
439
+ };
313
440
  }
314
- const res = await rpc_call({
441
+ const raw = await rpc_call_raw({
315
442
  app: transport,
316
443
  path: found.path,
317
444
  method: c.request.method,
318
- params: c.request.params,
445
+ params,
319
446
  headers,
320
447
  ...(c.request.verb && { verb: c.request.verb }),
321
448
  ...(suppress_default_origin && { suppress_default_origin: true })
322
449
  });
323
- assert_response_headers(res.headers, c);
450
+ return { target: { kind: 'rpc', ...found }, ...raw };
451
+ };
452
+ /**
453
+ * The request id the WS leg sends under — the id the HTTP leg's
454
+ * `rpc_call_raw` sends by default, so the two envelopes read alike.
455
+ */
456
+ const CONFORMANCE_REQUEST_ID = 'test';
457
+ /**
458
+ * Open an admitted socket as the principal, send one request frame, and read
459
+ * its response frame — the WS leg of `send_conformance_case`.
460
+ *
461
+ * The upgrade carries the principal's headers (its cookie, bearer or daemon
462
+ * token) and the `Origin` the HTTP leg would send: none for a principal that
463
+ * suppresses it, the target's otherwise. It also names the principal's own
464
+ * actor as `?acting=`. Only the TS spine resolves an actor at the upgrade,
465
+ * refusing a multi-actor account's upgrade without one (400
466
+ * `actor_required`); the Rust spine's upgrade resolves the account and
467
+ * ignores `?acting=`. Per-frame dispatch resolves the actor from each frame's
468
+ * own `acting` on both. Because the leg always sends `?acting=`, it does not
469
+ * cover that upgrade-level difference — the front-door table's multi-actor
470
+ * row does. The socket closes afterwards; a
471
+ * close error is not the case's subject (a request that revokes its own
472
+ * credential closes the socket after its reply).
473
+ */
474
+ const send_ws_frame = async (method, params, principal, ws) => {
475
+ const client = await create_admitted_ws_transport({
476
+ base_url: ws.base_url,
477
+ ws_path: principal.actor_id === undefined
478
+ ? ws.ws_path
479
+ : `${ws.ws_path}?acting=${encodeURIComponent(principal.actor_id)}`,
480
+ cookies: [],
481
+ headers: principal.headers,
482
+ origin: principal.suppress_default_origin ? null : (ws.origin ?? ws.base_url)
483
+ });
484
+ try {
485
+ const frame = { jsonrpc: '2.0', id: CONFORMANCE_REQUEST_ID, method };
486
+ if (params !== undefined && params !== null)
487
+ frame.params = params;
488
+ await client.send(frame);
489
+ const body = await client.wait_for(is_response_for(CONFORMANCE_REQUEST_ID));
490
+ const code = body.error?.code;
491
+ const status = body.error === undefined
492
+ ? 200
493
+ : typeof code === 'number'
494
+ ? jsonrpc_error_code_to_http_status(code)
495
+ : 500;
496
+ return { status, headers: {}, body };
497
+ }
498
+ finally {
499
+ await client.close().catch(() => { });
500
+ }
501
+ };
502
+ /**
503
+ * The actor id a case's `acting` binding names on this fixture.
504
+ *
505
+ * @throws Error when the binding names an actor the fixture or principal lacks
506
+ */
507
+ const resolve_acting_binding = (c, binding, fixture, principal) => {
508
+ switch (binding) {
509
+ case 'principal':
510
+ if (principal.actor_id === undefined) {
511
+ throw new Error(`conformance: case '${c.name}' binds acting to its principal, but '${c.request.as}' ` +
512
+ `presents no account to take an actor from.`);
513
+ }
514
+ return principal.actor_id;
515
+ case 'extra_actor': {
516
+ const extra = fixture.extra_actors[0];
517
+ if (!extra) {
518
+ throw new Error(`conformance: case '${c.name}' binds acting to the keeper's extra actor, but the ` +
519
+ `fixture seeds none — declare extra_actors at setup.`);
520
+ }
521
+ return extra.id;
522
+ }
523
+ case 'keeper_actor':
524
+ return fixture.actor.id;
525
+ }
526
+ };
527
+ /** A case's params with `acting` set to `actor_id`. */
528
+ const bind_acting = (c, actor_id) => {
529
+ const { params } = c.request;
530
+ if (params === undefined)
531
+ return { acting: actor_id };
532
+ if (params === null || typeof params !== 'object' || Array.isArray(params)) {
533
+ throw new Error(`conformance: case '${c.name}' binds acting, so its params must be an object or omitted.`);
534
+ }
535
+ if ('acting' in params) {
536
+ throw new Error(`conformance: case '${c.name}' binds acting and also sets params.acting — pick one.`);
537
+ }
538
+ return { ...params, acting: actor_id };
539
+ };
540
+ /**
541
+ * Run one conformance case end-to-end: resolve the principal, dispatch the
542
+ * request, and assert the expected status / reason / fields. Returns the
543
+ * normalized response so the caller can feed an equivalence group.
544
+ */
545
+ const run_case = async (c, options, resolved_rpc_endpoints) => {
546
+ const sent = await send_conformance_case(c, {
547
+ fixture: await options.setup_test(),
548
+ surface_source: options.surface_source,
549
+ ...(options.principals && { principals: options.principals }),
550
+ resolved_rpc_endpoints,
551
+ ...(options.ws && { ws: options.ws })
552
+ });
553
+ assert_response_headers(sent.headers, c);
554
+ const normalized = sent.target.kind === 'rest'
555
+ ? assert_rest_case(c, sent.target.route, sent)
556
+ : assert_rpc_case(c, sent.target.action, sent);
557
+ // Negative-space field check, uniform across REST + RPC and success + error
558
+ // bodies: a sensitive column (`password_hash`, `token_hash`) must not appear
559
+ // anywhere in what the prober sees.
560
+ if (c.expect.absent_fields) {
561
+ assert_absent_fields(normalized.body, c.expect.absent_fields, c.name);
562
+ }
563
+ return normalized;
564
+ };
565
+ /** Assert a case targeting an RPC method. */
566
+ const assert_rpc_case = (c, action, sent) => {
567
+ const res = parse_rpc_call_result(sent, c.request.method);
324
568
  if (is_success_status(c.expect.status)) {
325
569
  assert.ok(res.ok, `${c.name}: expected success (${c.expect.status}) but got error ${JSON.stringify(res.ok ? undefined : res.error)}`);
326
570
  assert.strictEqual(res.status, c.expect.status, `${c.name}: status`);
327
- const parsed = found.action.spec.output.safeParse(res.result);
571
+ const parsed = action.spec.output.safeParse(res.result);
328
572
  assert.ok(parsed.success, `${c.name}: result does not match spec.output: ${JSON.stringify(parsed.success ? undefined : parsed.error.issues)}`);
329
573
  if (c.expect.fields)
330
574
  assert_fields(res.result, c.expect.fields, c.name);
@@ -347,28 +591,10 @@ const run_rpc_case = async (c, transport, headers, suppress_default_origin, reso
347
591
  assert_fields(res.error.data, c.expect.fields, c.name);
348
592
  return { status: res.status, body: res.error };
349
593
  };
350
- /** Dispatch + assert a case targeting one of the 6 REST auth routes. */
351
- const run_rest_case = async (c, options, transport, headers) => {
352
- const suffix = c.request.method;
353
- if (!rest_auth_route_suffixes.includes(suffix)) {
354
- throw new Error(`conformance: REST method '${c.request.method}' is not a known auth-route suffix ` +
355
- `(${rest_auth_route_suffixes.join(', ')}). Use an RPC method name for RPC actions.`);
356
- }
357
- const verb = c.request.verb ?? 'POST';
358
- const route = find_auth_route(options.surface_source.route_specs, suffix, verb);
359
- if (!route) {
360
- throw new Error(`conformance: no REST route spec for ${verb} ${suffix} on the surface.`);
361
- }
362
- const init = {
363
- method: verb,
364
- headers: { 'Content-Type': 'application/json', ...headers },
365
- ...(verb !== 'GET' &&
366
- c.request.params !== undefined && { body: JSON.stringify(c.request.params) })
367
- };
368
- const response = await transport(route.path, init);
369
- assert_response_headers(headers_to_record(response.headers), c);
370
- assert.strictEqual(response.status, c.expect.status, `${c.name}: status`);
371
- const body = await response.json().catch(() => undefined);
594
+ /** Assert a case targeting one of the 6 REST auth routes. */
595
+ const assert_rest_case = (c, route, sent) => {
596
+ const { status, body } = sent;
597
+ assert.strictEqual(status, c.expect.status, `${c.name}: status`);
372
598
  if (is_success_status(c.expect.status)) {
373
599
  const parsed = route.output.safeParse(body);
374
600
  assert.ok(parsed.success, `${c.name}: body does not match route output: ${JSON.stringify(parsed.success ? undefined : parsed.error.issues)}`);
@@ -379,7 +605,7 @@ const run_rest_case = async (c, options, transport, headers) => {
379
605
  }
380
606
  if (c.expect.fields)
381
607
  assert_fields(body, c.expect.fields, c.name);
382
- return { status: response.status, body };
608
+ return { status, body };
383
609
  };
384
610
  /**
385
611
  * Register a `describe` block running every `ConformanceCase` as a
@@ -388,24 +614,30 @@ const run_rest_case = async (c, options, transport, headers) => {
388
614
  * capabilities}` protocol.
389
615
  */
390
616
  export const describe_conformance_table_tests = (options) => {
391
- const resolved_rpc_endpoints = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
617
+ const resolved_rpc_endpoints = resolve_conformance_rpc_endpoints(options);
392
618
  // Per-group normalized responses, populated as the member case tests run
393
619
  // and consulted by the per-group byte-identity assertions registered after
394
620
  // the case loop — vitest runs a describe's tests in registration order, so
395
621
  // the group assertions execute last, after every member has recorded.
396
622
  const equivalence_groups = new Map();
397
- const group_names = new Set();
398
- for (const c of options.cases) {
399
- if (c.expect.equivalence_group)
400
- group_names.add(c.expect.equivalence_group);
623
+ const cases = options.ws ? options.cases.filter(is_ws_conformance_case) : options.cases;
624
+ const group_sizes = new Map();
625
+ for (const c of cases) {
626
+ const group = c.expect.equivalence_group;
627
+ if (group)
628
+ group_sizes.set(group, (group_sizes.get(group) ?? 0) + 1);
401
629
  }
630
+ // a group the WS filter left a single member of has nothing to compare;
631
+ // on HTTP every group runs, so an undersized one still fails loud
632
+ const group_names = new Set([...group_sizes].filter(([, size]) => !options.ws || size >= 2).map(([group]) => group));
402
633
  describe(options.suite_name ?? 'conformance table', () => {
403
- for (const c of options.cases) {
634
+ for (const c of cases) {
404
635
  const label = c.note ? `${c.name} — ${c.note}` : c.name;
405
636
  const group = c.expect.equivalence_group;
637
+ const recorded = group !== undefined && group_names.has(group);
406
638
  const body = async () => {
407
639
  const normalized = await run_case(c, options, resolved_rpc_endpoints);
408
- if (group) {
640
+ if (recorded && group) {
409
641
  const members = equivalence_groups.get(group) ?? [];
410
642
  members.push({ name: c.name, normalized });
411
643
  equivalence_groups.set(group, members);
@@ -1 +1 @@
1
- {"version":3,"file":"cookie_attributes.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cookie_attributes.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA4C9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,6DAA6D;AAC7D,MAAM,WAAW,gCAAgC;IAChD,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,6DAA6D;IAC7D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAoED,eAAO,MAAM,sCAAsC,GAClD,SAAS,gCAAgC,KACvC,IAmFF,CAAC"}
1
+ {"version":3,"file":"cookie_attributes.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cookie_attributes.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA4C9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,6DAA6D;AAC7D,MAAM,WAAW,gCAAgC;IAChD,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,6DAA6D;IAC7D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAoED,eAAO,MAAM,sCAAsC,GAClD,SAAS,gCAAgC,KACvC,IAmFF,CAAC"}
@@ -39,6 +39,7 @@ import '../assert_dev_env.js';
39
39
  */
40
40
  import { describe, test, assert } from 'vitest';
41
41
  import { DEFAULT_TEST_PASSWORD } from '../test_credentials.js';
42
+ import { SPINE_ACCOUNT_LOGIN_PATH, SPINE_ACCOUNT_LOGOUT_PATH } from './spine_surface_constants.js';
42
43
  /**
43
44
  * Parse a single `Set-Cookie` header value into `{name, value, attributes}`.
44
45
  * Returns `null` for a malformed head (no `=`, empty name). Unlike the
@@ -93,8 +94,8 @@ const assert_hardened_flags = (cookie, label) => {
93
94
  };
94
95
  export const describe_cookie_attributes_cross_tests = (options) => {
95
96
  const { setup_test, cookie_name } = options;
96
- const login_path = options.login_path ?? '/api/account/login';
97
- const logout_path = options.logout_path ?? '/api/account/logout';
97
+ const login_path = options.login_path ?? SPINE_ACCOUNT_LOGIN_PATH;
98
+ const logout_path = options.logout_path ?? SPINE_ACCOUNT_LOGOUT_PATH;
98
99
  // Fresh-keeper-per-test wipes the DB between tests, so a literal username
99
100
  // never collides (see `setup.ts`). Each test mints it fresh.
100
101
  const username = 'cookie_attr_user';
@@ -6,8 +6,8 @@ import '../assert_dev_env.js';
6
6
  * sets its own `test.name`, and this factory derives the backend name from
7
7
  * that project name (vitest 4 passes the `TestProject` to globalSetup), picks
8
8
  * the matching `BackendConfig`, spawns + bootstraps it via `bootstrap_backend`,
9
- * and `provide`s a serializable handle that `*.cross.test.ts` files
10
- * `inject` and rebuild with `reconstruct_bootstrapped_handle`.
9
+ * and `provide`s a serializable handle that `*.cross.test.ts` files read
10
+ * with `inject_backend_handle` (`provided_context.ts`).
11
11
  *
12
12
  * A consumer's `global_setup.ts` collapses to:
13
13
  *
@@ -15,10 +15,11 @@ import '../assert_dev_env.js';
15
15
  * import {create_cross_backend_global_setup} from
16
16
  * '@fuzdev/fuz_app/testing/cross_backend/create_cross_backend_global_setup.ts';
17
17
  * import {deno_backend_config, rust_backend_config} from './my_backend_config.js';
18
- * import './cross_test_types.js'; // augments inject('backend_handle')
19
18
  *
20
19
  * export default create_cross_backend_global_setup({
21
20
  * configs: {deno: deno_backend_config, rust: rust_backend_config},
21
+ * ensure_databases: true,
22
+ * before_spawn: {rust: () => prepare_cargo_binary(my_cargo_binary)},
22
23
  * });
23
24
  * ```
24
25
  *
@@ -31,6 +32,7 @@ import '../assert_dev_env.js';
31
32
  */
32
33
  import type { TestProject } from 'vitest/node';
33
34
  import type { BackendConfig } from './backend_config.js';
35
+ import { bootstrap_backend } from './bootstrap_backend.js';
34
36
  export interface CrossBackendGlobalSetupOptions {
35
37
  /**
36
38
  * Map of derived backend name → `BackendConfig` factory. The derived
@@ -45,13 +47,44 @@ export interface CrossBackendGlobalSetupOptions {
45
47
  readonly derive_name?: (project_name: string) => string;
46
48
  /**
47
49
  * Key passed to `project.provide` (and read by `inject` in test files).
48
- * Default `'backend_handle'`. Augment vitest's `ProvidedContext` for it.
50
+ * Default `'backend_handle'`, which `provided_context.ts` declares on vitest's
51
+ * `ProvidedContext`; augment it yourself for another key.
49
52
  */
50
53
  readonly provide_key?: string;
54
+ /**
55
+ * Create the database the selected config's `env.DATABASE_URL` names, on
56
+ * the server it names, when it is missing and the URL is `postgres:`
57
+ * (`ensure_database_for_url` — idempotent `createdb`; a URL like
58
+ * `memory://` is left alone). Off by default, so a test role without
59
+ * `CREATEDB` is never required unless the setup asks.
60
+ *
61
+ * @default false
62
+ */
63
+ readonly ensure_databases?: boolean;
64
+ /**
65
+ * Hooks run before a backend's config is built and spawned, by backend
66
+ * name — the Cargo build of a Rust binary (`prepare_cargo_binary`), so it
67
+ * runs with its output shown and outside the spawn's startup window, and
68
+ * the config can spawn the built binary. Runs only for the selected
69
+ * backend; a key naming no config throws.
70
+ */
71
+ readonly before_spawn?: Readonly<Record<string, () => unknown>>;
72
+ }
73
+ /**
74
+ * What `create_cross_backend_global_setup` creates databases and spawns
75
+ * backends with — `ensure_database_for_url` and `bootstrap_backend` by default.
76
+ */
77
+ export interface CrossBackendGlobalSetupDeps {
78
+ readonly ensure_database_for_url: (database_url: string) => unknown;
79
+ readonly bootstrap_backend: typeof bootstrap_backend;
51
80
  }
52
81
  /**
53
82
  * Build a vitest `globalSetup` default export. Returns the
54
83
  * `(project) => teardown` function vitest 4 expects.
84
+ *
85
+ * @param options - the configs, and what runs before the spawn
86
+ * @param deps - the database creator and the spawn, the shipped ones by default
87
+ * @throws Error when a `before_spawn` key names no config
55
88
  */
56
- export declare const create_cross_backend_global_setup: ({ configs, derive_name, provide_key }: CrossBackendGlobalSetupOptions) => ((project: TestProject) => Promise<() => Promise<void>>);
89
+ export declare const create_cross_backend_global_setup: ({ configs, derive_name, provide_key, ensure_databases, before_spawn }: CrossBackendGlobalSetupOptions, deps?: CrossBackendGlobalSetupDeps) => ((project: TestProject) => Promise<() => Promise<void>>);
57
90
  //# sourceMappingURL=create_cross_backend_global_setup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"create_cross_backend_global_setup.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/create_cross_backend_global_setup.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAYzD,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,aAAa,CAAC,CAAC,CAAC;IAChE;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IACxD;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;GAGG;AACH,eAAO,MAAM,iCAAiC,GAAI,uCAI/C,8BAA8B,KAAG,CAAC,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAqB1F,CAAC"}
1
+ {"version":3,"file":"create_cross_backend_global_setup.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/create_cross_backend_global_setup.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAY3D,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,aAAa,CAAC,CAAC,CAAC;IAChE;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IACxD;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;OAQG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC;CAChE;AAED;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IAC3C,QAAQ,CAAC,uBAAuB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;IACpE,QAAQ,CAAC,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;CACrD;AAOD;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,GAC7C,uEAMG,8BAA8B,EACjC,OAAM,2BAAqE,KACzE,CAAC,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAiCzD,CAAC"}