@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
@@ -5,18 +5,30 @@ import '../assert_dev_env.js';
5
5
  *
6
6
  * Lifecycle:
7
7
  *
8
- * 1. Write the bootstrap token (`config.bootstrap.token`) to
9
- * `config.bootstrap.token_path` so the binary picks it up at startup.
10
- * 2. `child_process.spawn(...)` the binary with `detached: true` —
8
+ * 1. Refuse when `config.base_url`'s port already accepts a TCP connection —
9
+ * another cross run or a stale backend holds it, and the health probe
10
+ * would otherwise drive that process instead of the one spawned here.
11
+ * 2. Write the bootstrap token (`config.bootstrap.token`) to
12
+ * `config.bootstrap.token_path` so the binary picks it up at startup, and
13
+ * delete any `config.bootstrap.daemon_token_path` a previous run left, so
14
+ * step 5 can only read the token this binary writes.
15
+ * 3. `child_process.spawn(...)` the binary with `detached: true` —
11
16
  * creates a new process group so a `SIGTERM` to the negative PID
12
17
  * tears down any descendants the binary spawned (PTYs, child
13
- * workers). vitest worker death + Ctrl+C handlers also fire the
14
- * group teardown so ports never strand.
15
- * 3. Poll `{base_url}{health_path}` until it returns 2xx or
16
- * `startup_timeout_ms` elapses.
17
- * 4. Read `config.bootstrap.daemon_token_path` to load the binary's
18
+ * workers). vitest worker death + `SIGINT` / `SIGTERM` / `SIGHUP`
19
+ * handlers also fire the group teardown so ports never strand.
20
+ * 4. Poll `{base_url}{health_path}` until it returns 2xx or
21
+ * `startup_timeout_ms` elapses — each probe bounded by its own timeout, so
22
+ * a listener that accepts but never answers can't outlast the deadline.
23
+ * 5. Read `config.bootstrap.daemon_token_path` to load the binary's
18
24
  * deterministic daemon token; thread it onto `BackendHandle` so
19
25
  * `_testing_reset` and other keeper-credential calls can authenticate.
26
+ * 6. Log one line with the time from spawn to step 5 done, so a run shows
27
+ * what each backend costs to bring up.
28
+ *
29
+ * The child's stderr is kept as a bounded tail (`STDERR_TAIL_MAX_BYTES`):
30
+ * a startup failure reports it with the exit code or signal, and an exit
31
+ * after startup that teardown didn't cause is logged with it.
20
32
  *
21
33
  * Bootstrapping (`POST /api/account/bootstrap`) is a separate concern —
22
34
  * the caller composes `bootstrap()` from `testing/transports/bootstrap.ts`
@@ -27,10 +39,26 @@ import '../assert_dev_env.js';
27
39
  * @module
28
40
  */
29
41
  import { spawn } from 'node:child_process';
30
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
42
+ import { readFile, writeFile, mkdir, rm } from 'node:fs/promises';
43
+ import { connect } from 'node:net';
31
44
  import { dirname } from 'node:path';
32
45
  /** Number of ms between health-probe attempts. Tuned to be cheap on busy CI runners. */
33
46
  const HEALTH_PROBE_INTERVAL_MS = 100;
47
+ /**
48
+ * Upper bound on one health-probe request (ms), further capped by the time
49
+ * left before `startup_timeout_ms`. A backend answers `/health` in
50
+ * milliseconds once it listens; this only fires for a listener that accepts
51
+ * and never responds.
52
+ */
53
+ const HEALTH_PROBE_REQUEST_TIMEOUT_MS = 2_000;
54
+ /** How long the pre-spawn port check waits for a TCP connection (ms). */
55
+ const PORT_PROBE_TIMEOUT_MS = 1_000;
56
+ /**
57
+ * How much of the child's stderr is kept (bytes) — the tail a startup failure
58
+ * or an unexpected exit reports. Bounded so a backend that logs to stderr for
59
+ * a whole suite doesn't grow the runner's memory with it.
60
+ */
61
+ export const STDERR_TAIL_MAX_BYTES = 16 * 1024;
34
62
  /**
35
63
  * Grace window after teardown's SIGTERM before escalating to SIGKILL on the
36
64
  * process group. A well-behaved backend exits within milliseconds; this only
@@ -39,6 +67,8 @@ const HEALTH_PROBE_INTERVAL_MS = 100;
39
67
  * the await below can't strand the whole run forever.
40
68
  */
41
69
  const TEARDOWN_SIGKILL_GRACE_MS = 3_000;
70
+ /** How often teardown checks whether the process group has exited (ms). */
71
+ const TEARDOWN_POLL_INTERVAL_MS = 20;
42
72
  /**
43
73
  * Sleep helper for the probe loop. Resolves after `ms`.
44
74
  */
@@ -48,17 +78,19 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
48
78
  * Network errors during the wait window are expected (binary not yet
49
79
  * listening) — they reset the loop, not throw.
50
80
  */
51
- const wait_for_health = async (url, timeout_ms, is_alive) => {
81
+ const wait_for_health = async (url, timeout_ms, exit_status) => {
52
82
  const deadline = Date.now() + timeout_ms;
53
83
  let last_error;
54
84
  while (Date.now() < deadline) {
55
- if (!is_alive()) {
56
- throw new Error(`backend process exited before becoming healthy${last_error
85
+ const exited = exit_status();
86
+ if (exited !== null) {
87
+ throw new Error(`backend process ${exited} before becoming healthy${last_error
57
88
  ? ` (last probe error: ${last_error.message ?? String(last_error)})`
58
89
  : ''}`);
59
90
  }
60
91
  try {
61
- const response = await fetch(url);
92
+ const request_timeout_ms = Math.max(1, Math.min(HEALTH_PROBE_REQUEST_TIMEOUT_MS, deadline - Date.now()));
93
+ const response = await fetch(url, { signal: AbortSignal.timeout(request_timeout_ms) });
62
94
  if (response.ok) {
63
95
  // Drain the body so the connection can be released back to
64
96
  // the agent pool — unconsumed bodies keep the socket open.
@@ -74,6 +106,57 @@ const wait_for_health = async (url, timeout_ms, is_alive) => {
74
106
  }
75
107
  throw new Error(`health probe to ${url} timed out after ${timeout_ms}ms (last error: ${last_error ? (last_error.message ?? String(last_error)) : 'none'})`);
76
108
  };
109
+ /**
110
+ * Whether something accepts TCP connections at `base_url`'s host and port.
111
+ * A refused connection, an unresolvable host or no answer within
112
+ * `PORT_PROBE_TIMEOUT_MS` all read as free.
113
+ *
114
+ * @param base_url - an `http://` or `https://` URL; a missing port is the scheme's default
115
+ */
116
+ export const is_port_accepting = (base_url) => {
117
+ const url = new URL(base_url);
118
+ const port = Number(url.port || (url.protocol === 'https:' ? 443 : 80));
119
+ // `URL` keeps an IPv6 literal's brackets, which `connect` doesn't take
120
+ const host = url.hostname.replace(/^\[(.*)\]$/, '$1');
121
+ return new Promise((resolve) => {
122
+ const socket = connect({ host, port });
123
+ const settle = (accepting) => {
124
+ socket.destroy();
125
+ resolve(accepting);
126
+ };
127
+ socket.setTimeout(PORT_PROBE_TIMEOUT_MS, () => settle(false));
128
+ socket.once('connect', () => settle(true));
129
+ socket.once('error', () => settle(false));
130
+ });
131
+ };
132
+ /**
133
+ * Create an `OutputTail` keeping at most `max_bytes`.
134
+ *
135
+ * @param max_bytes - how many trailing bytes to keep
136
+ */
137
+ export const create_output_tail = (max_bytes) => {
138
+ let chunks = [];
139
+ let size = 0;
140
+ let dropped = false;
141
+ return {
142
+ push: (chunk) => {
143
+ chunks.push(chunk);
144
+ size += chunk.length;
145
+ if (size <= max_bytes)
146
+ return;
147
+ dropped = true;
148
+ const joined = Buffer.concat(chunks).subarray(size - max_bytes);
149
+ chunks = [joined];
150
+ size = joined.length;
151
+ },
152
+ text: () => {
153
+ const text = Buffer.concat(chunks).toString('utf-8');
154
+ return dropped ? `[earlier output dropped]\n${text}` : text;
155
+ }
156
+ };
157
+ };
158
+ /** `code=N` or `signal=SIG` for a child's exit, as `ChildProcess`'s `'exit'` reports it. */
159
+ const format_exit = (code, signal) => signal !== null ? `signal=${signal}` : `code=${code}`;
77
160
  /**
78
161
  * Process-level cleanup registry — every `spawn_backend` registers its
79
162
  * teardown here so vitest worker death or interactive Ctrl+C kills the
@@ -107,6 +190,9 @@ const install_process_handlers = () => {
107
190
  };
108
191
  process.on('SIGINT', () => passthrough_signal('SIGINT'));
109
192
  process.on('SIGTERM', () => passthrough_signal('SIGTERM'));
193
+ // a closed terminal: without a handler the default action kills the process
194
+ // before `'exit'` runs, and the detached backends would keep their ports
195
+ process.on('SIGHUP', () => passthrough_signal('SIGHUP'));
110
196
  };
111
197
  /**
112
198
  * Read the daemon token file. The file is `{"token": "<value>"}` JSON —
@@ -152,17 +238,31 @@ const read_daemon_token = async (path) => {
152
238
  *
153
239
  * Errors at any stage SIGTERM the child group before rethrowing — the
154
240
  * caller never sees a half-started backend.
241
+ *
242
+ * @throws Error when `config.base_url`'s port already accepts connections,
243
+ * before anything is spawned; when the child exits before it is healthy
244
+ * (with its exit code or signal and stderr tail); or when the health probe
245
+ * or the daemon-token read times out
155
246
  */
156
247
  export const spawn_backend = async (config) => {
157
248
  if (config.start_command.length === 0) {
158
249
  throw new Error(`spawn_backend(${config.name}): start_command is empty`);
159
250
  }
251
+ // a process already on the port would answer the health probe and every
252
+ // request after it, so the run would drive it instead of this spawn
253
+ if (await is_port_accepting(config.base_url)) {
254
+ throw new Error(`spawn_backend(${config.name}): ${config.base_url} already accepts connections — ` +
255
+ 'another cross run or a stale backend?');
256
+ }
160
257
  // Write the bootstrap token file before spawn so the binary reads it
161
258
  // at boot.
162
259
  await mkdir(dirname(config.bootstrap.token_path), { recursive: true });
163
260
  await writeFile(config.bootstrap.token_path, config.bootstrap.token, { mode: 0o600 });
261
+ // a token a previous run left would read as this binary's
262
+ await rm(config.bootstrap.daemon_token_path, { force: true });
164
263
  install_process_handlers();
165
264
  const [command, ...args] = config.start_command;
265
+ const spawned_at = performance.now();
166
266
  const child = spawn(command, args, {
167
267
  env: { ...process.env, ...config.env },
168
268
  // Own process group so SIGTERM to the negative PID tears down the
@@ -170,11 +270,15 @@ export const spawn_backend = async (config) => {
170
270
  detached: true,
171
271
  stdio: ['ignore', 'pipe', 'pipe']
172
272
  });
173
- // Buffer stderr so a startup-time crash surfaces with context.
174
- const stderr_chunks = [];
273
+ // Keep a bounded stderr tail so a crash surfaces with context.
274
+ const stderr_tail = create_output_tail(STDERR_TAIL_MAX_BYTES);
175
275
  child.stderr?.on('data', (chunk) => {
176
- stderr_chunks.push(chunk);
276
+ stderr_tail.push(chunk);
177
277
  });
278
+ const format_stderr_tail = () => {
279
+ const text = stderr_tail.text();
280
+ return text.length > 0 ? `\nstderr:\n${text}` : '';
281
+ };
178
282
  // Drain stdout — discard, but the read is mandatory. `stdio: 'pipe'`
179
283
  // leaves the stream paused until something consumes it; an unread
180
284
  // pipe fills its OS buffer (~64KB pipe / ~208KB AF_UNIX socketpair on
@@ -191,67 +295,122 @@ export const spawn_backend = async (config) => {
191
295
  // operational logging whose unbounded retention would leak across a
192
296
  // long suite.
193
297
  child.stdout?.on('data', () => { });
298
+ // how the child ended: an exit, or a spawn that never produced a process
299
+ // (a command that doesn't exist emits `'error'` and never `'exit'`)
194
300
  let exit_info = null;
301
+ let started = false;
302
+ let teardown_invoked = false;
303
+ // The child leads its own process group (`detached: true`), so its pid is
304
+ // the group id; a descendant stays in the group after the child exits. Once
305
+ // the group is seen empty it is never signalled again: its id is a pid the
306
+ // kernel may hand to an unrelated process, whose new group a later
307
+ // `kill(-pid)` would hit — say a backend that crashed mid-run, torn down
308
+ // minutes later. One window stays open: a crash that left a descendant
309
+ // running keeps the flag false, and if that descendant exits on its own,
310
+ // the next signal goes out before its ESRCH sets the flag. Closing it needs
311
+ // a pidfd, which Node lacks.
312
+ let group_gone = false;
313
+ const signal_group = (signal) => {
314
+ if (child.pid === undefined || group_gone)
315
+ return false;
316
+ try {
317
+ // Negative pid → process group.
318
+ process.kill(-child.pid, signal);
319
+ return true;
320
+ }
321
+ catch (err) {
322
+ const code = err.code;
323
+ // ESRCH: no process is left in the group; EPERM: one is, but not ours
324
+ // to signal
325
+ if (code === 'ESRCH')
326
+ group_gone = true;
327
+ return code === 'EPERM';
328
+ }
329
+ };
195
330
  child.on('exit', (code, signal) => {
196
- exit_info = { code, signal };
331
+ exit_info = { kind: 'exit', code, signal };
332
+ // a crash that left no descendant marks the group gone now, before its
333
+ // id can be reused
334
+ signal_group(0);
335
+ // a startup failure is reported by the throw below; an exit teardown
336
+ // caused is expected
337
+ if (started && !teardown_invoked) {
338
+ console.error(`spawn_backend(${config.name}): backend exited unexpectedly (${format_exit(code, signal)})` +
339
+ format_stderr_tail());
340
+ }
197
341
  });
198
- const is_alive = () => exit_info === null;
199
- let teardown_invoked = false;
342
+ child.on('error', (err) => {
343
+ if (child.pid === undefined) {
344
+ // never spawned — the health wait below reports it
345
+ exit_info ??= { kind: 'spawn_error', message: err.message };
346
+ }
347
+ else {
348
+ // Node emits `'error'` on a spawned child only from `child.kill()` or
349
+ // `child.send()`, neither of which this module calls (it signals the
350
+ // group through `process.kill`), so this branch is not expected to run;
351
+ // it logs rather than recording an exit, since `'exit'` still follows
352
+ console.error(`spawn_backend(${config.name}): ${err.message}`);
353
+ }
354
+ });
355
+ const exit_status = () => {
356
+ if (exit_info === null)
357
+ return null;
358
+ return exit_info.kind === 'spawn_error'
359
+ ? `failed to spawn (${exit_info.message})`
360
+ : `exited (${format_exit(exit_info.code, exit_info.signal)})`;
361
+ };
362
+ /** Whether the child or any process left in its group is still running. */
363
+ const is_group_running = () => child.pid !== undefined && (exit_info === null || signal_group(0));
200
364
  const teardown_sync = () => {
201
365
  if (teardown_invoked)
202
366
  return;
203
367
  teardown_invoked = true;
204
- if (child.pid !== undefined && exit_info === null) {
205
- try {
206
- // Negative pid → process group.
207
- process.kill(-child.pid, 'SIGTERM');
208
- }
209
- catch {
210
- // Already dead; ignore.
211
- }
212
- }
368
+ // the group, not just the child: a descendant outlives a child that has
369
+ // already exited, and would hold the port or the database
370
+ signal_group('SIGTERM');
213
371
  };
214
- const teardown = async () => {
372
+ let teardown_done = null;
373
+ const teardown = () => {
215
374
  teardown_sync();
216
375
  live_teardowns.delete(teardown_sync);
217
- if (exit_info !== null)
218
- return;
219
- // Wait for the child to actually exit so callers can be sure the port
220
- // is free. A backend that ignores SIGTERM — or whose graceful shutdown
221
- // wedges (e.g. a runtime whose `server.stop()` never resolves) — would
222
- // otherwise hang this await forever and strand the run, so escalate to
223
- // SIGKILL on the process group after a grace window. SIGKILL is
224
- // uncatchable, so the child then exits and the `'exit'` listener
225
- // resolves. Defense-in-depth: the per-runtime adapter shutdown is the
226
- // primary fix; this guarantees teardown completes regardless.
227
- await new Promise((resolve) => {
228
- const kill_timer = setTimeout(() => {
229
- if (child.pid !== undefined && exit_info === null) {
230
- try {
231
- // Negative pid → process group.
232
- process.kill(-child.pid, 'SIGKILL');
233
- }
234
- catch {
235
- // Already dead; ignore.
236
- }
237
- }
238
- }, TEARDOWN_SIGKILL_GRACE_MS);
239
- child.once('exit', () => {
240
- clearTimeout(kill_timer);
241
- resolve();
242
- });
243
- });
376
+ return (teardown_done ??= wait_for_group_exit());
377
+ };
378
+ // Wait for the child and every process left in its group to exit, so callers
379
+ // can be sure the port is free. A backend that ignores SIGTERM — or whose
380
+ // graceful shutdown wedges (e.g. a runtime whose `server.stop()` never
381
+ // resolves) — would otherwise hold this wait forever and strand the run, so
382
+ // escalate to SIGKILL on the process group after a grace window. SIGKILL is
383
+ // uncatchable, so the group then exits. Defense-in-depth: the per-runtime
384
+ // adapter shutdown is the primary fix; this guarantees teardown completes
385
+ // regardless — bounded at a second grace window after the SIGKILL, which
386
+ // only a process stuck in the kernel outlasts.
387
+ const wait_for_group_exit = async () => {
388
+ const kill_at = Date.now() + TEARDOWN_SIGKILL_GRACE_MS;
389
+ const give_up_at = kill_at + TEARDOWN_SIGKILL_GRACE_MS;
390
+ let killed = false;
391
+ while (is_group_running()) {
392
+ const now = Date.now();
393
+ if (now >= give_up_at) {
394
+ console.error(`spawn_backend(${config.name}): teardown gave up with the process group still running`);
395
+ return;
396
+ }
397
+ if (!killed && now >= kill_at) {
398
+ killed = true;
399
+ signal_group('SIGKILL');
400
+ }
401
+ await sleep(TEARDOWN_POLL_INTERVAL_MS);
402
+ }
244
403
  };
245
404
  live_teardowns.add(teardown_sync);
246
405
  try {
247
- await wait_for_health(`${config.base_url}${config.health_path}`, config.startup_timeout_ms, is_alive);
406
+ await wait_for_health(`${config.base_url}${config.health_path}`, config.startup_timeout_ms, exit_status);
248
407
  const daemon_token = await read_daemon_token(config.bootstrap.daemon_token_path);
408
+ started = true;
409
+ console.log(`spawn_backend(${config.name}): healthy in ${Math.round(performance.now() - spawned_at)}ms`);
249
410
  return { config, child, daemon_token, teardown };
250
411
  }
251
412
  catch (err) {
252
413
  await teardown();
253
- const stderr_dump = Buffer.concat(stderr_chunks).toString('utf-8');
254
- const stderr_tail = stderr_dump.length > 0 ? `\nstderr:\n${stderr_dump}` : '';
255
- throw new Error(`spawn_backend(${config.name}) failed: ${err.message}${stderr_tail}`);
414
+ throw new Error(`spawn_backend(${config.name}) failed: ${err.message}${format_stderr_tail()}`);
256
415
  }
257
416
  };