@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.
- package/dist/actions/CLAUDE.md +8 -6
- package/dist/actions/action_codegen.d.ts +15 -1
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +23 -2
- package/dist/actions/action_event.d.ts.map +1 -1
- package/dist/actions/action_event.js +14 -1
- package/dist/actions/perform_action.d.ts +17 -1
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +23 -3
- package/dist/auth/CLAUDE.md +3 -2
- package/dist/auth/account_action_specs.d.ts +12 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +16 -2
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +7 -4
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +29 -8
- package/dist/auth/audit_emitter.d.ts +14 -0
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +5 -1
- package/dist/auth/cell_action_specs.d.ts +3 -1
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +5 -5
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +2 -3
- package/dist/auth/cell_data_schema.d.ts +1 -1
- package/dist/auth/cell_data_schema.js +1 -1
- package/dist/auth/cell_field_action_specs.d.ts +4 -3
- package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_field_action_specs.js +5 -6
- package/dist/auth/cell_field_actions.d.ts.map +1 -1
- package/dist/auth/cell_field_actions.js +8 -1
- package/dist/auth/cell_item_action_specs.d.ts +4 -2
- package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_item_action_specs.js +5 -5
- package/dist/auth/cell_item_actions.d.ts.map +1 -1
- package/dist/auth/cell_item_actions.js +8 -1
- package/dist/auth/role_grant_offer_action_specs.d.ts +2 -0
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +8 -4
- package/dist/http/CLAUDE.md +14 -2
- package/dist/http/error_schemas.d.ts +8 -0
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +12 -0
- package/dist/http/jsonrpc_errors.d.ts +40 -15
- package/dist/http/jsonrpc_errors.d.ts.map +1 -1
- package/dist/http/jsonrpc_errors.js +90 -35
- package/dist/primitive_schemas.d.ts +1 -1
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +2 -1
- package/dist/schema_meta.d.ts +80 -0
- package/dist/schema_meta.d.ts.map +1 -1
- package/dist/schema_meta.js +71 -1
- package/dist/testing/CLAUDE.md +853 -146
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +73 -5
- package/dist/testing/adversarial_input.d.ts +0 -47
- package/dist/testing/adversarial_input.d.ts.map +1 -1
- package/dist/testing/adversarial_input.js +4 -340
- package/dist/testing/adversarial_input_cases.d.ts +124 -0
- package/dist/testing/adversarial_input_cases.d.ts.map +1 -0
- package/dist/testing/adversarial_input_cases.js +393 -0
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +24 -20
- package/dist/testing/audit_drift_guard.d.ts +30 -3
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +55 -3
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +29 -12
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
- package/dist/testing/cross_backend/actor_lookup.js +7 -25
- package/dist/testing/cross_backend/app_settings.d.ts.map +1 -1
- package/dist/testing/cross_backend/app_settings.js +4 -6
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts +57 -0
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/audit_cross_helpers.js +89 -0
- package/dist/testing/cross_backend/backend_config.d.ts +2 -1
- package/dist/testing/cross_backend/backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.d.ts +6 -0
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +26 -44
- package/dist/testing/cross_backend/body_size_smuggling.d.ts +5 -0
- package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size_smuggling.js +7 -14
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts +3 -1
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.d.ts +78 -59
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +25 -24
- package/dist/testing/cross_backend/cargo_binary.d.ts +70 -0
- package/dist/testing/cross_backend/cargo_binary.d.ts.map +1 -0
- package/dist/testing/cross_backend/cargo_binary.js +148 -0
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +174 -3
- package/dist/testing/cross_backend/cell_gated_create.d.ts +0 -60
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +9 -487
- package/dist/testing/cross_backend/cell_grant_role.d.ts +14 -2
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.js +22 -12
- package/dist/testing/cross_backend/cell_moderate.d.ts +66 -0
- package/dist/testing/cross_backend/cell_moderate.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_moderate.js +511 -0
- package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_ref_admission.js +1 -1
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +40 -19
- package/dist/testing/cross_backend/conformance_case.d.ts +43 -4
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +29 -1
- package/dist/testing/cross_backend/conformance_rpc_auth.d.ts +93 -0
- package/dist/testing/cross_backend/conformance_rpc_auth.d.ts.map +1 -0
- package/dist/testing/cross_backend/conformance_rpc_auth.js +125 -0
- package/dist/testing/cross_backend/conformance_table.d.ts +144 -4
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +297 -65
- package/dist/testing/cross_backend/cookie_attributes.d.ts.map +1 -1
- package/dist/testing/cross_backend/cookie_attributes.js +3 -2
- package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts +38 -5
- package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/create_cross_backend_global_setup.js +24 -3
- package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts +5 -5
- package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_deadline.d.ts +10 -6
- package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_deadline.js +32 -31
- package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_header_robustness.js +4 -6
- package/dist/testing/cross_backend/{cell_cross_helpers.d.ts → cross_rpc.d.ts} +14 -2
- package/dist/testing/cross_backend/cross_rpc.d.ts.map +1 -0
- package/dist/testing/cross_backend/{cell_cross_helpers.js → cross_rpc.js} +31 -7
- package/dist/testing/cross_backend/default_backend_configs.d.ts +58 -61
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +97 -91
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +2 -2
- package/dist/testing/cross_backend/differential.d.ts +126 -0
- package/dist/testing/cross_backend/differential.d.ts.map +1 -0
- package/dist/testing/cross_backend/differential.js +201 -0
- package/dist/testing/cross_backend/differential_normalize.d.ts +150 -0
- package/dist/testing/cross_backend/differential_normalize.d.ts.map +1 -0
- package/dist/testing/cross_backend/differential_normalize.js +153 -0
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/fact_serving.js +27 -17
- package/dist/testing/cross_backend/full_spine_mount.d.ts +15 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +55 -1
- package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/identity_parity.js +6 -5
- package/dist/testing/cross_backend/in_process_setup.d.ts +12 -2
- package/dist/testing/cross_backend/in_process_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/in_process_setup.js +10 -7
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +13 -18
- package/dist/testing/cross_backend/origin.js +1 -1
- package/dist/testing/cross_backend/origin_conformance.js +1 -1
- package/dist/testing/cross_backend/origin_scope.d.ts +6 -0
- package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin_scope.js +13 -12
- package/dist/testing/cross_backend/peer_ping_ws.d.ts +2 -5
- package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.js +14 -24
- package/dist/testing/cross_backend/postgres_database.d.ts +85 -0
- package/dist/testing/cross_backend/postgres_database.d.ts.map +1 -0
- package/dist/testing/cross_backend/postgres_database.js +140 -0
- package/dist/testing/cross_backend/provided_context.d.ts +28 -0
- package/dist/testing/cross_backend/provided_context.d.ts.map +1 -0
- package/dist/testing/cross_backend/provided_context.js +35 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/query_shape.js +10 -12
- package/dist/testing/cross_backend/ready.js +2 -2
- package/dist/testing/cross_backend/revocation_drivers.d.ts +4 -3
- package/dist/testing/cross_backend/revocation_drivers.d.ts.map +1 -1
- package/dist/testing/cross_backend/revocation_drivers.js +1 -1
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +3 -2
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts +13 -0
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.js +203 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +2 -5
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +203 -111
- package/dist/testing/cross_backend/role_grant_participation.d.ts +13 -0
- package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_participation.js +32 -21
- package/dist/testing/cross_backend/role_grant_role_grammar.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_role_grammar.js +6 -9
- package/dist/testing/cross_backend/rpc_adversarial.d.ts +47 -0
- package/dist/testing/cross_backend/rpc_adversarial.d.ts.map +1 -0
- package/dist/testing/cross_backend/rpc_adversarial.js +302 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.d.ts +24 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.d.ts.map +1 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.js +124 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts +101 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts.map +1 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.js +113 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +10 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +33 -21
- package/dist/testing/cross_backend/session_cap.d.ts +9 -8
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/session_cap.js +9 -4
- package/dist/testing/cross_backend/session_cookie_codec.js +1 -1
- package/dist/testing/cross_backend/setup.d.ts +58 -56
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +29 -29
- package/dist/testing/cross_backend/simple_request.d.ts.map +1 -1
- package/dist/testing/cross_backend/simple_request.js +4 -4
- package/dist/testing/cross_backend/spawn_backend.d.ts +59 -10
- package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -1
- package/dist/testing/cross_backend/spawn_backend.js +219 -60
- package/dist/testing/cross_backend/spine_suites.d.ts +311 -0
- package/dist/testing/cross_backend/spine_suites.d.ts.map +1 -0
- package/dist/testing/cross_backend/spine_suites.js +897 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +44 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +53 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +3 -4
- package/dist/testing/cross_backend/standard.d.ts +18 -1
- package/dist/testing/cross_backend/standard.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.js +2 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +24 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +15 -0
- package/dist/testing/cross_backend/testing_backdoor.d.ts +29 -12
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +143 -51
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +30 -14
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +58 -37
- package/dist/testing/cross_backend/testing_server_bun.js +1 -1
- package/dist/testing/cross_backend/token_lifetime.js +1 -1
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +9 -13
- package/dist/testing/cross_backend/transport_composition.d.ts +26 -2
- package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -1
- package/dist/testing/cross_backend/transport_composition.js +81 -51
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +8 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +23 -13
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts +2 -5
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_action_rate_limit.js +8 -14
- package/dist/testing/cross_backend/ws_connection_cap.d.ts +4 -9
- package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_connection_cap.js +11 -12
- package/dist/testing/cross_backend/ws_endpoint.d.ts +52 -0
- package/dist/testing/cross_backend/ws_endpoint.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_endpoint.js +54 -0
- package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts +2 -5
- package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_inbound_dispatch.js +9 -15
- package/dist/testing/cross_backend/ws_outbound_overflow.d.ts +2 -7
- package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_outbound_overflow.js +5 -9
- package/dist/testing/cross_backend/ws_role_gated.d.ts +4 -5
- package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_role_gated.js +12 -21
- package/dist/testing/cross_backend/ws_round_trip.d.ts +12 -9
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +29 -34
- package/dist/testing/db_entities.d.ts +16 -3
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +28 -5
- package/dist/testing/integration_helpers.d.ts +61 -2
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +75 -18
- package/dist/testing/round_trip.d.ts +17 -0
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +67 -24
- package/dist/testing/rpc_attack_surface.d.ts +10 -0
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +27 -140
- package/dist/testing/rpc_envelope_cases.d.ts +114 -0
- package/dist/testing/rpc_envelope_cases.d.ts.map +1 -0
- package/dist/testing/rpc_envelope_cases.js +243 -0
- package/dist/testing/rpc_helpers.d.ts +31 -0
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +35 -14
- package/dist/testing/rpc_round_trip.d.ts +62 -6
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +151 -39
- package/dist/testing/schema_generators.d.ts +99 -2
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +261 -73
- package/dist/testing/standard.d.ts +26 -0
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +4 -1
- package/dist/testing/stubs.d.ts +4 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +6 -4
- package/dist/testing/transports/raw_http.d.ts +21 -0
- package/dist/testing/transports/raw_http.d.ts.map +1 -1
- package/dist/testing/transports/raw_http.js +30 -0
- package/dist/zod.d.ts +9 -0
- package/dist/zod.d.ts.map +1 -1
- package/dist/zod.js +20 -0
- package/package.json +6 -4
- package/dist/testing/cross_backend/actor_search.d.ts +0 -6
- package/dist/testing/cross_backend/actor_search.d.ts.map +0 -1
- package/dist/testing/cross_backend/actor_search.js +0 -90
- 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.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 +
|
|
14
|
-
* group teardown so ports never strand.
|
|
15
|
-
*
|
|
16
|
-
* `startup_timeout_ms` elapses
|
|
17
|
-
*
|
|
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,
|
|
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
|
-
|
|
56
|
-
|
|
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
|
|
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
|
-
//
|
|
174
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
372
|
+
let teardown_done = null;
|
|
373
|
+
const teardown = () => {
|
|
215
374
|
teardown_sync();
|
|
216
375
|
live_teardowns.delete(teardown_sync);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
};
|