@fuzdev/fuz_app 0.65.0 → 0.67.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 +65 -86
- package/dist/actions/action_codegen.d.ts +1 -1
- package/dist/actions/action_codegen.js +1 -1
- package/dist/actions/action_event_data.d.ts +1 -1
- package/dist/auth/CLAUDE.md +83 -104
- package/dist/auth/audit_log_schema.js +2 -2
- package/dist/auth/daemon_token_middleware.d.ts +15 -5
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +24 -15
- package/dist/auth/invite_queries.d.ts +17 -7
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +19 -8
- package/dist/auth/signup_routes.d.ts +47 -1
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +103 -52
- package/dist/env/resolve.d.ts +44 -7
- package/dist/env/resolve.d.ts.map +1 -1
- package/dist/env/resolve.js +94 -27
- package/dist/http/CLAUDE.md +47 -52
- package/dist/http/jsonrpc.d.ts +23 -7
- package/dist/http/jsonrpc.d.ts.map +1 -1
- package/dist/http/jsonrpc.js +19 -3
- package/dist/http/surface.d.ts +9 -2
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/runtime/mock.d.ts +1 -1
- package/dist/runtime/mock.js +1 -1
- package/dist/testing/CLAUDE.md +659 -511
- package/dist/testing/admin_integration.d.ts +5 -5
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +95 -39
- package/dist/testing/app_server.d.ts +16 -1
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +18 -3
- package/dist/testing/audit_completeness.d.ts +7 -5
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +5 -9
- package/dist/testing/bootstrap_success.js +2 -2
- package/dist/testing/cross_backend/backend_config.d.ts +113 -0
- package/dist/testing/cross_backend/backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/backend_config.js +1 -0
- package/dist/testing/cross_backend/bench/bench_report.d.ts +46 -0
- package/dist/testing/cross_backend/bench/bench_report.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/bench_report.js +83 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +44 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +38 -0
- package/dist/testing/cross_backend/bench/scenario.d.ts +57 -0
- package/dist/testing/cross_backend/bench/scenario.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/scenario.js +28 -0
- package/dist/testing/cross_backend/bootstrap_backend.d.ts +41 -0
- package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -0
- package/dist/testing/cross_backend/bootstrap_backend.js +34 -0
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts +24 -0
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -0
- package/dist/testing/cross_backend/build_test_backend_paths.js +33 -0
- package/dist/testing/cross_backend/capabilities.d.ts +3 -2
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.d.ts +122 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_backend_configs.js +111 -0
- package/dist/testing/cross_backend/default_secrets.d.ts +40 -0
- package/dist/testing/cross_backend/default_secrets.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_secrets.js +39 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts +64 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_spine_surface.js +121 -0
- package/dist/testing/cross_backend/setup.d.ts +270 -34
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +495 -15
- package/dist/testing/cross_backend/spawn_backend.d.ts +58 -0
- package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -0
- package/dist/testing/cross_backend/spawn_backend.js +229 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.d.ts +66 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.js +49 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts +37 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -0
- package/dist/testing/cross_backend/sse_round_trip.js +137 -0
- package/dist/testing/cross_backend/standard.d.ts +96 -0
- package/dist/testing/cross_backend/standard.d.ts.map +1 -0
- package/dist/testing/cross_backend/standard.js +49 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +171 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_reset_actions.js +213 -0
- package/dist/testing/cross_backend/testing_server_bun.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_bun.js +59 -0
- package/dist/testing/cross_backend/testing_server_core.d.ts +140 -0
- package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_core.js +68 -0
- package/dist/testing/cross_backend/testing_server_deno.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_deno.js +37 -0
- package/dist/testing/cross_backend/testing_server_node.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_node.js +50 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +72 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.js +112 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +35 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_round_trip.js +113 -0
- package/dist/testing/data_exposure.d.ts +4 -6
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +1 -5
- package/dist/testing/db_entities.d.ts +18 -7
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +18 -7
- package/dist/testing/integration.d.ts +27 -6
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +93 -58
- package/dist/testing/round_trip.d.ts +4 -5
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +1 -5
- package/dist/testing/rpc_helpers.d.ts +10 -4
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +1 -1
- package/dist/testing/rpc_round_trip.d.ts +5 -5
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +1 -5
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +1 -68
- package/dist/testing/standard.d.ts +4 -5
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/stubs.d.ts +10 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +9 -2
- package/dist/testing/testing_rate_limiter.d.ts +59 -0
- package/dist/testing/testing_rate_limiter.d.ts.map +1 -0
- package/dist/testing/testing_rate_limiter.js +74 -0
- package/dist/testing/transports/bootstrap.d.ts +52 -0
- package/dist/testing/transports/bootstrap.d.ts.map +1 -0
- package/dist/testing/transports/bootstrap.js +70 -0
- package/dist/testing/transports/fetch_transport.d.ts +81 -0
- package/dist/testing/transports/fetch_transport.d.ts.map +1 -0
- package/dist/testing/transports/fetch_transport.js +74 -0
- package/dist/testing/transports/sse_frame_reader.d.ts +41 -0
- package/dist/testing/transports/sse_frame_reader.d.ts.map +1 -0
- package/dist/testing/transports/sse_frame_reader.js +84 -0
- package/dist/testing/transports/sse_transport.d.ts +54 -0
- package/dist/testing/transports/sse_transport.d.ts.map +1 -0
- package/dist/testing/transports/sse_transport.js +51 -0
- package/dist/testing/transports/ws_client.d.ts +108 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -0
- package/dist/testing/transports/ws_client.js +56 -0
- package/dist/testing/transports/ws_transport.d.ts +43 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -0
- package/dist/testing/transports/ws_transport.js +169 -0
- package/dist/testing/ws_round_trip.d.ts +21 -103
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +42 -40
- package/dist/ui/CLAUDE.md +5 -3
- package/dist/ui/MenuLink.svelte +16 -16
- package/dist/ui/MenuLink.svelte.d.ts +13 -4
- package/dist/ui/MenuLink.svelte.d.ts.map +1 -1
- package/package.json +20 -3
- package/dist/testing/transports/surface_source.d.ts +0 -51
- package/dist/testing/transports/surface_source.d.ts.map +0 -1
- package/dist/testing/transports/surface_source.js +0 -19
|
@@ -1,9 +1,70 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Per-test fixture protocol shared by in-process and cross-process
|
|
4
|
+
* transports.
|
|
5
|
+
*
|
|
6
|
+
* Each standard suite body takes a required
|
|
7
|
+
* `setup_test: () => Promise<TestFixture>` callback and invokes it once
|
|
8
|
+
* per test. The fixture carries everything a test needs to fire requests
|
|
9
|
+
* and assert on a single bootstrapped keeper account — transport,
|
|
10
|
+
* account / actor identity, three header builders, a multi-account mint
|
|
11
|
+
* factory, and (in-process only) the in-memory keyring + raw backend.
|
|
12
|
+
*
|
|
13
|
+
* `default_in_process_setup(options)` wraps `create_test_app` into the
|
|
14
|
+
* `SetupTest` contract. The cross-process sibling
|
|
15
|
+
* (`default_cross_process_setup`) lands alongside the spawn-a-backend
|
|
16
|
+
* transport plumbing — it implements the same contract by spawning a
|
|
17
|
+
* binary and bootstrapping over real HTTP.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
import { Uuid } from '@fuzdev/fuz_util/id.js';
|
|
2
23
|
import { ROLE_KEEPER } from '../../auth/role_schema.js';
|
|
3
|
-
import {
|
|
24
|
+
import { DAEMON_TOKEN_HEADER } from '../../auth/daemon_token.js';
|
|
25
|
+
import { USERNAME_LENGTH_MAX } from '../../primitive_schemas.js';
|
|
26
|
+
import { create_test_app, create_test_account_with_credentials, DEFAULT_TEST_PASSWORD, } from '../app_server.js';
|
|
4
27
|
import { create_test_app_surface_spec } from '../stubs.js';
|
|
5
28
|
import { http_transport, } from '../rpc_helpers.js';
|
|
6
29
|
import { in_process_capabilities } from './capabilities.js';
|
|
30
|
+
import { create_fetch_transport } from '../transports/fetch_transport.js';
|
|
31
|
+
/**
|
|
32
|
+
* Build an `ExtraAccountFixture` from a seeded `{account, actor,
|
|
33
|
+
* api_token, session_cookie}` bundle and the session cookie name.
|
|
34
|
+
*
|
|
35
|
+
* Same shape produced by either path that seeds bootstrap-time
|
|
36
|
+
* secondaries: in-process via `create_test_account_with_credentials`
|
|
37
|
+
* against the live backend's DB, or cross-process via the
|
|
38
|
+
* `_testing_reset` RPC's `extra_accounts` output. Both call this
|
|
39
|
+
* helper so the fixture-side header builders + field plumbing stays
|
|
40
|
+
* in one place.
|
|
41
|
+
*/
|
|
42
|
+
const build_extra_account_fixture = (seeded, cookie_name) => ({
|
|
43
|
+
account: seeded.account,
|
|
44
|
+
actor: seeded.actor,
|
|
45
|
+
api_token: seeded.api_token,
|
|
46
|
+
session_cookie: seeded.session_cookie,
|
|
47
|
+
create_session_headers: (extra) => ({
|
|
48
|
+
cookie: `${cookie_name}=${seeded.session_cookie}`,
|
|
49
|
+
...extra,
|
|
50
|
+
}),
|
|
51
|
+
create_bearer_headers: (extra) => ({
|
|
52
|
+
authorization: `Bearer ${seeded.api_token}`,
|
|
53
|
+
...extra,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Wrap a Hono-style app into a `FetchTransport`-shaped object so the
|
|
58
|
+
* shared `TestFixtureBase.transport` type holds for both in-process and
|
|
59
|
+
* cross-process setups. In-process has no real cookie jar — the no-op
|
|
60
|
+
* `cookies()` returns `[]`; in-process tests build cookies via
|
|
61
|
+
* `fixture.create_session_headers()` instead.
|
|
62
|
+
*/
|
|
63
|
+
const in_process_fetch_transport = (app) => {
|
|
64
|
+
const call = http_transport(app);
|
|
65
|
+
const transport = ((url, init) => call(url, init));
|
|
66
|
+
return Object.assign(transport, { cookies: () => [] });
|
|
67
|
+
};
|
|
7
68
|
/**
|
|
8
69
|
* Build a `SetupTest` that creates a fresh `TestApp` per call via
|
|
9
70
|
* `create_test_app` and projects it into the `TestFixture` shape.
|
|
@@ -11,7 +72,12 @@ import { in_process_capabilities } from './capabilities.js';
|
|
|
11
72
|
* Same factory inputs `create_test_app` already takes — this helper
|
|
12
73
|
* is a projection layer, not a new lifecycle. fuz_app's own `src/test/`
|
|
13
74
|
* and consumer suites pass `default_in_process_setup({...factory_inputs})`
|
|
14
|
-
* in place of the old per-suite factory-input bundle.
|
|
75
|
+
* in place of the old per-suite factory-input bundle. The `extra_accounts`
|
|
76
|
+
* slot (see `InProcessSetupOptions`) seeds bootstrap-time secondaries
|
|
77
|
+
* directly via `create_test_account_with_credentials` against the same
|
|
78
|
+
* DB the keeper just landed on — mirrors the cross-process
|
|
79
|
+
* `_testing_reset` cradle so suite bodies read
|
|
80
|
+
* `fixture.extra_accounts[username]` uniformly regardless of transport.
|
|
15
81
|
*
|
|
16
82
|
* The describe-level `auth_integration_truncate_tables` / pglite WASM
|
|
17
83
|
* cache lifecycle stays in `create_pglite_factory` / `create_describe_db`
|
|
@@ -20,19 +86,435 @@ import { in_process_capabilities } from './capabilities.js';
|
|
|
20
86
|
*/
|
|
21
87
|
export const default_in_process_setup = (options) => async () => {
|
|
22
88
|
const test_app = await create_test_app(options);
|
|
89
|
+
// Seed bootstrap-time secondaries against the same DB the keeper
|
|
90
|
+
// just landed on. Direct-insert is the only path for roles whose
|
|
91
|
+
// `grant_paths` excludes `'admin'` (e.g. `ROLE_KEEPER`) — see
|
|
92
|
+
// `ExtraAccountSpec` for why this bypass is bootstrap-cradle-only.
|
|
93
|
+
const extra_accounts = {};
|
|
94
|
+
const { cookie_name } = options.session_options;
|
|
95
|
+
for (const spec of options.extra_accounts ?? []) {
|
|
96
|
+
const seeded = await create_test_account_with_credentials({
|
|
97
|
+
db: test_app.backend.deps.db,
|
|
98
|
+
keyring: test_app.backend.keyring,
|
|
99
|
+
session_options: options.session_options,
|
|
100
|
+
password: test_app.backend.deps.password,
|
|
101
|
+
username: spec.username,
|
|
102
|
+
password_value: spec.password_value,
|
|
103
|
+
roles: [...spec.roles],
|
|
104
|
+
});
|
|
105
|
+
extra_accounts[spec.username] = build_extra_account_fixture(seeded, cookie_name);
|
|
106
|
+
}
|
|
23
107
|
return {
|
|
24
108
|
in_process: true,
|
|
25
|
-
transport:
|
|
109
|
+
transport: in_process_fetch_transport(test_app.app),
|
|
110
|
+
// In-process the wrapper is stateless and never auto-adds Origin —
|
|
111
|
+
// `options` is accepted for API symmetry with cross-process but
|
|
112
|
+
// has no observable effect.
|
|
113
|
+
fresh_transport: () => in_process_fetch_transport(test_app.app),
|
|
26
114
|
account: test_app.backend.account,
|
|
27
115
|
actor: test_app.backend.actor,
|
|
28
116
|
create_session_headers: test_app.create_session_headers,
|
|
29
117
|
create_bearer_headers: test_app.create_bearer_headers,
|
|
30
118
|
create_daemon_token_headers: test_app.create_daemon_token_headers,
|
|
31
119
|
create_account: test_app.create_account,
|
|
120
|
+
extra_accounts,
|
|
32
121
|
keyring: test_app.backend.keyring,
|
|
33
122
|
backend_internals: test_app.backend,
|
|
34
123
|
};
|
|
35
124
|
};
|
|
125
|
+
/**
|
|
126
|
+
* Strip the non-serializable members so the result can be passed to
|
|
127
|
+
* vitest's `project.provide`. Call in `globalSetup` before provide.
|
|
128
|
+
*/
|
|
129
|
+
export const serialize_bootstrapped_handle = (handle) => ({
|
|
130
|
+
config: handle.config,
|
|
131
|
+
daemon_token: handle.daemon_token,
|
|
132
|
+
keeper_account: handle.keeper_account,
|
|
133
|
+
keeper_actor: handle.keeper_actor,
|
|
134
|
+
keeper_cookies: [...handle.keeper_cookies],
|
|
135
|
+
});
|
|
136
|
+
/**
|
|
137
|
+
* Rebuild a usable handle from the serialized subset. Synthesizes a
|
|
138
|
+
* fresh {@link FetchTransport} primed with the keeper's `Set-Cookie`
|
|
139
|
+
* values so `_testing_reset` and other keeper-authenticated calls work.
|
|
140
|
+
* The returned shape omits `child` and `teardown` — lifecycle stays
|
|
141
|
+
* with `globalSetup`; tests that try to teardown themselves wouldn't
|
|
142
|
+
* have a serializable reference anyway.
|
|
143
|
+
*/
|
|
144
|
+
export const reconstruct_bootstrapped_handle = (serialized) => ({
|
|
145
|
+
config: serialized.config,
|
|
146
|
+
daemon_token: serialized.daemon_token,
|
|
147
|
+
keeper_account: serialized.keeper_account,
|
|
148
|
+
keeper_actor: serialized.keeper_actor,
|
|
149
|
+
keeper_cookies: serialized.keeper_cookies,
|
|
150
|
+
keeper_transport: create_fetch_transport({
|
|
151
|
+
base_url: serialized.config.base_url,
|
|
152
|
+
initial_cookies: serialized.keeper_cookies,
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
/**
|
|
156
|
+
* Structural subset of `SignupOutput` the runner cares about. Looser
|
|
157
|
+
* than the canonical `auth/signup_routes.ts` schema — kept local so this
|
|
158
|
+
* module doesn't pull the full auth-domain schema into its dep graph.
|
|
159
|
+
*/
|
|
160
|
+
const SignupResponseShape = z.object({
|
|
161
|
+
ok: z.literal(true),
|
|
162
|
+
account: z.object({ id: Uuid, username: z.string() }),
|
|
163
|
+
actor: z.object({ id: Uuid }),
|
|
164
|
+
});
|
|
165
|
+
/** Structural subset of `account_token_create`'s output. */
|
|
166
|
+
const TokenCreateResponseShape = z.object({
|
|
167
|
+
token: z.string(),
|
|
168
|
+
id: z.string(),
|
|
169
|
+
});
|
|
170
|
+
/**
|
|
171
|
+
* Per-test username generator for the *default* `fixture.create_account()`
|
|
172
|
+
* call shape (no caller-supplied username). PID + timestamp + counter
|
|
173
|
+
* keeps the generated username unique across vitest workers, parallel
|
|
174
|
+
* suites within one worker, and reruns of the same suite. The suffix
|
|
175
|
+
* is base36-encoded to stay compact; long prefixes are truncated so
|
|
176
|
+
* the total never exceeds `USERNAME_LENGTH_MAX` (39).
|
|
177
|
+
*
|
|
178
|
+
* Caller-supplied usernames pass through *as-is* now that fresh-keeper-
|
|
179
|
+
* per-test wipes the DB between tests — hardcoded names work and tests
|
|
180
|
+
* can reference accounts by their literal name.
|
|
181
|
+
*/
|
|
182
|
+
let username_counter = 0;
|
|
183
|
+
const PID_BASE36 = process.pid.toString(36);
|
|
184
|
+
const generate_default_username = () => {
|
|
185
|
+
const suffix = `_${PID_BASE36}_${Date.now().toString(36)}_${(++username_counter).toString(36)}`;
|
|
186
|
+
const max_prefix = USERNAME_LENGTH_MAX - suffix.length;
|
|
187
|
+
const prefix = 'test_user';
|
|
188
|
+
const safe_prefix = prefix.length > max_prefix ? prefix.slice(0, max_prefix) : prefix;
|
|
189
|
+
return `${safe_prefix}${suffix}`;
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* POST a JSON-RPC call via the supplied transport and return the raw
|
|
193
|
+
* `result` field. Throws with a labeled error on HTTP failure or RPC
|
|
194
|
+
* error envelope. Used by the cross-process setup harness for
|
|
195
|
+
* keeper-driven and per-test RPC plumbing — the setup module talks to
|
|
196
|
+
* the running backend at the wire level rather than via the
|
|
197
|
+
* spec-driven `rpc_call_for_spec` because each call is internal to the
|
|
198
|
+
* harness and doesn't need spec-shape validation (callers narrow the
|
|
199
|
+
* `unknown` result against the shape they expect).
|
|
200
|
+
*
|
|
201
|
+
* `extra_headers` covers the daemon-token auth case for
|
|
202
|
+
* `_testing_reset`; `Content-Type: application/json` is always set.
|
|
203
|
+
* The JSON-RPC `id` mirrors `method` so server-side logs correlate
|
|
204
|
+
* cleanly.
|
|
205
|
+
*/
|
|
206
|
+
const rpc_via_transport = async (transport, rpc_path, method, params, backend_name, extra_headers) => {
|
|
207
|
+
const response = await transport(rpc_path, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json', ...extra_headers },
|
|
210
|
+
body: JSON.stringify({ jsonrpc: '2.0', method, params, id: method }),
|
|
211
|
+
});
|
|
212
|
+
if (!response.ok) {
|
|
213
|
+
const body = await response.text().catch(() => '<unreadable>');
|
|
214
|
+
throw new Error(`${method}(${backend_name}) HTTP failed: status=${response.status} body=${body}`);
|
|
215
|
+
}
|
|
216
|
+
const raw = (await response.json());
|
|
217
|
+
if (raw.error) {
|
|
218
|
+
throw new Error(`${method}(${backend_name}) RPC error: ${JSON.stringify(raw.error)}`);
|
|
219
|
+
}
|
|
220
|
+
return raw.result;
|
|
221
|
+
};
|
|
222
|
+
/** Structural subset of `_testing_reset`'s output. */
|
|
223
|
+
const TestingResetResponseShape = z.object({
|
|
224
|
+
account: z.object({ id: Uuid, username: z.string() }),
|
|
225
|
+
actor: z.object({ id: Uuid }),
|
|
226
|
+
api_token: z.string(),
|
|
227
|
+
session_cookie: z.string(),
|
|
228
|
+
extra_accounts: z.array(z.object({
|
|
229
|
+
account: z.object({ id: Uuid, username: z.string() }),
|
|
230
|
+
actor: z.object({ id: Uuid }),
|
|
231
|
+
api_token: z.string(),
|
|
232
|
+
session_cookie: z.string(),
|
|
233
|
+
})),
|
|
234
|
+
});
|
|
235
|
+
/**
|
|
236
|
+
* Fire the `_testing_reset` RPC action over the keeper's daemon-token
|
|
237
|
+
* channel. Wipes the DB, re-seeds a fresh keeper (with any
|
|
238
|
+
* `extra_keeper_roles`), and seeds any caller-requested
|
|
239
|
+
* `extra_accounts`. Returns the new credentials so the per-test fixture
|
|
240
|
+
* can close over them.
|
|
241
|
+
*/
|
|
242
|
+
const fire_testing_reset = async (handle, options) => {
|
|
243
|
+
const raw = await rpc_via_transport(handle.keeper_transport, handle.config.rpc_path, '_testing_reset', {
|
|
244
|
+
extra_keeper_roles: options.extra_keeper_roles ?? [],
|
|
245
|
+
extra_accounts: (options.extra_accounts ?? []).map((spec) => ({
|
|
246
|
+
username: spec.username,
|
|
247
|
+
...(spec.password_value !== undefined && { password_value: spec.password_value }),
|
|
248
|
+
roles: [...spec.roles],
|
|
249
|
+
})),
|
|
250
|
+
}, handle.config.name, { [DAEMON_TOKEN_HEADER]: handle.daemon_token });
|
|
251
|
+
const parsed = TestingResetResponseShape.safeParse(raw);
|
|
252
|
+
if (!parsed.success) {
|
|
253
|
+
throw new Error(`_testing_reset(${handle.config.name}) returned unexpected result: ${JSON.stringify(raw)} (${parsed.error.message})`);
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
keeper: {
|
|
257
|
+
account: parsed.data.account,
|
|
258
|
+
actor: parsed.data.actor,
|
|
259
|
+
api_token: parsed.data.api_token,
|
|
260
|
+
session_cookie: parsed.data.session_cookie,
|
|
261
|
+
},
|
|
262
|
+
extra_accounts: parsed.data.extra_accounts,
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* Extract the named cookie's value from `transport.cookies()`. The jar
|
|
267
|
+
* stores `name=value` heads; this peels the value side for the named
|
|
268
|
+
* cookie. Throws when the cookie is missing — every authenticated
|
|
269
|
+
* mint should land one in the jar, so absence is a setup bug.
|
|
270
|
+
*/
|
|
271
|
+
const extract_cookie_value = (transport, cookie_name, backend_name) => {
|
|
272
|
+
for (const raw of transport.cookies()) {
|
|
273
|
+
const eq = raw.indexOf('=');
|
|
274
|
+
if (eq <= 0)
|
|
275
|
+
continue;
|
|
276
|
+
if (raw.slice(0, eq).trim() === cookie_name) {
|
|
277
|
+
return raw.slice(eq + 1);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
throw new Error(`session cookie '${cookie_name}' missing from ${backend_name} transport jar after auth — ` +
|
|
281
|
+
`got ${JSON.stringify(transport.cookies())}`);
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* Mint an account via invite-gated `POST /signup` + `POST /login` on a
|
|
285
|
+
* fresh `FetchTransport`, then create an API token via the
|
|
286
|
+
* `account_token_create` RPC so the returned account has both session
|
|
287
|
+
* + bearer credentials.
|
|
288
|
+
*
|
|
289
|
+
* The keeper (admin via bootstrap, holds both `ROLE_KEEPER` +
|
|
290
|
+
* `ROLE_ADMIN`) creates a username-scoped invite via `invite_create`
|
|
291
|
+
* RPC; signup claims the invite atomically. Lets the cross-process
|
|
292
|
+
* harness stay on the production `open_signup: false` default —
|
|
293
|
+
* mirroring real-user signup semantics rather than synthetically
|
|
294
|
+
* opening signup for the duration of the suite.
|
|
295
|
+
*
|
|
296
|
+
* Signup and login both fire so the per-test fixture exercises both
|
|
297
|
+
* production code paths — signup mints the account + initial session;
|
|
298
|
+
* login replaces the cookie with a fresh one (so any login-specific
|
|
299
|
+
* post-conditions hold). See §Open Q10 for the design rationale.
|
|
300
|
+
*/
|
|
301
|
+
const mint_account = async (handle, options) => {
|
|
302
|
+
const transport = create_fetch_transport({ base_url: handle.config.base_url });
|
|
303
|
+
// Caller-supplied usernames pass through as-is — fresh-keeper-per-test
|
|
304
|
+
// wipes the DB between tests, so hardcoded names (e.g. `'eve_attacker'`,
|
|
305
|
+
// `'user_two'`) don't collide. Default to a unique generated name when
|
|
306
|
+
// the caller doesn't care.
|
|
307
|
+
const username = options.username ?? generate_default_username();
|
|
308
|
+
// Use the shared `DEFAULT_TEST_PASSWORD` so the cross-process bootstrap
|
|
309
|
+
// can never drift from the in-process default — the integration suite's
|
|
310
|
+
// hardcoded login bodies also import the same constant, so a future
|
|
311
|
+
// divergence becomes a typecheck miss instead of a runtime password
|
|
312
|
+
// mismatch (which previously silently 401'd ~20 login tests).
|
|
313
|
+
const password = options.password_value ?? DEFAULT_TEST_PASSWORD;
|
|
314
|
+
// Keeper creates a username-scoped invite so the signup below can claim
|
|
315
|
+
// it. The keeper holds `ROLE_ADMIN` from bootstrap (see
|
|
316
|
+
// `bootstrap_account.ts` — both `ROLE_KEEPER` and `ROLE_ADMIN` grants
|
|
317
|
+
// are created in the bootstrap transaction), so `invite_create` (admin-
|
|
318
|
+
// only) authorizes without any extra grants.
|
|
319
|
+
const invite_result = (await rpc_via_transport(handle.keeper_transport, handle.config.rpc_path, 'invite_create', { username }, handle.config.name));
|
|
320
|
+
if (!invite_result?.invite?.id) {
|
|
321
|
+
throw new Error(`invite_create(${handle.config.name}, username=${username}) returned unexpected result: ` +
|
|
322
|
+
JSON.stringify(invite_result));
|
|
323
|
+
}
|
|
324
|
+
const signup_response = await transport('/api/account/signup', {
|
|
325
|
+
method: 'POST',
|
|
326
|
+
headers: { 'Content-Type': 'application/json' },
|
|
327
|
+
body: JSON.stringify({ username, password }),
|
|
328
|
+
});
|
|
329
|
+
if (!signup_response.ok) {
|
|
330
|
+
const body = await signup_response.text().catch(() => '<unreadable>');
|
|
331
|
+
throw new Error(`signup(${handle.config.name}) failed: status=${signup_response.status} body=${body}`);
|
|
332
|
+
}
|
|
333
|
+
const signup_raw = await signup_response.json();
|
|
334
|
+
const parsed = SignupResponseShape.safeParse(signup_raw);
|
|
335
|
+
if (!parsed.success) {
|
|
336
|
+
throw new Error(`signup(${handle.config.name}) returned unexpected body: ${JSON.stringify(signup_raw)} (${parsed.error.message})`);
|
|
337
|
+
}
|
|
338
|
+
const login_response = await transport('/api/account/login', {
|
|
339
|
+
method: 'POST',
|
|
340
|
+
headers: { 'Content-Type': 'application/json' },
|
|
341
|
+
body: JSON.stringify({ username, password }),
|
|
342
|
+
});
|
|
343
|
+
if (!login_response.ok) {
|
|
344
|
+
const body = await login_response.text().catch(() => '<unreadable>');
|
|
345
|
+
throw new Error(`login(${handle.config.name}) failed: status=${login_response.status} body=${body}`);
|
|
346
|
+
}
|
|
347
|
+
// Drain the body so the connection releases — Hono's login returns
|
|
348
|
+
// `{ok: true}` and we already have the cookie via the jar.
|
|
349
|
+
await login_response.arrayBuffer().catch(() => undefined);
|
|
350
|
+
const token_result = await rpc_via_transport(transport, handle.config.rpc_path, 'account_token_create', {}, handle.config.name);
|
|
351
|
+
const token_parsed = TokenCreateResponseShape.safeParse(token_result);
|
|
352
|
+
if (!token_parsed.success) {
|
|
353
|
+
throw new Error(`account_token_create(${handle.config.name}) returned unexpected result: ${JSON.stringify(token_result)}`);
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
transport,
|
|
357
|
+
account: parsed.data.account,
|
|
358
|
+
actor: parsed.data.actor,
|
|
359
|
+
session_cookie: extract_cookie_value(transport, handle.config.cookie_name, handle.config.name),
|
|
360
|
+
api_token: token_parsed.data.token,
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
364
|
+
* Grant additional roles to a per-test account by driving the production
|
|
365
|
+
* `role_grant_offer_create` (keeper) + `role_grant_offer_accept`
|
|
366
|
+
* (account) consent flow. After this returns, the account holds a real
|
|
367
|
+
* `role_grant` row for each role — indistinguishable from a production
|
|
368
|
+
* grant. Costs ~2 RPCs (~30-50ms) per role.
|
|
369
|
+
*
|
|
370
|
+
* Used by `fixture.create_account({roles: [...]})`. Roles whose
|
|
371
|
+
* `RoleSpec.grant_paths` don't include `'admin'` reject at
|
|
372
|
+
* offer-create time and surface a loud RPC error — those roles must be
|
|
373
|
+
* declared via `extra_accounts` instead (bootstrap-time seeding).
|
|
374
|
+
*/
|
|
375
|
+
const grant_roles_via_offer_accept = async (handle, minted, roles) => {
|
|
376
|
+
for (const role of roles) {
|
|
377
|
+
const offer_result = (await rpc_via_transport(handle.keeper_transport, handle.config.rpc_path, 'role_grant_offer_create', { to_account_id: minted.account.id, role }, `${handle.config.name}, role=${role}`));
|
|
378
|
+
if (!offer_result?.offer?.id) {
|
|
379
|
+
throw new Error(`role_grant_offer_create(${handle.config.name}, role=${role}) returned unexpected result: ` +
|
|
380
|
+
JSON.stringify(offer_result));
|
|
381
|
+
}
|
|
382
|
+
const offer_id = offer_result.offer.id;
|
|
383
|
+
const accept_result = await rpc_via_transport(minted.transport, handle.config.rpc_path, 'role_grant_offer_accept', { offer_id }, `${handle.config.name}, role=${role}`);
|
|
384
|
+
if (!accept_result) {
|
|
385
|
+
throw new Error(`role_grant_offer_accept(${handle.config.name}, role=${role}) returned unexpected result: ` +
|
|
386
|
+
JSON.stringify(accept_result));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
/**
|
|
391
|
+
* Build a keeper-authenticated `FetchTransport` that closes over the
|
|
392
|
+
* supplied session cookie. Used by the per-test fixture so each call to
|
|
393
|
+
* `setup_test()` builds a transport carrying the freshly re-seeded
|
|
394
|
+
* keeper's cookie (not the original `globalSetup` keeper's, which is
|
|
395
|
+
* stale after `_testing_reset` wipes it).
|
|
396
|
+
*/
|
|
397
|
+
const create_keeper_transport = (handle, cookie_name, session_cookie) => create_fetch_transport({
|
|
398
|
+
base_url: handle.config.base_url,
|
|
399
|
+
initial_cookies: [`${cookie_name}=${session_cookie}`],
|
|
400
|
+
});
|
|
401
|
+
/**
|
|
402
|
+
* Build a `SetupTest` against a spawned + bootstrapped backend.
|
|
403
|
+
*
|
|
404
|
+
* Per-test body (unconditional reset — fresh keeper every test):
|
|
405
|
+
*
|
|
406
|
+
* 1. Fire `_testing_reset` via the keeper's daemon-token channel. The
|
|
407
|
+
* action wipes auth tables, seeds a fresh keeper (with
|
|
408
|
+
* `extra_keeper_roles` applied), seeds any `extra_accounts`, and
|
|
409
|
+
* returns the new credentials.
|
|
410
|
+
* 2. Build the `TestFixture` closing over the new keeper as the
|
|
411
|
+
* fixture's primary `account` / `actor` (matching in-process
|
|
412
|
+
* semantics). `fixture.extra_accounts[username]` exposes any
|
|
413
|
+
* bootstrap-time secondaries.
|
|
414
|
+
* 3. `fixture.create_account()` mints additional *post-bootstrap*
|
|
415
|
+
* accounts via the production signup + login flow (invite → signup
|
|
416
|
+
* → login → token). Roles go through offer/accept (production
|
|
417
|
+
* consent path).
|
|
418
|
+
*
|
|
419
|
+
* No `reset: boolean` opt-in — every test runs against a freshly
|
|
420
|
+
* bootstrapped keeper. This converges in-process and cross-process
|
|
421
|
+
* keeper lifetimes; mutation-cascade tests (password change,
|
|
422
|
+
* revoke-all) and hardcoded-username signup tests work uniformly.
|
|
423
|
+
*/
|
|
424
|
+
export const default_cross_process_setup = (handle, options) => {
|
|
425
|
+
const extra_keeper_roles = options?.extra_keeper_roles ?? [];
|
|
426
|
+
const extra_account_specs = options?.extra_accounts ?? [];
|
|
427
|
+
const { cookie_name } = handle.config;
|
|
428
|
+
return async () => {
|
|
429
|
+
const { keeper, extra_accounts: seeded_extras } = await fire_testing_reset(handle, {
|
|
430
|
+
extra_keeper_roles,
|
|
431
|
+
extra_accounts: extra_account_specs,
|
|
432
|
+
});
|
|
433
|
+
// Rebuild the keeper transport with the new session cookie — the
|
|
434
|
+
// reset action wiped the `globalSetup` keeper's auth_session row,
|
|
435
|
+
// so the handle's `keeper_transport` is now signing requests with
|
|
436
|
+
// a stale cookie. The new transport closes over the fresh
|
|
437
|
+
// `session_cookie` for any keeper-acting calls this test makes
|
|
438
|
+
// (e.g. `fixture.create_account()`'s `invite_create` step).
|
|
439
|
+
const keeper_transport = create_keeper_transport(handle, cookie_name, keeper.session_cookie);
|
|
440
|
+
const refreshed_handle = {
|
|
441
|
+
...handle,
|
|
442
|
+
keeper_transport,
|
|
443
|
+
keeper_account: keeper.account,
|
|
444
|
+
keeper_actor: keeper.actor,
|
|
445
|
+
keeper_cookies: [`${cookie_name}=${keeper.session_cookie}`],
|
|
446
|
+
};
|
|
447
|
+
const create_session_headers = (extra) => ({
|
|
448
|
+
cookie: `${cookie_name}=${keeper.session_cookie}`,
|
|
449
|
+
...extra,
|
|
450
|
+
});
|
|
451
|
+
const create_bearer_headers = (extra) => ({
|
|
452
|
+
authorization: `Bearer ${keeper.api_token}`,
|
|
453
|
+
...extra,
|
|
454
|
+
});
|
|
455
|
+
const create_daemon_token_headers = (extra) => ({
|
|
456
|
+
[DAEMON_TOKEN_HEADER]: handle.daemon_token,
|
|
457
|
+
...extra,
|
|
458
|
+
});
|
|
459
|
+
const create_account = async (account_options) => {
|
|
460
|
+
const other = await mint_account(refreshed_handle, {
|
|
461
|
+
...(account_options?.username !== undefined && { username: account_options.username }),
|
|
462
|
+
...(account_options?.password_value !== undefined && {
|
|
463
|
+
password_value: account_options.password_value,
|
|
464
|
+
}),
|
|
465
|
+
});
|
|
466
|
+
if (account_options?.roles && account_options.roles.length > 0) {
|
|
467
|
+
await grant_roles_via_offer_accept(refreshed_handle, other, account_options.roles);
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
account: other.account,
|
|
471
|
+
actor: other.actor,
|
|
472
|
+
session_cookie: other.session_cookie,
|
|
473
|
+
api_token: other.api_token,
|
|
474
|
+
create_session_headers: (extra) => ({
|
|
475
|
+
cookie: `${cookie_name}=${other.session_cookie}`,
|
|
476
|
+
...extra,
|
|
477
|
+
}),
|
|
478
|
+
create_bearer_headers: (extra) => ({
|
|
479
|
+
authorization: `Bearer ${other.api_token}`,
|
|
480
|
+
...extra,
|
|
481
|
+
}),
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
const extra_accounts = {};
|
|
485
|
+
for (let i = 0; i < extra_account_specs.length; i++) {
|
|
486
|
+
const spec = extra_account_specs[i];
|
|
487
|
+
const seeded = seeded_extras[i];
|
|
488
|
+
if (!spec || !seeded)
|
|
489
|
+
continue;
|
|
490
|
+
extra_accounts[spec.username] = build_extra_account_fixture(seeded, cookie_name);
|
|
491
|
+
}
|
|
492
|
+
// Per-test transport — fresh jar carrying the new keeper's cookie
|
|
493
|
+
// so requests authenticate as the keeper without callers having to
|
|
494
|
+
// thread cookies manually. Tests acting as the fresh keeper use
|
|
495
|
+
// this transport directly; tests minting secondaries thread the
|
|
496
|
+
// secondary's transport via `fixture.create_account()`.
|
|
497
|
+
const transport = create_fetch_transport({
|
|
498
|
+
base_url: handle.config.base_url,
|
|
499
|
+
initial_cookies: [`${cookie_name}=${keeper.session_cookie}`],
|
|
500
|
+
});
|
|
501
|
+
return {
|
|
502
|
+
in_process: false,
|
|
503
|
+
transport,
|
|
504
|
+
fresh_transport: (fresh_options) => create_fetch_transport({
|
|
505
|
+
base_url: handle.config.base_url,
|
|
506
|
+
...(fresh_options?.origin !== undefined && { origin: fresh_options.origin }),
|
|
507
|
+
}),
|
|
508
|
+
account: keeper.account,
|
|
509
|
+
actor: keeper.actor,
|
|
510
|
+
create_session_headers,
|
|
511
|
+
create_bearer_headers,
|
|
512
|
+
create_daemon_token_headers,
|
|
513
|
+
create_account,
|
|
514
|
+
extra_accounts,
|
|
515
|
+
};
|
|
516
|
+
};
|
|
517
|
+
};
|
|
36
518
|
// NOTE: bootstrap config is read from `options.bootstrap` — top-level slot,
|
|
37
519
|
// single source of truth for both the surface spec (so the route appears in
|
|
38
520
|
// `expected_public_routes` / attack-surface iteration) AND the live
|
|
@@ -80,20 +562,18 @@ export const default_in_process_suite_options = (options) => ({
|
|
|
80
562
|
bootstrap: options.bootstrap,
|
|
81
563
|
app_options: options.app_options,
|
|
82
564
|
roles: [ROLE_KEEPER, ...(options.extra_keeper_roles ?? [])],
|
|
565
|
+
extra_accounts: options.extra_accounts,
|
|
83
566
|
}),
|
|
84
567
|
surface_source: options.surface_source ??
|
|
85
|
-
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
bootstrap: options.bootstrap,
|
|
95
|
-
}),
|
|
96
|
-
},
|
|
568
|
+
create_test_app_surface_spec({
|
|
569
|
+
session_options: options.session_options,
|
|
570
|
+
create_route_specs: options.create_route_specs,
|
|
571
|
+
rpc_endpoints: options.rpc_endpoints,
|
|
572
|
+
// Mirror what `create_test_app` → `create_app_server` will mount.
|
|
573
|
+
// Both helpers read from the top-level `bootstrap` slot so surface
|
|
574
|
+
// and live app stay in sync by construction.
|
|
575
|
+
bootstrap: options.bootstrap,
|
|
576
|
+
}),
|
|
97
577
|
capabilities: in_process_capabilities,
|
|
98
578
|
session_options: options.session_options,
|
|
99
579
|
create_route_specs: options.create_route_specs,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Spawn a test backend binary, wait for it to come up, and return a
|
|
4
|
+
* handle the test harness drives.
|
|
5
|
+
*
|
|
6
|
+
* Lifecycle:
|
|
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` —
|
|
11
|
+
* creates a new process group so a `SIGTERM` to the negative PID
|
|
12
|
+
* 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
|
+
* deterministic daemon token; thread it onto `BackendHandle` so
|
|
19
|
+
* `_testing_reset` and other keeper-credential calls can authenticate.
|
|
20
|
+
*
|
|
21
|
+
* Bootstrapping (`POST /api/account/bootstrap`) is a separate concern —
|
|
22
|
+
* the caller composes `bootstrap()` from `../transports/bootstrap.ts`
|
|
23
|
+
* against a `FetchTransport` built around `handle.config.base_url`.
|
|
24
|
+
* Splitting the two keeps `spawn_backend` consumer-agnostic — fuz_app
|
|
25
|
+
* knows nothing about specific binary contents.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
import { type ChildProcess } from 'node:child_process';
|
|
30
|
+
import type { BackendConfig } from './backend_config.js';
|
|
31
|
+
/** Handle returned by `spawn_backend` — passed to per-test setup helpers. */
|
|
32
|
+
export interface BackendHandle {
|
|
33
|
+
/** The config used to spawn this backend. Carried for diagnostic + downstream access. */
|
|
34
|
+
readonly config: BackendConfig;
|
|
35
|
+
/** Child process reference — exposed for diagnostic logging only. */
|
|
36
|
+
readonly child: ChildProcess;
|
|
37
|
+
/**
|
|
38
|
+
* Deterministic daemon token captured from
|
|
39
|
+
* `config.bootstrap.daemon_token_path` after the binary booted.
|
|
40
|
+
* `default_cross_process_setup` builds keeper-daemon-token headers
|
|
41
|
+
* from this for `_testing_reset` calls.
|
|
42
|
+
*/
|
|
43
|
+
readonly daemon_token: string;
|
|
44
|
+
/**
|
|
45
|
+
* SIGTERM the child's process group, drain stderr, await exit. Idempotent —
|
|
46
|
+
* calls after the first are no-ops.
|
|
47
|
+
*/
|
|
48
|
+
readonly teardown: () => Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Spawn `config.start_command` and return a handle once the binary is
|
|
52
|
+
* health-probe-ready and the daemon-token file is readable.
|
|
53
|
+
*
|
|
54
|
+
* Errors at any stage SIGTERM the child group before rethrowing — the
|
|
55
|
+
* caller never sees a half-started backend.
|
|
56
|
+
*/
|
|
57
|
+
export declare const spawn_backend: (config: BackendConfig) => Promise<BackendHandle>;
|
|
58
|
+
//# sourceMappingURL=spawn_backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spawn_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/spawn_backend.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAQ,KAAK,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAI5D,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAIvD,6EAA6E;AAC7E,MAAM,WAAW,aAAa;IAC7B,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAmID;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAU,QAAQ,aAAa,KAAG,OAAO,CAAC,aAAa,CA8FhF,CAAC"}
|