@fuzdev/fuz_app 0.65.0 → 0.66.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 (159) hide show
  1. package/dist/actions/CLAUDE.md +65 -86
  2. package/dist/actions/action_codegen.d.ts +1 -1
  3. package/dist/actions/action_codegen.js +1 -1
  4. package/dist/actions/action_event_data.d.ts +1 -1
  5. package/dist/auth/CLAUDE.md +83 -104
  6. package/dist/auth/audit_log_schema.js +2 -2
  7. package/dist/auth/daemon_token_middleware.d.ts +15 -5
  8. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  9. package/dist/auth/daemon_token_middleware.js +24 -15
  10. package/dist/auth/invite_queries.d.ts +17 -7
  11. package/dist/auth/invite_queries.d.ts.map +1 -1
  12. package/dist/auth/invite_queries.js +19 -8
  13. package/dist/auth/signup_routes.d.ts +47 -1
  14. package/dist/auth/signup_routes.d.ts.map +1 -1
  15. package/dist/auth/signup_routes.js +103 -52
  16. package/dist/env/resolve.d.ts +44 -7
  17. package/dist/env/resolve.d.ts.map +1 -1
  18. package/dist/env/resolve.js +94 -27
  19. package/dist/http/CLAUDE.md +47 -52
  20. package/dist/http/jsonrpc.d.ts +23 -7
  21. package/dist/http/jsonrpc.d.ts.map +1 -1
  22. package/dist/http/jsonrpc.js +19 -3
  23. package/dist/http/surface.d.ts +9 -2
  24. package/dist/http/surface.d.ts.map +1 -1
  25. package/dist/runtime/mock.d.ts +1 -1
  26. package/dist/runtime/mock.js +1 -1
  27. package/dist/testing/CLAUDE.md +659 -511
  28. package/dist/testing/admin_integration.d.ts +5 -5
  29. package/dist/testing/admin_integration.d.ts.map +1 -1
  30. package/dist/testing/admin_integration.js +95 -39
  31. package/dist/testing/app_server.d.ts +16 -1
  32. package/dist/testing/app_server.d.ts.map +1 -1
  33. package/dist/testing/app_server.js +18 -3
  34. package/dist/testing/audit_completeness.d.ts +7 -5
  35. package/dist/testing/audit_completeness.d.ts.map +1 -1
  36. package/dist/testing/audit_completeness.js +5 -9
  37. package/dist/testing/bootstrap_success.js +2 -2
  38. package/dist/testing/cross_backend/backend_config.d.ts +113 -0
  39. package/dist/testing/cross_backend/backend_config.d.ts.map +1 -0
  40. package/dist/testing/cross_backend/backend_config.js +1 -0
  41. package/dist/testing/cross_backend/bench/bench_report.d.ts +46 -0
  42. package/dist/testing/cross_backend/bench/bench_report.d.ts.map +1 -0
  43. package/dist/testing/cross_backend/bench/bench_report.js +83 -0
  44. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +44 -0
  45. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts.map +1 -0
  46. package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +38 -0
  47. package/dist/testing/cross_backend/bench/scenario.d.ts +57 -0
  48. package/dist/testing/cross_backend/bench/scenario.d.ts.map +1 -0
  49. package/dist/testing/cross_backend/bench/scenario.js +28 -0
  50. package/dist/testing/cross_backend/bootstrap_backend.d.ts +41 -0
  51. package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -0
  52. package/dist/testing/cross_backend/bootstrap_backend.js +34 -0
  53. package/dist/testing/cross_backend/build_test_backend_paths.d.ts +24 -0
  54. package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -0
  55. package/dist/testing/cross_backend/build_test_backend_paths.js +33 -0
  56. package/dist/testing/cross_backend/capabilities.d.ts +3 -2
  57. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  58. package/dist/testing/cross_backend/default_backend_configs.d.ts +122 -0
  59. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -0
  60. package/dist/testing/cross_backend/default_backend_configs.js +111 -0
  61. package/dist/testing/cross_backend/default_secrets.d.ts +40 -0
  62. package/dist/testing/cross_backend/default_secrets.d.ts.map +1 -0
  63. package/dist/testing/cross_backend/default_secrets.js +39 -0
  64. package/dist/testing/cross_backend/default_spine_surface.d.ts +64 -0
  65. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -0
  66. package/dist/testing/cross_backend/default_spine_surface.js +121 -0
  67. package/dist/testing/cross_backend/setup.d.ts +270 -34
  68. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  69. package/dist/testing/cross_backend/setup.js +495 -15
  70. package/dist/testing/cross_backend/spawn_backend.d.ts +58 -0
  71. package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -0
  72. package/dist/testing/cross_backend/spawn_backend.js +229 -0
  73. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts +66 -0
  74. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts.map +1 -0
  75. package/dist/testing/cross_backend/spine_stub_backend_config.js +49 -0
  76. package/dist/testing/cross_backend/sse_round_trip.d.ts +37 -0
  77. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -0
  78. package/dist/testing/cross_backend/sse_round_trip.js +137 -0
  79. package/dist/testing/cross_backend/standard.d.ts +96 -0
  80. package/dist/testing/cross_backend/standard.d.ts.map +1 -0
  81. package/dist/testing/cross_backend/standard.js +49 -0
  82. package/dist/testing/cross_backend/testing_reset_actions.d.ts +171 -0
  83. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -0
  84. package/dist/testing/cross_backend/testing_reset_actions.js +213 -0
  85. package/dist/testing/cross_backend/testing_server_bun.d.ts +5 -0
  86. package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -0
  87. package/dist/testing/cross_backend/testing_server_bun.js +59 -0
  88. package/dist/testing/cross_backend/testing_server_core.d.ts +140 -0
  89. package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -0
  90. package/dist/testing/cross_backend/testing_server_core.js +68 -0
  91. package/dist/testing/cross_backend/testing_server_deno.d.ts +5 -0
  92. package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -0
  93. package/dist/testing/cross_backend/testing_server_deno.js +37 -0
  94. package/dist/testing/cross_backend/testing_server_node.d.ts +5 -0
  95. package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -0
  96. package/dist/testing/cross_backend/testing_server_node.js +50 -0
  97. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +72 -0
  98. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -0
  99. package/dist/testing/cross_backend/ts_spine_backend_config.js +112 -0
  100. package/dist/testing/cross_backend/ws_round_trip.d.ts +35 -0
  101. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -0
  102. package/dist/testing/cross_backend/ws_round_trip.js +113 -0
  103. package/dist/testing/data_exposure.d.ts +4 -6
  104. package/dist/testing/data_exposure.d.ts.map +1 -1
  105. package/dist/testing/data_exposure.js +1 -5
  106. package/dist/testing/db_entities.d.ts +18 -7
  107. package/dist/testing/db_entities.d.ts.map +1 -1
  108. package/dist/testing/db_entities.js +18 -7
  109. package/dist/testing/integration.d.ts +27 -6
  110. package/dist/testing/integration.d.ts.map +1 -1
  111. package/dist/testing/integration.js +93 -58
  112. package/dist/testing/round_trip.d.ts +4 -5
  113. package/dist/testing/round_trip.d.ts.map +1 -1
  114. package/dist/testing/round_trip.js +1 -5
  115. package/dist/testing/rpc_helpers.d.ts +10 -4
  116. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  117. package/dist/testing/rpc_helpers.js +1 -1
  118. package/dist/testing/rpc_round_trip.d.ts +5 -5
  119. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  120. package/dist/testing/rpc_round_trip.js +1 -5
  121. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  122. package/dist/testing/sse_round_trip.js +1 -68
  123. package/dist/testing/standard.d.ts +4 -5
  124. package/dist/testing/standard.d.ts.map +1 -1
  125. package/dist/testing/stubs.d.ts +10 -3
  126. package/dist/testing/stubs.d.ts.map +1 -1
  127. package/dist/testing/stubs.js +9 -2
  128. package/dist/testing/testing_rate_limiter.d.ts +59 -0
  129. package/dist/testing/testing_rate_limiter.d.ts.map +1 -0
  130. package/dist/testing/testing_rate_limiter.js +74 -0
  131. package/dist/testing/transports/bootstrap.d.ts +52 -0
  132. package/dist/testing/transports/bootstrap.d.ts.map +1 -0
  133. package/dist/testing/transports/bootstrap.js +70 -0
  134. package/dist/testing/transports/fetch_transport.d.ts +81 -0
  135. package/dist/testing/transports/fetch_transport.d.ts.map +1 -0
  136. package/dist/testing/transports/fetch_transport.js +74 -0
  137. package/dist/testing/transports/sse_frame_reader.d.ts +41 -0
  138. package/dist/testing/transports/sse_frame_reader.d.ts.map +1 -0
  139. package/dist/testing/transports/sse_frame_reader.js +84 -0
  140. package/dist/testing/transports/sse_transport.d.ts +54 -0
  141. package/dist/testing/transports/sse_transport.d.ts.map +1 -0
  142. package/dist/testing/transports/sse_transport.js +51 -0
  143. package/dist/testing/transports/ws_client.d.ts +108 -0
  144. package/dist/testing/transports/ws_client.d.ts.map +1 -0
  145. package/dist/testing/transports/ws_client.js +56 -0
  146. package/dist/testing/transports/ws_transport.d.ts +43 -0
  147. package/dist/testing/transports/ws_transport.d.ts.map +1 -0
  148. package/dist/testing/transports/ws_transport.js +169 -0
  149. package/dist/testing/ws_round_trip.d.ts +21 -103
  150. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  151. package/dist/testing/ws_round_trip.js +42 -40
  152. package/dist/ui/CLAUDE.md +5 -3
  153. package/dist/ui/MenuLink.svelte +16 -16
  154. package/dist/ui/MenuLink.svelte.d.ts +13 -4
  155. package/dist/ui/MenuLink.svelte.d.ts.map +1 -1
  156. package/package.json +7 -1
  157. package/dist/testing/transports/surface_source.d.ts +0 -51
  158. package/dist/testing/transports/surface_source.d.ts.map +0 -1
  159. package/dist/testing/transports/surface_source.js +0 -19
@@ -0,0 +1,229 @@
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 { spawn } from 'node:child_process';
30
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
31
+ import { dirname } from 'node:path';
32
+ /** Number of ms between health-probe attempts. Tuned to be cheap on busy CI runners. */
33
+ const HEALTH_PROBE_INTERVAL_MS = 100;
34
+ /**
35
+ * Sleep helper for the probe loop. Resolves after `ms`.
36
+ */
37
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
38
+ /**
39
+ * Poll `url` until it returns a 2xx response or `timeout_ms` elapses.
40
+ * Network errors during the wait window are expected (binary not yet
41
+ * listening) — they reset the loop, not throw.
42
+ */
43
+ const wait_for_health = async (url, timeout_ms, is_alive) => {
44
+ const deadline = Date.now() + timeout_ms;
45
+ let last_error;
46
+ while (Date.now() < deadline) {
47
+ if (!is_alive()) {
48
+ throw new Error(`backend process exited before becoming healthy${last_error
49
+ ? ` (last probe error: ${last_error.message ?? String(last_error)})`
50
+ : ''}`);
51
+ }
52
+ try {
53
+ const response = await fetch(url);
54
+ if (response.ok) {
55
+ // Drain the body so the connection can be released back to
56
+ // the agent pool — unconsumed bodies keep the socket open.
57
+ await response.arrayBuffer().catch(() => undefined);
58
+ return;
59
+ }
60
+ last_error = new Error(`status=${response.status}`);
61
+ }
62
+ catch (err) {
63
+ last_error = err;
64
+ }
65
+ await sleep(HEALTH_PROBE_INTERVAL_MS);
66
+ }
67
+ throw new Error(`health probe to ${url} timed out after ${timeout_ms}ms (last error: ${last_error ? (last_error.message ?? String(last_error)) : 'none'})`);
68
+ };
69
+ /**
70
+ * Process-level cleanup registry — every `spawn_backend` registers its
71
+ * teardown here so vitest worker death or interactive Ctrl+C kills the
72
+ * binaries before they strand ports.
73
+ */
74
+ const live_teardowns = new Set();
75
+ let process_handlers_installed = false;
76
+ const install_process_handlers = () => {
77
+ if (process_handlers_installed)
78
+ return;
79
+ process_handlers_installed = true;
80
+ const fire_all = () => {
81
+ for (const t of live_teardowns) {
82
+ try {
83
+ t();
84
+ }
85
+ catch {
86
+ // Swallow — exit-time best-effort cleanup, errors here go nowhere.
87
+ }
88
+ }
89
+ live_teardowns.clear();
90
+ };
91
+ process.on('exit', fire_all);
92
+ // Re-emit signals after handling so the default behaviour (process exit)
93
+ // still applies once we've torn down children.
94
+ const passthrough_signal = (signal) => {
95
+ fire_all();
96
+ // Restore default and re-raise so the process exits with the right code.
97
+ process.removeAllListeners(signal);
98
+ process.kill(process.pid, signal);
99
+ };
100
+ process.on('SIGINT', () => passthrough_signal('SIGINT'));
101
+ process.on('SIGTERM', () => passthrough_signal('SIGTERM'));
102
+ };
103
+ /**
104
+ * Read the daemon token file. The file is `{"token": "<value>"}` JSON —
105
+ * single canonical shape across every consumer's test binary.
106
+ *
107
+ * Retries briefly to cover the race between the binary becoming
108
+ * health-probe-ready and writing the token file (some binaries write
109
+ * the file inside a startup task that fires shortly after the readiness
110
+ * signal). Bounded by `attempt_count` so a binary that never writes the
111
+ * file surfaces as a clean error rather than hanging.
112
+ *
113
+ * @throws Error if the file never becomes readable within the retry
114
+ * window, or if the parsed contents don't match `{token: string}`.
115
+ */
116
+ const read_daemon_token = async (path) => {
117
+ const attempt_count = 50; // 50 × 20ms = 1s window
118
+ const attempt_interval_ms = 20;
119
+ let last_error;
120
+ for (let i = 0; i < attempt_count; i++) {
121
+ try {
122
+ const raw = (await readFile(path, 'utf-8')).trim();
123
+ if (raw.length > 0) {
124
+ const parsed = JSON.parse(raw);
125
+ if (typeof parsed !== 'object' ||
126
+ parsed === null ||
127
+ !('token' in parsed) ||
128
+ typeof parsed.token !== 'string') {
129
+ throw new Error(`expected {token: string}, got ${raw}`);
130
+ }
131
+ return parsed.token;
132
+ }
133
+ }
134
+ catch (err) {
135
+ last_error = err;
136
+ }
137
+ await sleep(attempt_interval_ms);
138
+ }
139
+ throw new Error(`daemon token file ${path} never became readable as {token: string} (last error: ${last_error ? (last_error.message ?? String(last_error)) : 'none'})`);
140
+ };
141
+ /**
142
+ * Spawn `config.start_command` and return a handle once the binary is
143
+ * health-probe-ready and the daemon-token file is readable.
144
+ *
145
+ * Errors at any stage SIGTERM the child group before rethrowing — the
146
+ * caller never sees a half-started backend.
147
+ */
148
+ export const spawn_backend = async (config) => {
149
+ if (config.start_command.length === 0) {
150
+ throw new Error(`spawn_backend(${config.name}): start_command is empty`);
151
+ }
152
+ // Write the bootstrap token file before spawn so the binary reads it
153
+ // at boot.
154
+ await mkdir(dirname(config.bootstrap.token_path), { recursive: true });
155
+ await writeFile(config.bootstrap.token_path, config.bootstrap.token, { mode: 0o600 });
156
+ install_process_handlers();
157
+ const [command, ...args] = config.start_command;
158
+ const child = spawn(command, args, {
159
+ env: { ...process.env, ...config.env },
160
+ // Own process group so SIGTERM to the negative PID tears down the
161
+ // binary's descendants too — Hono workers, PTY children, etc.
162
+ detached: true,
163
+ stdio: ['ignore', 'pipe', 'pipe'],
164
+ });
165
+ // Buffer stderr so a startup-time crash surfaces with context.
166
+ const stderr_chunks = [];
167
+ child.stderr?.on('data', (chunk) => {
168
+ stderr_chunks.push(chunk);
169
+ });
170
+ // Drain stdout — discard, but the read is mandatory. `stdio: 'pipe'`
171
+ // leaves the stream paused until something consumes it; an unread
172
+ // pipe fills its OS buffer (~64KB pipe / ~208KB AF_UNIX socketpair on
173
+ // Linux) and the child's next blocking write to stdout parks in the
174
+ // kernel. A backend that logs synchronously to stdout (the default
175
+ // `tracing_subscriber::fmt()` writer) then wedges its whole async
176
+ // runtime: the writing worker holds stdout's lock while parked, every
177
+ // other worker that logs blocks behind it, and even lock-free routes
178
+ // like `/health` (which the request-tracing layer logs) hang. The
179
+ // failure is volume- and time-dependent — it only surfaces after a
180
+ // long run pumps more than a buffer's worth of `info` logs through one
181
+ // long-lived binary — so it hides in short/isolated runs. We discard
182
+ // rather than buffer (unlike stderr): stdout carries high-volume
183
+ // operational logging whose unbounded retention would leak across a
184
+ // long suite.
185
+ child.stdout?.on('data', () => { });
186
+ let exit_info = null;
187
+ child.on('exit', (code, signal) => {
188
+ exit_info = { code, signal };
189
+ });
190
+ const is_alive = () => exit_info === null;
191
+ let teardown_invoked = false;
192
+ const teardown_sync = () => {
193
+ if (teardown_invoked)
194
+ return;
195
+ teardown_invoked = true;
196
+ if (child.pid !== undefined && exit_info === null) {
197
+ try {
198
+ // Negative pid → process group.
199
+ process.kill(-child.pid, 'SIGTERM');
200
+ }
201
+ catch {
202
+ // Already dead; ignore.
203
+ }
204
+ }
205
+ };
206
+ const teardown = async () => {
207
+ teardown_sync();
208
+ live_teardowns.delete(teardown_sync);
209
+ if (exit_info !== null)
210
+ return;
211
+ // Wait for the child to actually exit so callers can be sure the
212
+ // port is free.
213
+ await new Promise((resolve) => {
214
+ child.once('exit', () => resolve());
215
+ });
216
+ };
217
+ live_teardowns.add(teardown_sync);
218
+ try {
219
+ await wait_for_health(`${config.base_url}${config.health_path}`, config.startup_timeout_ms, is_alive);
220
+ const daemon_token = await read_daemon_token(config.bootstrap.daemon_token_path);
221
+ return { config, child, daemon_token, teardown };
222
+ }
223
+ catch (err) {
224
+ await teardown();
225
+ const stderr_dump = Buffer.concat(stderr_chunks).toString('utf-8');
226
+ const stderr_tail = stderr_dump.length > 0 ? `\nstderr:\n${stderr_dump}` : '';
227
+ throw new Error(`spawn_backend(${config.name}) failed: ${err.message}${stderr_tail}`);
228
+ }
229
+ };
@@ -0,0 +1,66 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-process `BackendConfig` preset for the non-domain spine consumer,
4
+ * `testing_spine_stub` — a Rust binary that mounts only the spine surface
5
+ * (auth / account / admin / audit / role-grant offers) with no domain
6
+ * layer. fuz_app drives it from `src/test/cross_backend/*.cross.test.ts`
7
+ * to verify its TS spec against the Rust spine end-to-end with no domain
8
+ * implementation in the loop — drift becomes a fuz_app failure rather than
9
+ * a downstream consumer's failure with mixed signals.
10
+ *
11
+ * **Binary discovery — env-supplied, never hardcoded.** The binary lives
12
+ * in a sibling Rust workspace, not in fuz_app, so the preset never bakes a
13
+ * path in. `FUZ_TESTING_SPINE_STUB_BIN` (or the `binary_path` option) must
14
+ * point at a prebuilt binary; the preset throws a clear error when neither
15
+ * is set rather than guessing. Build once with
16
+ * `cargo build -p testing_spine_stub --release` and point the env var at
17
+ * the resulting `target/release/testing_spine_stub`; operators / CI cache
18
+ * the binary across runs for fast spawns.
19
+ *
20
+ * **Operator setup** — the target Postgres database must exist before the
21
+ * harness runs (the harness never issues `CREATE DATABASE`, to avoid
22
+ * forcing a `CREATEDB` grant on the test role):
23
+ *
24
+ * ```bash
25
+ * createdb fuz_app_test_spine_stub 2>/dev/null || true
26
+ * ```
27
+ *
28
+ * The binary self-wipes the auth-namespace schema on every boot
29
+ * (`FUZ_TESTING_RESET_DB_ON_STARTUP=true`, set by the Rust-family builder),
30
+ * so no manual `DROP TABLE` between sessions is needed; per-test reset is
31
+ * the orthogonal `_testing_reset` RPC action `default_cross_process_setup`
32
+ * fires.
33
+ *
34
+ * @module
35
+ */
36
+ import type { BackendConfig } from './backend_config.js';
37
+ /** Env var naming the prebuilt `testing_spine_stub` binary. Required when `binary_path` is omitted. */
38
+ export declare const SPINE_STUB_BIN_ENV = "FUZ_TESTING_SPINE_STUB_BIN";
39
+ /** Default listening port — slots beside zzz's 1175/1176; matches the binary's `DEFAULT_PORT`. */
40
+ export declare const SPINE_STUB_DEFAULT_PORT = 1177;
41
+ /** Default Postgres database — real PG (PGlite isn't reachable from `tokio-postgres`). */
42
+ export declare const SPINE_STUB_DEFAULT_DATABASE_URL = "postgres://localhost/fuz_app_test_spine_stub";
43
+ export interface SpineStubBackendConfigOptions {
44
+ /** Listening port. Default `SPINE_STUB_DEFAULT_PORT`. */
45
+ readonly port?: number;
46
+ /** Postgres connection URL. Default `SPINE_STUB_DEFAULT_DATABASE_URL`. */
47
+ readonly database_url?: string;
48
+ /**
49
+ * Prebuilt binary path. Overrides the `FUZ_TESTING_SPINE_STUB_BIN` env
50
+ * var. When neither is set the preset throws.
51
+ */
52
+ readonly binary_path?: string;
53
+ }
54
+ /**
55
+ * Build the `BackendConfig` for `testing_spine_stub`. Resolves the binary
56
+ * from `options.binary_path` or `FUZ_TESTING_SPINE_STUB_BIN`; throws when
57
+ * neither is set so a missing build surfaces as a clear error rather than
58
+ * a confusing spawn failure. Reconciles the binary's env contract: port
59
+ * via `--port` (and `FUZ_SPINE_STUB_PORT`), daemon-token dir via
60
+ * `FUZ_SPINE_STUB_DIR` (anchored to `paths.root` so the written
61
+ * `{dir}/run/daemon_token` matches the path `spawn_backend` reads).
62
+ *
63
+ * @throws Error when no binary path is available.
64
+ */
65
+ export declare const spine_stub_backend_config: (options?: SpineStubBackendConfigOptions) => BackendConfig;
66
+ //# sourceMappingURL=spine_stub_backend_config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAIvD,uGAAuG;AACvG,eAAO,MAAM,kBAAkB,+BAA+B,CAAC;AAE/D,kGAAkG;AAClG,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,0FAA0F;AAC1F,eAAO,MAAM,+BAA+B,iDAAiD,CAAC;AAE9F,MAAM,WAAW,6BAA6B;IAC7C,yDAAyD;IACzD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,GACrC,UAAS,6BAAkC,KACzC,aAkCF,CAAC"}
@@ -0,0 +1,49 @@
1
+ import '../assert_dev_env.js';
2
+ import { build_test_backend_paths } from './build_test_backend_paths.js';
3
+ import { make_default_rust_backend_config } from './default_backend_configs.js';
4
+ /** Env var naming the prebuilt `testing_spine_stub` binary. Required when `binary_path` is omitted. */
5
+ export const SPINE_STUB_BIN_ENV = 'FUZ_TESTING_SPINE_STUB_BIN';
6
+ /** Default listening port — slots beside zzz's 1175/1176; matches the binary's `DEFAULT_PORT`. */
7
+ export const SPINE_STUB_DEFAULT_PORT = 1177;
8
+ /** Default Postgres database — real PG (PGlite isn't reachable from `tokio-postgres`). */
9
+ export const SPINE_STUB_DEFAULT_DATABASE_URL = 'postgres://localhost/fuz_app_test_spine_stub';
10
+ /**
11
+ * Build the `BackendConfig` for `testing_spine_stub`. Resolves the binary
12
+ * from `options.binary_path` or `FUZ_TESTING_SPINE_STUB_BIN`; throws when
13
+ * neither is set so a missing build surfaces as a clear error rather than
14
+ * a confusing spawn failure. Reconciles the binary's env contract: port
15
+ * via `--port` (and `FUZ_SPINE_STUB_PORT`), daemon-token dir via
16
+ * `FUZ_SPINE_STUB_DIR` (anchored to `paths.root` so the written
17
+ * `{dir}/run/daemon_token` matches the path `spawn_backend` reads).
18
+ *
19
+ * @throws Error when no binary path is available.
20
+ */
21
+ export const spine_stub_backend_config = (options = {}) => {
22
+ const { port = SPINE_STUB_DEFAULT_PORT, database_url = SPINE_STUB_DEFAULT_DATABASE_URL, binary_path = process.env[SPINE_STUB_BIN_ENV], } = options;
23
+ if (!binary_path) {
24
+ throw new Error(`spine_stub_backend_config: no binary path — set ${SPINE_STUB_BIN_ENV} to a prebuilt ` +
25
+ '`testing_spine_stub` binary (build it with `cargo build -p testing_spine_stub --release`) ' +
26
+ 'or pass `binary_path`.');
27
+ }
28
+ const name = 'spine_stub';
29
+ const paths = build_test_backend_paths(name);
30
+ return make_default_rust_backend_config({
31
+ name,
32
+ port,
33
+ // `--port` is the binary's authoritative port input; the
34
+ // `FUZ_SPINE_STUB_PORT` env the builder also sets (via `port_env_var`)
35
+ // is the lower-precedence fallback — both carry the same value.
36
+ start_command: [binary_path, '--port', String(port)],
37
+ database_url,
38
+ port_env_var: 'FUZ_SPINE_STUB_PORT',
39
+ rust_log: 'info,testing_spine_stub=info',
40
+ paths,
41
+ extra_env: {
42
+ // The binary writes its daemon-token JSON to
43
+ // `{FUZ_SPINE_STUB_DIR}/run/daemon_token`; anchoring the dir to
44
+ // `paths.root` makes that equal `paths.daemon_token_path`, which
45
+ // `spawn_backend` reads after the health probe.
46
+ FUZ_SPINE_STUB_DIR: paths.root,
47
+ },
48
+ });
49
+ };
@@ -0,0 +1,37 @@
1
+ import '../assert_dev_env.js';
2
+ import { type BackendCapabilities } from './capabilities.js';
3
+ import type { SetupTest } from './setup.js';
4
+ /** Configuration for {@link describe_cross_process_sse_tests}. */
5
+ export interface CrossProcessSseTestOptions {
6
+ /**
7
+ * Per-test fixture producer (`default_cross_process_setup(handle)`). Each
8
+ * case reads the fresh-per-test keeper's session cookies from
9
+ * `fixture.transport.cookies()` to thread onto the stream. The keeper
10
+ * holds `ROLE_ADMIN` by default, so it can subscribe to the admin-gated
11
+ * audit stream and drive `admin_session_revoke_all`.
12
+ */
13
+ readonly setup_test: SetupTest;
14
+ /** Backend capability flags; every case gates on `capabilities.sse`. */
15
+ readonly capabilities: BackendCapabilities;
16
+ /** Base URL the backend is reachable at (e.g. `http://localhost:1178`). */
17
+ readonly base_url: string;
18
+ /** SSE stream path on the backend. Defaults to `/api/admin/audit/stream`. */
19
+ readonly sse_path?: string;
20
+ /**
21
+ * RPC endpoint path (e.g. `/api/rpc`) used by the data-frame and
22
+ * close-on-revoke cases to fire `admin_session_revoke_all` /
23
+ * `account_session_revoke_all` over the keeper's session channel. When
24
+ * omitted, those cases are skipped — they depend on the standard account
25
+ * + admin actions being mounted on the RPC endpoint.
26
+ */
27
+ readonly rpc_path?: string;
28
+ /** Origin for the stream request. Defaults to `base_url`. */
29
+ readonly origin?: string;
30
+ }
31
+ /**
32
+ * Register the cross-process SSE round-trip suite. Up to three cases over a
33
+ * real streaming `fetch`: connected-comment, audit data frame, and
34
+ * close-on-revoke.
35
+ */
36
+ export declare const describe_cross_process_sse_tests: (options: CrossProcessSseTestOptions) => void;
37
+ //# sourceMappingURL=sse_round_trip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/sse_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA8C9B,OAAO,EAAC,KAAK,mBAAmB,EAAU,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AAK1C,kEAAkE;AAClE,MAAM,WAAW,0BAA0B;IAC1C;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,0BAA0B,KAAG,IAwGtF,CAAC"}
@@ -0,0 +1,137 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-process SSE round-trip suite — the cross-process counterpart to the
4
+ * in-process `testing/sse_round_trip.ts` harness.
5
+ *
6
+ * Where the in-process harness reads a Hono `Response.body` directly, this
7
+ * suite opens a **real** streaming `fetch` against a spawned backend's
8
+ * audit-log SSE endpoint via `create_sse_transport`, threading the
9
+ * fresh-per-test keeper's session cookie. It is the only coverage of the
10
+ * spawned binary's live SSE path — the standard cross-process bundle
11
+ * (`describe_standard_cross_process_tests`) omits SSE by design, so consumers
12
+ * call this alongside it (paralleling `describe_cross_process_ws_tests`).
13
+ *
14
+ * Three cases, mirroring the in-process SSE self-test against fuz_app's
15
+ * standard audit-log stream:
16
+ *
17
+ * 1. **connects** — the stream opens and emits the `: connected` comment.
18
+ * 2. **data frame** (gated on `rpc_path`) — a minted secondary's sessions are
19
+ * revoked over the keeper's admin channel (`admin_session_revoke_all`),
20
+ * broadcasting a `session_revoke_all` audit event as one `data:` frame to
21
+ * the subscribed keeper **without** closing its stream (the event targets
22
+ * the secondary, not the subscriber). The secondary is minted *before* the
23
+ * stream opens so `create_account`'s own audit events (invite / signup /
24
+ * login / token) don't land on it.
25
+ * 3. **close-on-revoke** (gated on `rpc_path`) — the subscriber's *own*
26
+ * sessions are revoked (`account_session_revoke_all`), so the
27
+ * `session_revoke_all` event targets the keeper and the audit guard drops
28
+ * the live stream. Asserted via `SseTransport.wait_for_close`.
29
+ *
30
+ * Gated on `capabilities.sse` — backends without an end-to-end SSE stream
31
+ * skip (the cases still surface as `.skip` in the report). Cross-process
32
+ * only: `create_sse_transport` needs a real bound socket, so wire it from a
33
+ * `*.cross.test.ts` file, never an in-process setup.
34
+ *
35
+ * @module
36
+ */
37
+ import { assert, describe } from 'vitest';
38
+ import { account_session_revoke_all_action_spec } from '../../auth/account_action_specs.js';
39
+ import { admin_session_revoke_all_action_spec } from '../../auth/admin_action_specs.js';
40
+ import { audit_log_event_specs } from '../../realtime/sse_auth_guard.js';
41
+ import { SSE_CONNECTED_COMMENT } from '../../realtime/sse.js';
42
+ import { create_sse_transport } from '../transports/sse_transport.js';
43
+ import { create_rpc_post_init } from '../rpc_helpers.js';
44
+ import { test_if } from './capabilities.js';
45
+ /** Default audit-log SSE stream path — the standard fuz_app `/api/admin/audit/stream`. */
46
+ const DEFAULT_SSE_PATH = '/api/admin/audit/stream';
47
+ /**
48
+ * Assert a decoded SSE frame is a well-formed audit `{method, params}`
49
+ * payload whose `params` validate against the matching `audit_log_event_specs`
50
+ * entry.
51
+ */
52
+ const assert_audit_data_frame = (frame) => {
53
+ const data_line = frame.split('\n').find((line) => line.startsWith('data: '));
54
+ assert.ok(data_line, `SSE frame has no 'data:' line: ${JSON.stringify(frame)}`);
55
+ const payload = JSON.parse(data_line.slice('data: '.length));
56
+ assert.strictEqual(typeof payload.method, 'string', 'audit data frame method must be a string');
57
+ const spec = audit_log_event_specs.find((s) => s.method === payload.method);
58
+ assert.ok(spec, `no EventSpec declared for audit method '${String(payload.method)}'`);
59
+ const result = spec.params.safeParse(payload.params);
60
+ assert.ok(result.success, `audit data frame params mismatch for '${String(payload.method)}': ${result.success ? '' : JSON.stringify(result.error.issues)}`);
61
+ };
62
+ /**
63
+ * Register the cross-process SSE round-trip suite. Up to three cases over a
64
+ * real streaming `fetch`: connected-comment, audit data frame, and
65
+ * close-on-revoke.
66
+ */
67
+ export const describe_cross_process_sse_tests = (options) => {
68
+ const { setup_test, capabilities, base_url, rpc_path, origin } = options;
69
+ const sse_path = options.sse_path ?? DEFAULT_SSE_PATH;
70
+ describe('cross-process sse', () => {
71
+ test_if(capabilities.sse, 'connects and emits the connected comment', async () => {
72
+ const fixture = await setup_test();
73
+ const sse = await create_sse_transport({
74
+ base_url,
75
+ sse_path,
76
+ cookies: fixture.transport.cookies(),
77
+ origin,
78
+ });
79
+ try {
80
+ const first = await sse.read_frame();
81
+ assert.strictEqual(first + '\n\n', SSE_CONNECTED_COMMENT, 'first frame must be the connected comment');
82
+ }
83
+ finally {
84
+ await sse.close();
85
+ }
86
+ });
87
+ // Mint the secondary BEFORE opening the stream so `create_account`'s own
88
+ // audit events stay off it; then revoke the secondary's sessions over the
89
+ // keeper's admin channel → one `session_revoke_all` data frame reaches the
90
+ // keeper (target ≠ subscriber, so the stream stays open).
91
+ test_if(capabilities.sse && rpc_path !== undefined, 'broadcasts an audit event as a data frame', async () => {
92
+ const fixture = await setup_test();
93
+ const secondary = await fixture.create_account({ username: 'sse_revoke_target', roles: [] });
94
+ const sse = await create_sse_transport({
95
+ base_url,
96
+ sse_path,
97
+ cookies: fixture.transport.cookies(),
98
+ origin,
99
+ });
100
+ try {
101
+ const first = await sse.read_frame();
102
+ assert.strictEqual(first + '\n\n', SSE_CONNECTED_COMMENT, 'first frame must be the connected comment');
103
+ const res = await fixture.transport(rpc_path, create_rpc_post_init(admin_session_revoke_all_action_spec.method, {
104
+ account_id: secondary.account.id,
105
+ }));
106
+ assert.strictEqual(res.status, 200, `admin_session_revoke_all RPC failed (status=${res.status})`);
107
+ const data_frame = await sse.read_frame();
108
+ assert_audit_data_frame(data_frame);
109
+ }
110
+ finally {
111
+ await sse.close();
112
+ }
113
+ });
114
+ // Revoke the subscriber's OWN sessions → `session_revoke_all` targets the
115
+ // keeper, so the audit guard closes the live stream.
116
+ test_if(capabilities.sse && rpc_path !== undefined, 'stream closes when the subscriber sessions are revoked', async () => {
117
+ const fixture = await setup_test();
118
+ const sse = await create_sse_transport({
119
+ base_url,
120
+ sse_path,
121
+ cookies: fixture.transport.cookies(),
122
+ origin,
123
+ });
124
+ try {
125
+ const first = await sse.read_frame();
126
+ assert.strictEqual(first + '\n\n', SSE_CONNECTED_COMMENT, 'first frame must be the connected comment');
127
+ const res = await fixture.transport(rpc_path, create_rpc_post_init(account_session_revoke_all_action_spec.method));
128
+ assert.strictEqual(res.status, 200, `account_session_revoke_all RPC failed (status=${res.status})`);
129
+ const closed = await sse.wait_for_close(2000);
130
+ assert.ok(closed, 'stream did not close within 2s after session_revoke_all');
131
+ }
132
+ finally {
133
+ await sse.close();
134
+ }
135
+ });
136
+ });
137
+ };
@@ -0,0 +1,96 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-process counterpart to `describe_standard_tests`.
4
+ *
5
+ * Wires the cross-process-safe subset of the standard bundle — the five
6
+ * suites whose option shape is `{setup_test, surface_source, capabilities, ...}`
7
+ * and whose bodies fire requests through `fixture.transport` rather than
8
+ * touching the in-process `Backend`. Consumers wire one call against a
9
+ * spawned binary instead of repeating the five sibling calls per file.
10
+ *
11
+ * **Suites included** — always run:
12
+ *
13
+ * - `describe_standard_integration_tests`
14
+ * - `describe_round_trip_validation`
15
+ * - `describe_rpc_round_trip_tests`
16
+ * - `describe_data_exposure_tests`
17
+ *
18
+ * **Gated on `roles`** — included when the consumer supplies a
19
+ * `RoleSchemaResult`:
20
+ *
21
+ * - `describe_standard_admin_integration_tests`
22
+ *
23
+ * **Suites omitted** — the three that don't survive a process boundary,
24
+ * documented here so per-consumer files don't have to repeat the
25
+ * bookkeeping:
26
+ *
27
+ * - `describe_rate_limiting_tests` — builds a fresh `TestApp` per test to
28
+ * inject tight per-test rate-limiter overrides. That path requires
29
+ * in-process construction of `Backend` + rate limiter; the spawned
30
+ * binary has neither knob nor restart-per-test budget.
31
+ * - `describe_audit_completeness_tests` — reaches into FK-structural
32
+ * introspection that only the in-process backend exposes. Wire-level
33
+ * audit observability lives in the consumer's own audit `.cross.test.ts`
34
+ * driving `audit_log_list` / `audit_log_role_grant_history`.
35
+ * - `describe_bootstrap_success_tests` — bootstrap is one-shot per
36
+ * backend lifecycle, and the consumer's `globalSetup` already consumed
37
+ * it before the suite file loads. Re-running would 409.
38
+ *
39
+ * @module
40
+ */
41
+ import type { SessionOptions } from '../../auth/session_cookie.js';
42
+ import type { RoleSchemaResult } from '../../auth/role_schema.js';
43
+ import type { AppSurfaceSpec } from '../../http/surface.js';
44
+ import type { RpcEndpointsSuiteOption } from '../rpc_helpers.js';
45
+ import type { BackendCapabilities } from './capabilities.js';
46
+ import type { SetupTest } from './setup.js';
47
+ /**
48
+ * Configuration for `describe_standard_cross_process_tests`.
49
+ *
50
+ * Mirrors `StandardTestOptions` minus the in-process-only knobs
51
+ * (`create_route_specs`, `bootstrap`, `rate_limiting_app_options`,
52
+ * `bootstrap_token`) — those drive the three omitted suites.
53
+ */
54
+ export interface StandardCrossProcessTestOptions {
55
+ /** Per-test fixture-producing function. */
56
+ setup_test: SetupTest;
57
+ /**
58
+ * App surface. Constructed in TS by the consumer; same shape for
59
+ * in-process and cross-process tests.
60
+ */
61
+ surface_source: AppSurfaceSpec;
62
+ /** Backend capability declarations. */
63
+ capabilities: BackendCapabilities;
64
+ /** Session config — needed for cookie_name + factory-form rpc_endpoints resolution. */
65
+ session_options: SessionOptions<string>;
66
+ /**
67
+ * RPC endpoint specs — required. The standard integration tests drive
68
+ * `account_verify`, `account_session_*`, `account_token_*` through the
69
+ * RPC surface (and admin tests, when wired, drive role_grant grant/revoke
70
+ * through it too).
71
+ */
72
+ rpc_endpoints: RpcEndpointsSuiteOption;
73
+ /**
74
+ * Role schema result from `create_role_schema()`.
75
+ * When provided, the admin integration suite is included.
76
+ */
77
+ roles?: RoleSchemaResult;
78
+ /**
79
+ * Path prefix where admin routes are mounted.
80
+ * Default `'/api/admin'`.
81
+ */
82
+ admin_prefix?: string;
83
+ /**
84
+ * Forwarded to `describe_standard_integration_tests` — overrides the
85
+ * default error-coverage threshold on the scoped REST surface. Set to
86
+ * `0` to skip the assertion entirely.
87
+ */
88
+ error_coverage_min?: number;
89
+ }
90
+ /**
91
+ * Run the cross-process standard test bundle — integration, admin (when
92
+ * `roles` provided), round trip, RPC round trip, data exposure. See the
93
+ * module doc for the suites omitted from this bundle and why.
94
+ */
95
+ export declare const describe_standard_cross_process_tests: (options: StandardCrossProcessTestOptions) => void;
96
+ //# sourceMappingURL=standard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standard.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/standard.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAM1D,OAAO,KAAK,EAAC,uBAAuB,EAAC,MAAM,mBAAmB,CAAC;AAC/D,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,mBAAmB,CAAC;AAC3D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,WAAW,+BAA+B;IAC/C,2CAA2C;IAC3C,UAAU,EAAE,SAAS,CAAC;IACtB;;;OAGG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,uCAAuC;IACvC,YAAY,EAAE,mBAAmB,CAAC;IAClC,uFAAuF;IACvF,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;OAGG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,GACjD,SAAS,+BAA+B,KACtC,IAqCF,CAAC"}