@fuzdev/fuz_app 0.147.0 → 0.148.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/dist/actions/CLAUDE.md +6 -4
  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/perform_action.js +1 -1
  6. package/dist/auth/cell_action_specs.d.ts +3 -1
  7. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  8. package/dist/auth/cell_action_specs.js +5 -5
  9. package/dist/auth/cell_actions.d.ts.map +1 -1
  10. package/dist/auth/cell_actions.js +2 -3
  11. package/dist/auth/cell_data_schema.d.ts +1 -1
  12. package/dist/auth/cell_data_schema.js +1 -1
  13. package/dist/auth/cell_field_action_specs.d.ts +4 -3
  14. package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
  15. package/dist/auth/cell_field_action_specs.js +5 -6
  16. package/dist/auth/cell_field_actions.d.ts.map +1 -1
  17. package/dist/auth/cell_field_actions.js +8 -1
  18. package/dist/auth/cell_item_action_specs.d.ts +4 -2
  19. package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
  20. package/dist/auth/cell_item_action_specs.js +5 -5
  21. package/dist/auth/cell_item_actions.d.ts.map +1 -1
  22. package/dist/auth/cell_item_actions.js +8 -1
  23. package/dist/auth/role_grant_offer_action_specs.d.ts +2 -0
  24. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  25. package/dist/auth/role_grant_offer_action_specs.js +3 -1
  26. package/dist/primitive_schemas.d.ts +1 -1
  27. package/dist/primitive_schemas.d.ts.map +1 -1
  28. package/dist/primitive_schemas.js +2 -1
  29. package/dist/schema_meta.d.ts +20 -0
  30. package/dist/schema_meta.d.ts.map +1 -1
  31. package/dist/schema_meta.js +7 -1
  32. package/dist/testing/CLAUDE.md +720 -114
  33. package/dist/testing/admin_integration.d.ts.map +1 -1
  34. package/dist/testing/admin_integration.js +73 -5
  35. package/dist/testing/adversarial_input.d.ts +0 -47
  36. package/dist/testing/adversarial_input.d.ts.map +1 -1
  37. package/dist/testing/adversarial_input.js +4 -340
  38. package/dist/testing/adversarial_input_cases.d.ts +64 -0
  39. package/dist/testing/adversarial_input_cases.d.ts.map +1 -0
  40. package/dist/testing/adversarial_input_cases.js +348 -0
  41. package/dist/testing/audit_completeness.d.ts.map +1 -1
  42. package/dist/testing/audit_completeness.js +24 -20
  43. package/dist/testing/audit_drift_guard.d.ts +27 -1
  44. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  45. package/dist/testing/audit_drift_guard.js +50 -0
  46. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
  47. package/dist/testing/cross_backend/account_lifecycle.js +29 -12
  48. package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
  49. package/dist/testing/cross_backend/actor_lookup.js +7 -25
  50. package/dist/testing/cross_backend/app_settings.d.ts.map +1 -1
  51. package/dist/testing/cross_backend/app_settings.js +4 -6
  52. package/dist/testing/cross_backend/audit_cross_helpers.d.ts +51 -0
  53. package/dist/testing/cross_backend/audit_cross_helpers.d.ts.map +1 -0
  54. package/dist/testing/cross_backend/audit_cross_helpers.js +78 -0
  55. package/dist/testing/cross_backend/backend_config.d.ts +2 -1
  56. package/dist/testing/cross_backend/backend_config.d.ts.map +1 -1
  57. package/dist/testing/cross_backend/body_size.d.ts +6 -0
  58. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  59. package/dist/testing/cross_backend/body_size.js +26 -44
  60. package/dist/testing/cross_backend/body_size_smuggling.d.ts +5 -0
  61. package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
  62. package/dist/testing/cross_backend/body_size_smuggling.js +7 -14
  63. package/dist/testing/cross_backend/build_test_backend_paths.d.ts +3 -1
  64. package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -1
  65. package/dist/testing/cross_backend/capabilities.d.ts +55 -59
  66. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  67. package/dist/testing/cross_backend/capabilities.js +20 -23
  68. package/dist/testing/cross_backend/cargo_binary.d.ts +70 -0
  69. package/dist/testing/cross_backend/cargo_binary.d.ts.map +1 -0
  70. package/dist/testing/cross_backend/cargo_binary.js +148 -0
  71. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  72. package/dist/testing/cross_backend/cell_crud.js +57 -1
  73. package/dist/testing/cross_backend/cell_gated_create.d.ts +0 -60
  74. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  75. package/dist/testing/cross_backend/cell_gated_create.js +9 -487
  76. package/dist/testing/cross_backend/cell_grant_role.d.ts +14 -2
  77. package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
  78. package/dist/testing/cross_backend/cell_grant_role.js +22 -12
  79. package/dist/testing/cross_backend/cell_moderate.d.ts +66 -0
  80. package/dist/testing/cross_backend/cell_moderate.d.ts.map +1 -0
  81. package/dist/testing/cross_backend/cell_moderate.js +511 -0
  82. package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -1
  83. package/dist/testing/cross_backend/cell_ref_admission.js +1 -1
  84. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  85. package/dist/testing/cross_backend/cell_relations.js +40 -19
  86. package/dist/testing/cross_backend/conformance_case.d.ts +43 -4
  87. package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
  88. package/dist/testing/cross_backend/conformance_case.js +29 -1
  89. package/dist/testing/cross_backend/conformance_rpc_auth.d.ts +93 -0
  90. package/dist/testing/cross_backend/conformance_rpc_auth.d.ts.map +1 -0
  91. package/dist/testing/cross_backend/conformance_rpc_auth.js +125 -0
  92. package/dist/testing/cross_backend/conformance_table.d.ts +144 -4
  93. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  94. package/dist/testing/cross_backend/conformance_table.js +297 -65
  95. package/dist/testing/cross_backend/cookie_attributes.d.ts.map +1 -1
  96. package/dist/testing/cross_backend/cookie_attributes.js +3 -2
  97. package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts +38 -5
  98. package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts.map +1 -1
  99. package/dist/testing/cross_backend/create_cross_backend_global_setup.js +24 -3
  100. package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts +5 -5
  101. package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts.map +1 -1
  102. package/dist/testing/cross_backend/credential_deadline.d.ts +10 -6
  103. package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -1
  104. package/dist/testing/cross_backend/credential_deadline.js +32 -31
  105. package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -1
  106. package/dist/testing/cross_backend/credential_header_robustness.js +4 -6
  107. package/dist/testing/cross_backend/{cell_cross_helpers.d.ts → cross_rpc.d.ts} +12 -2
  108. package/dist/testing/cross_backend/cross_rpc.d.ts.map +1 -0
  109. package/dist/testing/cross_backend/{cell_cross_helpers.js → cross_rpc.js} +31 -7
  110. package/dist/testing/cross_backend/default_backend_configs.d.ts +56 -61
  111. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  112. package/dist/testing/cross_backend/default_backend_configs.js +97 -91
  113. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
  114. package/dist/testing/cross_backend/default_spine_surface.js +2 -2
  115. package/dist/testing/cross_backend/differential.d.ts +126 -0
  116. package/dist/testing/cross_backend/differential.d.ts.map +1 -0
  117. package/dist/testing/cross_backend/differential.js +201 -0
  118. package/dist/testing/cross_backend/differential_normalize.d.ts +150 -0
  119. package/dist/testing/cross_backend/differential_normalize.d.ts.map +1 -0
  120. package/dist/testing/cross_backend/differential_normalize.js +153 -0
  121. package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
  122. package/dist/testing/cross_backend/fact_serving.js +27 -17
  123. package/dist/testing/cross_backend/full_spine_mount.d.ts +15 -0
  124. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  125. package/dist/testing/cross_backend/full_spine_mount.js +55 -1
  126. package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
  127. package/dist/testing/cross_backend/identity_parity.js +6 -5
  128. package/dist/testing/cross_backend/in_process_setup.d.ts +12 -2
  129. package/dist/testing/cross_backend/in_process_setup.d.ts.map +1 -1
  130. package/dist/testing/cross_backend/in_process_setup.js +10 -7
  131. package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
  132. package/dist/testing/cross_backend/login_security.js +13 -18
  133. package/dist/testing/cross_backend/origin.js +1 -1
  134. package/dist/testing/cross_backend/origin_conformance.js +1 -1
  135. package/dist/testing/cross_backend/origin_scope.d.ts +6 -0
  136. package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -1
  137. package/dist/testing/cross_backend/origin_scope.js +13 -12
  138. package/dist/testing/cross_backend/peer_ping_ws.d.ts +2 -5
  139. package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
  140. package/dist/testing/cross_backend/peer_ping_ws.js +14 -24
  141. package/dist/testing/cross_backend/postgres_database.d.ts +85 -0
  142. package/dist/testing/cross_backend/postgres_database.d.ts.map +1 -0
  143. package/dist/testing/cross_backend/postgres_database.js +140 -0
  144. package/dist/testing/cross_backend/provided_context.d.ts +28 -0
  145. package/dist/testing/cross_backend/provided_context.d.ts.map +1 -0
  146. package/dist/testing/cross_backend/provided_context.js +35 -0
  147. package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
  148. package/dist/testing/cross_backend/query_shape.js +10 -12
  149. package/dist/testing/cross_backend/ready.js +2 -2
  150. package/dist/testing/cross_backend/revocation_drivers.d.ts +4 -3
  151. package/dist/testing/cross_backend/revocation_drivers.d.ts.map +1 -1
  152. package/dist/testing/cross_backend/revocation_drivers.js +1 -1
  153. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +3 -2
  154. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -1
  155. package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts +13 -0
  156. package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts.map +1 -0
  157. package/dist/testing/cross_backend/role_grant_offer_lifecycle.js +203 -0
  158. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +2 -5
  159. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  160. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +203 -111
  161. package/dist/testing/cross_backend/role_grant_participation.d.ts +13 -0
  162. package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
  163. package/dist/testing/cross_backend/role_grant_participation.js +32 -21
  164. package/dist/testing/cross_backend/role_grant_role_grammar.d.ts.map +1 -1
  165. package/dist/testing/cross_backend/role_grant_role_grammar.js +6 -9
  166. package/dist/testing/cross_backend/rpc_adversarial.d.ts +38 -0
  167. package/dist/testing/cross_backend/rpc_adversarial.d.ts.map +1 -0
  168. package/dist/testing/cross_backend/rpc_adversarial.js +292 -0
  169. package/dist/testing/cross_backend/rpc_success_fixtures.d.ts +24 -0
  170. package/dist/testing/cross_backend/rpc_success_fixtures.d.ts.map +1 -0
  171. package/dist/testing/cross_backend/rpc_success_fixtures.js +124 -0
  172. package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts +101 -0
  173. package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts.map +1 -0
  174. package/dist/testing/cross_backend/rust_spine_consumer_surface.js +113 -0
  175. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +10 -0
  176. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  177. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +27 -18
  178. package/dist/testing/cross_backend/session_cap.d.ts +9 -8
  179. package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
  180. package/dist/testing/cross_backend/session_cap.js +9 -4
  181. package/dist/testing/cross_backend/session_cookie_codec.js +1 -1
  182. package/dist/testing/cross_backend/setup.d.ts +56 -54
  183. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  184. package/dist/testing/cross_backend/setup.js +29 -29
  185. package/dist/testing/cross_backend/simple_request.d.ts.map +1 -1
  186. package/dist/testing/cross_backend/simple_request.js +4 -4
  187. package/dist/testing/cross_backend/spawn_backend.d.ts +59 -10
  188. package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -1
  189. package/dist/testing/cross_backend/spawn_backend.js +219 -60
  190. package/dist/testing/cross_backend/spine_suites.d.ts +231 -0
  191. package/dist/testing/cross_backend/spine_suites.d.ts.map +1 -0
  192. package/dist/testing/cross_backend/spine_suites.js +626 -0
  193. package/dist/testing/cross_backend/spine_surface_constants.d.ts +29 -0
  194. package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
  195. package/dist/testing/cross_backend/spine_surface_constants.js +29 -0
  196. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
  197. package/dist/testing/cross_backend/sse_round_trip.js +3 -4
  198. package/dist/testing/cross_backend/standard.d.ts +18 -1
  199. package/dist/testing/cross_backend/standard.d.ts.map +1 -1
  200. package/dist/testing/cross_backend/standard.js +2 -0
  201. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +24 -0
  202. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
  203. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +15 -0
  204. package/dist/testing/cross_backend/testing_backdoor.d.ts +2 -8
  205. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  206. package/dist/testing/cross_backend/testing_backdoor.js +7 -10
  207. package/dist/testing/cross_backend/testing_reset_actions.d.ts +3 -2
  208. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  209. package/dist/testing/cross_backend/testing_reset_actions.js +8 -4
  210. package/dist/testing/cross_backend/testing_server_bun.js +1 -1
  211. package/dist/testing/cross_backend/token_lifetime.js +1 -1
  212. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  213. package/dist/testing/cross_backend/token_scope_surface.js +9 -13
  214. package/dist/testing/cross_backend/transport_composition.d.ts +26 -2
  215. package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -1
  216. package/dist/testing/cross_backend/transport_composition.js +81 -51
  217. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +8 -0
  218. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  219. package/dist/testing/cross_backend/ts_spine_backend_config.js +17 -10
  220. package/dist/testing/cross_backend/ws_action_rate_limit.d.ts +2 -5
  221. package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
  222. package/dist/testing/cross_backend/ws_action_rate_limit.js +8 -14
  223. package/dist/testing/cross_backend/ws_connection_cap.d.ts +4 -9
  224. package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -1
  225. package/dist/testing/cross_backend/ws_connection_cap.js +11 -12
  226. package/dist/testing/cross_backend/ws_endpoint.d.ts +52 -0
  227. package/dist/testing/cross_backend/ws_endpoint.d.ts.map +1 -0
  228. package/dist/testing/cross_backend/ws_endpoint.js +54 -0
  229. package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts +2 -5
  230. package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts.map +1 -1
  231. package/dist/testing/cross_backend/ws_inbound_dispatch.js +9 -15
  232. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts +2 -7
  233. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -1
  234. package/dist/testing/cross_backend/ws_outbound_overflow.js +5 -9
  235. package/dist/testing/cross_backend/ws_role_gated.d.ts +4 -5
  236. package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -1
  237. package/dist/testing/cross_backend/ws_role_gated.js +12 -21
  238. package/dist/testing/cross_backend/ws_round_trip.d.ts +12 -9
  239. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  240. package/dist/testing/cross_backend/ws_round_trip.js +29 -34
  241. package/dist/testing/db_entities.d.ts +16 -3
  242. package/dist/testing/db_entities.d.ts.map +1 -1
  243. package/dist/testing/db_entities.js +28 -5
  244. package/dist/testing/integration_helpers.d.ts +61 -2
  245. package/dist/testing/integration_helpers.d.ts.map +1 -1
  246. package/dist/testing/integration_helpers.js +75 -18
  247. package/dist/testing/round_trip.d.ts +17 -0
  248. package/dist/testing/round_trip.d.ts.map +1 -1
  249. package/dist/testing/round_trip.js +67 -24
  250. package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
  251. package/dist/testing/rpc_attack_surface.js +13 -138
  252. package/dist/testing/rpc_envelope_cases.d.ts +111 -0
  253. package/dist/testing/rpc_envelope_cases.d.ts.map +1 -0
  254. package/dist/testing/rpc_envelope_cases.js +240 -0
  255. package/dist/testing/rpc_helpers.d.ts +31 -0
  256. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  257. package/dist/testing/rpc_helpers.js +35 -14
  258. package/dist/testing/rpc_round_trip.d.ts +62 -6
  259. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  260. package/dist/testing/rpc_round_trip.js +151 -39
  261. package/dist/testing/schema_generators.d.ts.map +1 -1
  262. package/dist/testing/schema_generators.js +22 -2
  263. package/dist/testing/standard.d.ts +26 -0
  264. package/dist/testing/standard.d.ts.map +1 -1
  265. package/dist/testing/standard.js +4 -1
  266. package/dist/testing/transports/raw_http.d.ts +21 -0
  267. package/dist/testing/transports/raw_http.d.ts.map +1 -1
  268. package/dist/testing/transports/raw_http.js +30 -0
  269. package/package.json +6 -4
  270. package/dist/testing/cross_backend/actor_search.d.ts +0 -6
  271. package/dist/testing/cross_backend/actor_search.d.ts.map +0 -1
  272. package/dist/testing/cross_backend/actor_search.js +0 -90
  273. package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +0 -1
@@ -9,38 +9,72 @@ import './assert_dev_env.js';
9
9
  * fixture-producing callback.
10
10
  *
11
11
  * Cadence: per-describe `setup_test()` call (see `testing/round_trip.ts` module
12
- * docstring). RPC round-trip tests fire one JSON-RPC envelope per
13
- * method-direction and don't mutate state in a way that contaminates the
14
- * next case.
12
+ * docstring). The generic POST and GET cases fire one JSON-RPC envelope per
13
+ * method-direction against shared principals (an authed account, an admin
14
+ * account, the keeper). A side-effecting method called as the authed account
15
+ * runs on a fresh account of its own (`round_trip_uses_fresh_account`):
16
+ * the generated params of a self-targeting method act on the caller — an
17
+ * `account_delete` with no `account_id` deletes it, an
18
+ * `account_session_revoke_all` ends its session — and on the shared account
19
+ * that would leave every later call refused as unauthenticated. A 401 to any
20
+ * non-public call fails (`assert_rpc_authenticated`), so a credential lost
21
+ * some other way goes red rather than vacuous. The `success_fixtures` case
22
+ * also mutates — each fixture builds a row and the call revokes, deletes, or
23
+ * decides it — so it runs last on the describe's fixture, and each factory
24
+ * seeds its own rows.
15
25
  *
16
26
  * @module
17
27
  */
18
28
  import { describe, test, beforeAll, assert } from 'vitest';
19
29
  import { ROLE_ADMIN } from '../auth/role_schema.js';
20
- import { JSONRPC_METHOD_NOT_FOUND, JsonrpcErrorResponse } from '../http/jsonrpc.js';
30
+ import { JSONRPC_INTERNAL_ERROR, JSONRPC_METHOD_NOT_FOUND, JsonrpcErrorResponse } from '../http/jsonrpc.js';
31
+ import { JSONRPC_ERROR_CODE_TO_HTTP_STATUS } from '../http/jsonrpc_errors.js';
21
32
  import { generate_valid_body } from './schema_generators.js';
22
- import { is_public_auth } from '../http/auth_shape.js';
23
33
  import { create_rpc_post_init, create_rpc_get_url, assert_jsonrpc_error_response, assert_jsonrpc_success_response, resolve_rpc_endpoints_for_setup, find_rpc_action, find_rpc_method } from './rpc_helpers.js';
34
+ import { is_unauthenticated_refusal, pick_auth_principal, round_trip_uses_fresh_account, to_principal_headers } from './integration_helpers.js';
24
35
  /**
25
- * Pick auth headers matching an RPC method's auth requirement. Accepts
26
- * any `KeeperHeaderProvider` — both `TestApp` (in-process) and
27
- * `TestFixture` (cross-backend) satisfy the shape structurally.
36
+ * Pick the principal matching an RPC method's auth requirement
37
+ * (`pick_auth_principal`, shared with the REST round-trip), and the headers
38
+ * carrying its credential. Accepts any `KeeperHeaderProvider` — both `TestApp`
39
+ * (in-process) and `TestFixture` (cross-backend) satisfy the shape
40
+ * structurally.
28
41
  */
29
- const pick_rpc_auth_headers = (method, keeper, authed_account, admin_account) => {
30
- const { auth } = method;
31
- if (is_public_auth(auth)) {
32
- return { host: 'localhost', origin: 'http://localhost:5173' };
33
- }
34
- if (auth.credential_types?.includes('daemon_token')) {
35
- return keeper.create_daemon_token_headers();
36
- }
37
- if (auth.roles?.length) {
38
- if (auth.roles.includes(ROLE_ADMIN)) {
39
- return admin_account.create_session_headers();
40
- }
41
- return keeper.create_session_headers();
42
- }
43
- return authed_account.create_session_headers();
42
+ export const pick_rpc_auth = (method, keeper, authed_account, admin_account) => {
43
+ const principal = pick_auth_principal(method.auth);
44
+ return {
45
+ principal,
46
+ headers: to_principal_headers(principal, keeper, authed_account, admin_account)
47
+ };
48
+ };
49
+ /**
50
+ * Guard against a refusal that says the call carried no valid credential
51
+ * when it carried one (`is_unauthenticated_refusal`). The round-trip sends
52
+ * every non-public method a live credential its auth admits, so such a 401
53
+ * means the backend failed to authenticate it — most often because an earlier
54
+ * call destroyed it (a self-delete, a revoke-all), which would turn every
55
+ * later method's coverage into a vacuous refusal. A public method may still
56
+ * answer 401 (a login with a wrong password), and so may a handler refusing
57
+ * an authenticated call with a reason of its own (`invalid_credentials`).
58
+ *
59
+ * @param method - the RPC method name
60
+ * @param status - the HTTP status
61
+ * @param body - the parsed response body (or raw text when not JSON)
62
+ * @param principal - the principal the call was sent as
63
+ */
64
+ export const assert_rpc_authenticated = (method, status, body, principal) => {
65
+ if (principal === 'public')
66
+ return;
67
+ const parsed = JsonrpcErrorResponse.safeParse(body);
68
+ const data = parsed.success ? parsed.data.error.data : undefined;
69
+ const reason = data !== null && typeof data === 'object' ? data.reason : undefined;
70
+ if (!is_unauthenticated_refusal(status, reason))
71
+ return;
72
+ const detail = parsed.success
73
+ ? `code ${parsed.data.error.code}, message ${JSON.stringify(parsed.data.error.message)}${data === undefined ? '' : `, data ${JSON.stringify(data)}`}`
74
+ : `body ${typeof body === 'string' ? body.slice(0, 500) : JSON.stringify(body).slice(0, 500)}`;
75
+ assert.fail(`method '${method}' answered 401 to the ${principal} principal's credential (${detail})` +
76
+ ' — the call carried a credential its auth admits, so the backend owed an' +
77
+ ' authenticated answer; a 401 here means that credential stopped working');
44
78
  };
45
79
  /**
46
80
  * Guard against silent parity gaps: a method enumerated from the local action
@@ -62,6 +96,76 @@ const assert_method_implemented = (method, body) => {
62
96
  ` returned method-not-found — backend is missing this method (parity gap)`);
63
97
  }
64
98
  };
99
+ /**
100
+ * Read a response body as JSON, falling back to the raw text — a crashed
101
+ * backend may answer with a non-JSON body (an HTML or plain-text 500), and
102
+ * the round-trip must report that response, not a JSON parse error with no
103
+ * method attached.
104
+ */
105
+ const read_rpc_body = async (res) => {
106
+ const text = await res.text();
107
+ try {
108
+ return JSON.parse(text);
109
+ }
110
+ catch {
111
+ return text;
112
+ }
113
+ };
114
+ /** The HTTP status an `internal_error` maps to, read off the shared status table. */
115
+ const INTERNAL_ERROR_HTTP_STATUS = JSONRPC_ERROR_CODE_TO_HTTP_STATUS[JSONRPC_INTERNAL_ERROR];
116
+ /**
117
+ * Guard against a backend that crashes on schema-valid input. A response is
118
+ * a crash when any of these holds:
119
+ *
120
+ * - its HTTP status is the one `internal_error` maps to (500) — what an
121
+ * uncaught throw and an unmapped error code both answer
122
+ * - its JSON-RPC code is `internal_error` (-32603)
123
+ * - its status is ≥500 and its body is not a well-formed JSON-RPC error
124
+ * envelope (an HTML or plain-text crash page)
125
+ *
126
+ * Any other ≥500 that carries a well-formed envelope with a declared code
127
+ * passes like a 4xx: `service_unavailable` (503), `timeout` (504), or a
128
+ * consumer's own code mapped in `JSONRPC_ERROR_CODE_TO_HTTP_STATUS` (a 502).
129
+ * Those are server conditions a handler declares on purpose — a dependency
130
+ * the test environment doesn't provide — while an undeclared 500 or -32603
131
+ * is a handler that failed where it owed an answer.
132
+ *
133
+ * @param method - the RPC method name
134
+ * @param status - the HTTP status
135
+ * @param body - the parsed response body (or raw text when not JSON)
136
+ * @param expected - what the call owed: a refusal (the generic nil-id loop)
137
+ * or a success (a declared success fixture), for the failure message
138
+ */
139
+ const assert_not_crash = (method, status, body, expected) => {
140
+ const parsed = JsonrpcErrorResponse.safeParse(body);
141
+ const code = parsed.success ? parsed.data.error.code : null;
142
+ const crashed = parsed.success
143
+ ? status === INTERNAL_ERROR_HTTP_STATUS || code === JSONRPC_INTERNAL_ERROR
144
+ : status >= 500;
145
+ if (!crashed)
146
+ return;
147
+ const message = parsed.success
148
+ ? parsed.data.error.message
149
+ : typeof body === 'string'
150
+ ? body.slice(0, 500)
151
+ : JSON.stringify(body).slice(0, 500);
152
+ assert.fail(`method '${method}' crashed on ${expected === 'success' ? 'its success fixture' : 'schema-valid params'} (status ${status}, code ${code ?? 'none'}, message ${JSON.stringify(message)})` +
153
+ (expected === 'success'
154
+ ? ' — the fixture built a real row, so the backend owed a success here'
155
+ : ' — the backend owes a declared refusal here (a 4xx, or a declared 5xx code)'));
156
+ };
157
+ /**
158
+ * Assert a non-ok response is an acceptable refusal: the method exists, the
159
+ * failure is not a crash (`assert_not_crash`), an authenticated call was not
160
+ * refused as unauthenticated (`assert_rpc_authenticated`), and the body is a
161
+ * well-formed JSON-RPC error envelope.
162
+ */
163
+ const assert_rpc_refusal = (method, status, body, principal) => {
164
+ assert_method_implemented(method, body);
165
+ assert_not_crash(method, status, body, 'refusal');
166
+ assert_rpc_authenticated(method, status, body, principal);
167
+ assert_jsonrpc_error_response(body);
168
+ };
65
169
  /**
66
170
  * Run schema-driven round-trip validation for RPC endpoints.
67
171
  *
@@ -74,10 +178,15 @@ const assert_method_implemented = (method, body) => {
74
178
  *
75
179
  * Error responses (from missing DB state, etc.) are expected and validated
76
180
  * as well-formed JSON-RPC errors. Successful responses are validated against
77
- * `action.spec.output`. A `method not found` (-32601) error is the one
78
- * exception — it means the backend is missing a method the local surface
79
- * advertises, so the round-trip fails loud (`assert_method_implemented`)
80
- * rather than accepting it as a valid error envelope.
181
+ * `action.spec.output`. Two error classes fail loud instead of passing as a
182
+ * valid envelope: `method not found` (-32601) means the backend is missing a
183
+ * method the local surface advertises (`assert_method_implemented`), and a
184
+ * crash — HTTP 500, `internal_error` (-32603), or a ≥500 with no JSON-RPC
185
+ * envelope — means the backend failed on schema-valid input
186
+ * (`assert_not_crash`). A declared ≥500 code (`service_unavailable`,
187
+ * `timeout`) passes as a refusal. A method the suite can't drive generically
188
+ * goes in `skip_methods`; one whose generated params can't reach its handler
189
+ * gets an `input_overrides` entry.
81
190
  */
82
191
  export const describe_rpc_round_trip_tests = (options) => {
83
192
  const skip_set = new Set(options.skip_methods);
@@ -116,18 +225,20 @@ export const describe_rpc_round_trip_tests = (options) => {
116
225
  continue;
117
226
  const override = options.input_overrides?.get(action.spec.method);
118
227
  const params = override ?? generate_valid_body(action.spec.input) ?? null;
119
- const headers = pick_rpc_auth_headers(surface_method, fixture, authed_account, admin_account);
228
+ const picked = pick_rpc_auth(surface_method, fixture, authed_account, admin_account);
229
+ const { principal, headers } = round_trip_uses_fresh_account(picked.principal, action.spec.side_effects)
230
+ ? pick_rpc_auth(surface_method, fixture, await fixture.create_account({ roles: [] }), admin_account)
231
+ : picked;
120
232
  const init = create_rpc_post_init(action.spec.method, params);
121
233
  Object.assign(init.headers, headers);
122
234
  const res = await fixture.transport(ep_spec.path, init);
123
- const body = await res.json();
235
+ const body = await read_rpc_body(res);
124
236
  try {
125
237
  if (res.ok) {
126
238
  assert_jsonrpc_success_response(body, action.spec.output);
127
239
  }
128
240
  else {
129
- assert_method_implemented(action.spec.method, body);
130
- assert_jsonrpc_error_response(body);
241
+ assert_rpc_refusal(action.spec.method, res.status, body, principal);
131
242
  }
132
243
  }
133
244
  catch (e) {
@@ -150,17 +261,16 @@ export const describe_rpc_round_trip_tests = (options) => {
150
261
  continue;
151
262
  const override = options.input_overrides?.get(action.spec.method);
152
263
  const params = override ?? generate_valid_body(action.spec.input) ?? undefined;
153
- const headers = pick_rpc_auth_headers(surface_method, fixture, authed_account, admin_account);
264
+ const { principal, headers } = pick_rpc_auth(surface_method, fixture, authed_account, admin_account);
154
265
  const url = create_rpc_get_url(ep_spec.path, action.spec.method, params);
155
266
  const res = await fixture.transport(url, { headers });
156
- const body = await res.json();
267
+ const body = await read_rpc_body(res);
157
268
  try {
158
269
  if (res.ok) {
159
270
  assert_jsonrpc_success_response(body, action.spec.output);
160
271
  }
161
272
  else {
162
- assert_method_implemented(action.spec.method, body);
163
- assert_jsonrpc_error_response(body);
273
+ assert_rpc_refusal(action.spec.method, res.status, body, principal);
164
274
  }
165
275
  }
166
276
  catch (e) {
@@ -178,14 +288,16 @@ export const describe_rpc_round_trip_tests = (options) => {
178
288
  assert.ok(located, `success_fixtures references unknown RPC method '${method}'`);
179
289
  const surface = find_rpc_method(surface_rpc_endpoints, method);
180
290
  assert.ok(surface, `success_fixtures method '${method}' missing from generated surface`);
181
- const params = await build(fixture);
182
- const headers = pick_rpc_auth_headers(surface.method_spec, fixture, authed_account, admin_account);
183
- const init = create_rpc_post_init(method, params);
291
+ const fixture_case = await build(fixture);
292
+ const headers = fixture_case.headers ??
293
+ pick_rpc_auth(surface.method_spec, fixture, authed_account, admin_account).headers;
294
+ const init = create_rpc_post_init(method, fixture_case.params);
184
295
  Object.assign(init.headers, headers);
185
296
  const res = await fixture.transport(located.path, init);
186
- const body = await res.json();
297
+ const body = await read_rpc_body(res);
187
298
  try {
188
299
  assert_method_implemented(method, body);
300
+ assert_not_crash(method, res.status, body, 'success');
189
301
  assert.ok(res.ok, `success fixture expected a success response, got status ${res.status}: ${JSON.stringify(body)}`);
190
302
  assert_jsonrpc_success_response(body, located.action.spec.output);
191
303
  }
@@ -1 +1 @@
1
- {"version":3,"file":"schema_generators.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_generators.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAKN,KAAK,YAAY,EACjB,MAAM,WAAW,CAAC;AAInB;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,MAAM,GAAG,IAkBhE,CAAC;AAmHF,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,GAAI,OAAO,YAAY,EAAE,cAAc,CAAC,CAAC,OAAO,KAAG,OAgGnF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,KAAG,MACL,CAAC;AAElE;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,gBAAgB,CAAC,CAAC,SAAS,KAAG,MAiB9E,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAC/B,cAAc,CAAC,CAAC,OAAO,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SA+B5B,CAAC"}
1
+ {"version":3,"file":"schema_generators.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_generators.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAKN,KAAK,YAAY,EACjB,MAAM,WAAW,CAAC;AAInB;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,MAAM,GAAG,IAkBhE,CAAC;AAmHF,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,GAAI,OAAO,YAAY,EAAE,cAAc,CAAC,CAAC,OAAO,KAAG,OA8GnF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,KAAG,MACL,CAAC;AAElE;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,gBAAgB,CAAC,CAAC,SAAS,KAAG,MAiB9E,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAC/B,cAAc,CAAC,CAAC,OAAO,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SA+B5B,CAAC"}
@@ -174,8 +174,28 @@ export const generate_valid_value = (field, field_schema) => {
174
174
  return '2020-01-01T00:00:00.000Z';
175
175
  return generate_valid_string(field_schema);
176
176
  case 'number':
177
- case 'int':
178
- return 1;
177
+ case 'int': {
178
+ // 1, unless the schema's bounds rule it out (`.negative()`, `.min(10)`)
179
+ for (const candidate of [1, -1, 0]) {
180
+ if (field_schema.safeParse(candidate).success)
181
+ return candidate;
182
+ }
183
+ try {
184
+ const json = z.toJSONSchema(field_schema);
185
+ if (typeof json.minimum === 'number')
186
+ return Math.ceil(json.minimum);
187
+ if (typeof json.exclusiveMinimum === 'number')
188
+ return Math.floor(json.exclusiveMinimum) + 1;
189
+ if (typeof json.maximum === 'number')
190
+ return Math.floor(json.maximum);
191
+ if (typeof json.exclusiveMaximum === 'number')
192
+ return Math.ceil(json.exclusiveMaximum) - 1;
193
+ }
194
+ catch {
195
+ // no bounds to read
196
+ }
197
+ return 1; // fall through — generate_valid_body will report the failure
198
+ }
179
199
  case 'boolean':
180
200
  return true;
181
201
  case 'array': {
@@ -21,6 +21,7 @@ import type { RoleSchemaResult } from '../auth/role_schema.js';
21
21
  import type { BootstrapServerOptions } from '../server/app_server.js';
22
22
  import type { AppServerContext } from '../server/app_server_context.js';
23
23
  import type { RouteSpec } from '../http/route_spec.js';
24
+ import { type RpcSuccessFixtures } from './rpc_round_trip.js';
24
25
  import type { RpcEndpointsSuiteOption } from './rpc_helpers.js';
25
26
  import type { BackendCapabilities } from './cross_backend/capabilities.js';
26
27
  import type { SetupTest } from './cross_backend/setup.js';
@@ -85,6 +86,31 @@ export interface StandardTestOptions {
85
86
  error_coverage_min?: number;
86
87
  /** Override the bootstrap-success suite's synthetic token. */
87
88
  bootstrap_token?: string;
89
+ /**
90
+ * Forwarded to `describe_rpc_round_trip_tests` as `success_fixtures`
91
+ * (method name → async params factory) — drives a populated **success**
92
+ * body for methods that act on an existing row, validated against the
93
+ * method's `output` schema. `create_spine_rpc_success_fixtures`
94
+ * (`cross_backend/rpc_success_fixtures.ts`) builds the map for the
95
+ * standard spine methods. See `RpcRoundTripTestOptions.success_fixtures`.
96
+ */
97
+ rpc_success_fixtures?: RpcSuccessFixtures;
98
+ /**
99
+ * Forwarded to `describe_rpc_round_trip_tests` as `skip_methods` — the
100
+ * consumer escape hatch for a method the generic round-trip can't drive at
101
+ * all (a handler whose test wiring stubs out the deps it reaches, or reads
102
+ * tables the suite's database doesn't migrate). Each entry hides the
103
+ * method from both the crash check and the envelope check, so name the
104
+ * reason beside it.
105
+ */
106
+ rpc_skip_methods?: Array<string>;
107
+ /**
108
+ * Forwarded to `describe_rpc_round_trip_tests` as `input_overrides`
109
+ * (method name → params) — for a method whose generated params can't
110
+ * reach its handler cleanly. The response is held to the same rules as a
111
+ * generated one.
112
+ */
113
+ rpc_input_overrides?: Map<string, Record<string, unknown>>;
88
114
  }
89
115
  /**
90
116
  * Run the full standard test bundle — integration, admin (when `roles`
@@ -1 +1 @@
1
- {"version":3,"file":"standard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/standard.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AASvD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,2CAA2C;IAC3C,UAAU,EAAE,SAAS,CAAC;IACtB;;;OAGG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B;;;;OAIG;IACH,YAAY,EAAE,mBAAmB,CAAC;IAClC,uFAAuF;IACvF,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE;;;;;OAKG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;OAGG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,sEAAsE;IACtE,yBAAyB,CAAC,EAAE,eAAe,CAAC;IAC5C;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,SAAS,mBAAmB,KAAG,IAsDtE,CAAC"}
1
+ {"version":3,"file":"standard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/standard.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAKvD,OAAO,EAAiC,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAI7F,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,2CAA2C;IAC3C,UAAU,EAAE,SAAS,CAAC;IACtB;;;OAGG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B;;;;OAIG;IACH,YAAY,EAAE,mBAAmB,CAAC;IAClC,uFAAuF;IACvF,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE;;;;;OAKG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;OAGG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,sEAAsE;IACtE,yBAAyB,CAAC,EAAE,eAAe,CAAC;IAC5C;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,kBAAkB,CAAC;IAC1C;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACjC;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC3D;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,SAAS,mBAAmB,KAAG,IAyDtE,CAAC"}
@@ -30,7 +30,10 @@ export const describe_standard_tests = (options) => {
30
30
  setup_test: options.setup_test,
31
31
  surface_source: options.surface_source,
32
32
  session_options: options.session_options,
33
- rpc_endpoints: options.rpc_endpoints
33
+ rpc_endpoints: options.rpc_endpoints,
34
+ skip_methods: options.rpc_skip_methods,
35
+ input_overrides: options.rpc_input_overrides,
36
+ success_fixtures: options.rpc_success_fixtures
34
37
  });
35
38
  describe_data_exposure_tests({
36
39
  setup_test: options.setup_test,
@@ -1,4 +1,11 @@
1
1
  import '../assert_dev_env.js';
2
+ /**
3
+ * The read window a raw exchange gets by default in the cross suites. The
4
+ * server holds every byte it will get when the wait starts, so a backend that
5
+ * answers on what it has answers at once, and one that waits for more runs the
6
+ * whole window.
7
+ */
8
+ export declare const RAW_HTTP_READ_TIMEOUT_MS = 2000;
2
9
  /** What a raw exchange read back, and how it ended. */
3
10
  export interface RawHttpExchange {
4
11
  /** Every byte the server sent, `latin1`-decoded. */
@@ -53,6 +60,20 @@ export interface RawHttpResponse {
53
60
  * @param request_bytes - the bytes to send, `latin1`-encoded
54
61
  */
55
62
  export declare const raw_http_exchange: (base_url: string, request_bytes: string, options: RawHttpExchangeOptions) => Promise<RawHttpExchange>;
63
+ /**
64
+ * Render request headers as raw HTTP/1.1 header lines, each ending in CRLF.
65
+ *
66
+ * @param headers - header names to values, sent in iteration order
67
+ * @returns the lines, concatenated
68
+ */
69
+ export declare const raw_http_header_lines: (headers: Record<string, string>) => string;
70
+ /**
71
+ * Parse a response body as JSON.
72
+ *
73
+ * @param body - the body text
74
+ * @returns the parsed value, or `undefined` when the body is not JSON
75
+ */
76
+ export declare const raw_http_parse_json: (body: string) => unknown;
56
77
  /**
57
78
  * Count HTTP response status lines in a raw byte stream. Deliberately
58
79
  * **unanchored** (no `^`/`m`): a second pipelined response is concatenated
@@ -1 +1 @@
1
- {"version":3,"file":"raw_http.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/raw_http.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC/B,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED,uCAAuC;AACvC,MAAM,WAAW,sBAAsB;IACtC,uDAAuD;IACvD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;CACtC;AAED,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAC7B,UAAU,MAAM,EAChB,eAAe,MAAM,EACrB,SAAS,sBAAsB,KAC7B,OAAO,CAAC,eAAe,CAwBvB,CAAC;AAEJ;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,GAAI,KAAK,MAAM,KAAG,MACN,CAAC;AAElD,0FAA0F;AAC1F,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAG5D,CAAC;AAyBF;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,MAAM,KAAG,eAAe,GAAG,IAyBvE,CAAC"}
1
+ {"version":3,"file":"raw_http.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/raw_http.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAE7C,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC/B,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED,uCAAuC;AACvC,MAAM,WAAW,sBAAsB;IACtC,uDAAuD;IACvD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;CACtC;AAED,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAC7B,UAAU,MAAM,EAChB,eAAe,MAAM,EACrB,SAAS,sBAAsB,KAC7B,OAAO,CAAC,eAAe,CAwBvB,CAAC;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,MAG7D,CAAC;AAEZ;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,KAAG,OAMlD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,GAAI,KAAK,MAAM,KAAG,MACN,CAAC;AAElD,0FAA0F;AAC1F,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAG5D,CAAC;AAyBF;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,MAAM,KAAG,eAAe,GAAG,IAyBvE,CAAC"}
@@ -18,6 +18,13 @@ import '../assert_dev_env.js';
18
18
  * @module
19
19
  */
20
20
  import { connect } from 'node:net';
21
+ /**
22
+ * The read window a raw exchange gets by default in the cross suites. The
23
+ * server holds every byte it will get when the wait starts, so a backend that
24
+ * answers on what it has answers at once, and one that waits for more runs the
25
+ * whole window.
26
+ */
27
+ export const RAW_HTTP_READ_TIMEOUT_MS = 2000;
21
28
  /**
22
29
  * Open a raw TCP socket to `base_url`, write `request_bytes` once, and collect
23
30
  * what the server sends back until it closes the connection, a whole response
@@ -59,6 +66,29 @@ export const raw_http_exchange = (base_url, request_bytes, options) => new Promi
59
66
  socket.on('error', () => { }); // EPIPE/ECONNRESET on a mid-write close is expected
60
67
  socket.on('close', () => finish(true));
61
68
  });
69
+ /**
70
+ * Render request headers as raw HTTP/1.1 header lines, each ending in CRLF.
71
+ *
72
+ * @param headers - header names to values, sent in iteration order
73
+ * @returns the lines, concatenated
74
+ */
75
+ export const raw_http_header_lines = (headers) => Object.entries(headers)
76
+ .map(([name, value]) => `${name}: ${value}\r\n`)
77
+ .join('');
78
+ /**
79
+ * Parse a response body as JSON.
80
+ *
81
+ * @param body - the body text
82
+ * @returns the parsed value, or `undefined` when the body is not JSON
83
+ */
84
+ export const raw_http_parse_json = (body) => {
85
+ try {
86
+ return JSON.parse(body);
87
+ }
88
+ catch {
89
+ return undefined;
90
+ }
91
+ };
62
92
  /**
63
93
  * Count HTTP response status lines in a raw byte stream. Deliberately
64
94
  * **unanchored** (no `^`/`m`): a second pipelined response is concatenated
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.147.0",
3
+ "version": "0.148.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",
@@ -14,10 +14,12 @@
14
14
  "build": "gro build",
15
15
  "check": "gro check",
16
16
  "test": "gro test",
17
+ "test:cross": "node --conditions=development src/test/cross_backend/run_cross.ts",
17
18
  "test:cross:ts": "FUZ_TEST_CROSS_BACKEND=1 vitest run --project cross_backend_ts_node --project cross_backend_ts_deno --project cross_backend_ts_bun",
18
19
  "test:cross:rust-spine-stub": "FUZ_TEST_CROSS_BACKEND=1 vitest run --project cross_backend_rust_spine_stub",
19
20
  "test:cross:parity": "FUZ_TEST_CROSS_BACKEND=1 vitest run --project cross_backend_parity",
20
21
  "test:cross:security": "FUZ_TEST_CROSS_BACKEND=1 vitest run --project cross_backend_security",
22
+ "test:cross:ts-differential": "FUZ_TEST_CROSS_BACKEND=1 vitest run --project cross_backend_ts_differential",
21
23
  "benchmark:cross-impl": "gro run src/benchmarks/cross_impl.bench.ts",
22
24
  "preview": "vite preview",
23
25
  "deploy": "gro deploy"
@@ -33,7 +35,7 @@
33
35
  "peerDependencies": {
34
36
  "@electric-sql/pglite": ">=0.4",
35
37
  "@fuzdev/blake3-wasm": ">=0.2.0",
36
- "@fuzdev/fuz_css": ">=0.70.0",
38
+ "@fuzdev/fuz_css": ">=0.71.1",
37
39
  "@fuzdev/fuz_schema": "^0.5.0",
38
40
  "@fuzdev/fuz_util": ">=0.78.0",
39
41
  "@fuzdev/pglet-wasm": ">=0.1.2",
@@ -92,11 +94,11 @@
92
94
  "@electric-sql/pglite": "^0.5.8",
93
95
  "@fuzdev/blake3-wasm": "^0.2.0",
94
96
  "@fuzdev/fuz_code": "^0.54.0",
95
- "@fuzdev/fuz_css": "^0.71.0",
97
+ "@fuzdev/fuz_css": "^0.71.1",
96
98
  "@fuzdev/fuz_schema": "^0.5.0",
97
99
  "@fuzdev/fuz_ui": "^0.220.0",
98
100
  "@fuzdev/fuz_util": "^0.78.0",
99
- "@fuzdev/gro": "^0.220.0",
101
+ "@fuzdev/gro": "^0.222.0",
100
102
  "@fuzdev/mdz": "^0.6.0",
101
103
  "@fuzdev/pglet-wasm": "^0.1.2",
102
104
  "@hono/bun": "^1.0.1",
@@ -1,6 +0,0 @@
1
- import '../assert_dev_env.js';
2
- import type { RpcPathCrossSuiteOptions } from './setup.js';
3
- /** Options for the actor-search parity suite (the standard RPC-dispatched shape). */
4
- export type ActorSearchCrossTestOptions = RpcPathCrossSuiteOptions;
5
- export declare const describe_actor_search_cross_tests: (options: ActorSearchCrossTestOptions) => void;
6
- //# sourceMappingURL=actor_search.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"actor_search.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/actor_search.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAyC9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D,qFAAqF;AACrF,MAAM,MAAM,2BAA2B,GAAG,wBAAwB,CAAC;AASnE,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,IAyDxF,CAAC"}
@@ -1,90 +0,0 @@
1
- import '../assert_dev_env.js';
2
- /**
3
- * Cross-backend parity suite for `actor_search`.
4
- *
5
- * `actor_search` is an opt-in case-insensitive prefix search over
6
- * `actor.name` (`{query, scope_ids?, limit?} → {actors: [{id, username,
7
- * display_name?}]}`), not folded into the standard bundle. Like
8
- * `actor_lookup` / cells, it's live-mounted on the spine RPC path but kept
9
- * off the declared surface, so this dedicated suite is its validator. The
10
- * security property under test is the **empty-`scope_ids` admin gate**:
11
- *
12
- * - **anonymous → 401** — the account-grain auth gate refuses an
13
- * unauthenticated caller before the handler runs.
14
- * - **non-admin + no `scope_ids` → 400** `actor_search_scope_required` — an
15
- * unbounded global search is admin-only; a non-admin must scope the query.
16
- * This is the core security assertion, exercised against each impl's real
17
- * auth resolution.
18
- * - **non-admin + `scope_ids` → 200** — passing a scope bypasses the admin
19
- * requirement (results are filtered to actors holding active role_grants on
20
- * those scopes); an unheld scope simply yields an empty result, not a
21
- * rejection — proving the gate keys on `scope_ids` presence, not identity.
22
- * - **admin + no `scope_ids` → 200** — the admin path reaches the unbounded
23
- * search.
24
- *
25
- * Cites `security.md` §Authorization (the `actor_search` scope gate).
26
- *
27
- * Runs both legs via the shared `{setup_test}` protocol: in-process
28
- * (`auth/actor_search_parity.db.test.ts`) + cross-process
29
- * (`cross_backend/actor_search.cross.test.ts`, TS spine binaries + Rust
30
- * `testing_spine_stub`). Mounted on every spine, so the suite is ungated.
31
- *
32
- * @module
33
- */
34
- import { describe, test, assert } from 'vitest';
35
- import { actor_search_action_spec, ERROR_ACTOR_SEARCH_SCOPE_REQUIRED } from '../../auth/actor_search_action_specs.js';
36
- import { SPINE_RPC_PATH } from './spine_surface_constants.js';
37
- /** Nil UUID — an unheld scope id (no actor holds a role_grant on it). */
38
- const NIL_UUID = '00000000-0000-0000-0000-000000000000';
39
- /** Build the JSON-RPC envelope body for an `actor_search` call. */
40
- const search_envelope = (params, id) => JSON.stringify({ jsonrpc: '2.0', method: actor_search_action_spec.method, params, id });
41
- export const describe_actor_search_cross_tests = (options) => {
42
- const { setup_test } = options;
43
- const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
44
- describe('actor_search parity', () => {
45
- test('anonymous → 401 (account-grain auth gate)', async () => {
46
- const fixture = await setup_test();
47
- const res = await fixture.fresh_transport()(rpc_path, {
48
- method: 'POST',
49
- headers: { 'content-type': 'application/json' },
50
- body: search_envelope({ query: 'a' }, 'anon-search')
51
- });
52
- assert.strictEqual(res.status, 401, 'unauthenticated actor_search must be refused');
53
- });
54
- test('non-admin + no scope_ids → 400 actor_search_scope_required', async () => {
55
- const fixture = await setup_test();
56
- const account = await fixture.create_account();
57
- const res = await fixture.fresh_transport()(rpc_path, {
58
- method: 'POST',
59
- headers: { ...account.create_session_headers(), 'content-type': 'application/json' },
60
- body: search_envelope({ query: 'a' }, 'nonadmin-noscope')
61
- });
62
- assert.strictEqual(res.status, 400, 'non-admin unbounded search must be rejected');
63
- const body = (await res.json());
64
- assert.strictEqual(body.error?.data?.reason, ERROR_ACTOR_SEARCH_SCOPE_REQUIRED, 'rejection carries the scope-required reason');
65
- });
66
- test('non-admin + scope_ids → 200 (scope bypasses admin gate)', async () => {
67
- const fixture = await setup_test();
68
- const account = await fixture.create_account();
69
- const res = await fixture.fresh_transport()(rpc_path, {
70
- method: 'POST',
71
- headers: { ...account.create_session_headers(), 'content-type': 'application/json' },
72
- body: search_envelope({ query: 'a', scope_ids: [NIL_UUID] }, 'nonadmin-scope')
73
- });
74
- assert.strictEqual(res.status, 200, 'non-admin with a scope filter is allowed');
75
- const body = (await res.json());
76
- assert(Array.isArray(body.result?.actors), 'response carries an actors array');
77
- });
78
- test('admin + no scope_ids → 200 (admin reaches unbounded search)', async () => {
79
- const fixture = await setup_test();
80
- const res = await fixture.transport(rpc_path, {
81
- method: 'POST',
82
- headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
83
- body: search_envelope({ query: 'a' }, 'admin-noscope')
84
- });
85
- assert.strictEqual(res.status, 200, 'admin unbounded search is allowed');
86
- const body = (await res.json());
87
- assert(Array.isArray(body.result?.actors), 'response carries an actors array');
88
- });
89
- });
90
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"cell_cross_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_cross_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAgB9B,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAG7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAEvE,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC9F;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAC1B,WAAW,cAAc,EACzB,MAAM,MAAM,EACZ,QAAQ,MAAM,EACd,QAAQ,OAAO,EACf,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC7B,OAAO,CAAC,SAAS,CAMnB,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,YAAY,GAAI,GAAG,SAAS,KAAG,OAG/B,CAAC;AAEd;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,GAAG,SAAS,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAG,CAUrE,CAAC"}