@fuzdev/fuz_app 0.64.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 (223) hide show
  1. package/dist/actions/CLAUDE.md +510 -946
  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/actions/broadcast_api.d.ts +1 -1
  6. package/dist/actions/broadcast_api.js +1 -1
  7. package/dist/actions/cancel.d.ts +2 -2
  8. package/dist/actions/cancel.js +3 -3
  9. package/dist/actions/connection_closer.d.ts +1 -4
  10. package/dist/actions/connection_closer.d.ts.map +1 -1
  11. package/dist/actions/connection_closer.js +1 -4
  12. package/dist/actions/register_action_ws.d.ts +2 -2
  13. package/dist/actions/register_ws_endpoint.d.ts +1 -1
  14. package/dist/actions/transports_ws_auth_guard.d.ts +1 -2
  15. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  16. package/dist/actions/transports_ws_auth_guard.js +1 -2
  17. package/dist/auth/CLAUDE.md +570 -1871
  18. package/dist/auth/account_schema.d.ts +1 -1
  19. package/dist/auth/account_schema.d.ts.map +1 -1
  20. package/dist/auth/api_token_queries.js +1 -1
  21. package/dist/auth/audit_log_ddl.d.ts +1 -1
  22. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  23. package/dist/auth/audit_log_ddl.js +1 -1
  24. package/dist/auth/audit_log_schema.js +2 -2
  25. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  26. package/dist/auth/bootstrap_account.js +1 -5
  27. package/dist/auth/bootstrap_routes.d.ts +7 -1
  28. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  29. package/dist/auth/bootstrap_routes.js +15 -11
  30. package/dist/auth/daemon_token_middleware.d.ts +15 -5
  31. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  32. package/dist/auth/daemon_token_middleware.js +24 -15
  33. package/dist/auth/invite_queries.d.ts +17 -7
  34. package/dist/auth/invite_queries.d.ts.map +1 -1
  35. package/dist/auth/invite_queries.js +19 -8
  36. package/dist/auth/keyring.d.ts +6 -6
  37. package/dist/auth/keyring.js +8 -8
  38. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  39. package/dist/auth/role_grant_offer_actions.js +4 -2
  40. package/dist/auth/signup_routes.d.ts +47 -1
  41. package/dist/auth/signup_routes.d.ts.map +1 -1
  42. package/dist/auth/signup_routes.js +103 -52
  43. package/dist/db/create_db.d.ts.map +1 -1
  44. package/dist/db/create_db.js +13 -0
  45. package/dist/dev/setup.d.ts +2 -2
  46. package/dist/dev/setup.js +3 -3
  47. package/dist/env/resolve.d.ts +44 -7
  48. package/dist/env/resolve.d.ts.map +1 -1
  49. package/dist/env/resolve.js +94 -27
  50. package/dist/http/CLAUDE.md +243 -522
  51. package/dist/http/error_schemas.d.ts +0 -4
  52. package/dist/http/error_schemas.d.ts.map +1 -1
  53. package/dist/http/error_schemas.js +0 -4
  54. package/dist/http/ip_canonical.d.ts +5 -4
  55. package/dist/http/ip_canonical.d.ts.map +1 -1
  56. package/dist/http/ip_canonical.js +8 -4
  57. package/dist/http/jsonrpc.d.ts +23 -7
  58. package/dist/http/jsonrpc.d.ts.map +1 -1
  59. package/dist/http/jsonrpc.js +19 -3
  60. package/dist/http/origin.d.ts +1 -1
  61. package/dist/http/origin.js +1 -1
  62. package/dist/http/surface.d.ts +9 -2
  63. package/dist/http/surface.d.ts.map +1 -1
  64. package/dist/runtime/mock.d.ts +1 -1
  65. package/dist/runtime/mock.js +2 -2
  66. package/dist/server/app_server.d.ts +41 -10
  67. package/dist/server/app_server.d.ts.map +1 -1
  68. package/dist/server/app_server.js +10 -4
  69. package/dist/server/env.d.ts +7 -7
  70. package/dist/server/env.d.ts.map +1 -1
  71. package/dist/server/env.js +14 -14
  72. package/dist/server/static.d.ts +4 -4
  73. package/dist/server/static.js +7 -7
  74. package/dist/testing/CLAUDE.md +740 -418
  75. package/dist/testing/admin_integration.d.ts +18 -23
  76. package/dist/testing/admin_integration.d.ts.map +1 -1
  77. package/dist/testing/admin_integration.js +230 -216
  78. package/dist/testing/app_server.d.ts +141 -39
  79. package/dist/testing/app_server.d.ts.map +1 -1
  80. package/dist/testing/app_server.js +157 -44
  81. package/dist/testing/audit_completeness.d.ts +25 -22
  82. package/dist/testing/audit_completeness.d.ts.map +1 -1
  83. package/dist/testing/audit_completeness.js +198 -159
  84. package/dist/testing/bootstrap_success.d.ts +28 -0
  85. package/dist/testing/bootstrap_success.d.ts.map +1 -0
  86. package/dist/testing/bootstrap_success.js +144 -0
  87. package/dist/testing/cross_backend/backend_config.d.ts +113 -0
  88. package/dist/testing/cross_backend/backend_config.d.ts.map +1 -0
  89. package/dist/testing/cross_backend/backend_config.js +1 -0
  90. package/dist/testing/cross_backend/bench/bench_report.d.ts +46 -0
  91. package/dist/testing/cross_backend/bench/bench_report.d.ts.map +1 -0
  92. package/dist/testing/cross_backend/bench/bench_report.js +83 -0
  93. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +44 -0
  94. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts.map +1 -0
  95. package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +38 -0
  96. package/dist/testing/cross_backend/bench/scenario.d.ts +57 -0
  97. package/dist/testing/cross_backend/bench/scenario.d.ts.map +1 -0
  98. package/dist/testing/cross_backend/bench/scenario.js +28 -0
  99. package/dist/testing/cross_backend/bootstrap_backend.d.ts +41 -0
  100. package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -0
  101. package/dist/testing/cross_backend/bootstrap_backend.js +34 -0
  102. package/dist/testing/cross_backend/build_test_backend_paths.d.ts +24 -0
  103. package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -0
  104. package/dist/testing/cross_backend/build_test_backend_paths.js +33 -0
  105. package/dist/testing/cross_backend/capabilities.d.ts +65 -0
  106. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
  107. package/dist/testing/cross_backend/capabilities.js +47 -0
  108. package/dist/testing/cross_backend/default_backend_configs.d.ts +122 -0
  109. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -0
  110. package/dist/testing/cross_backend/default_backend_configs.js +111 -0
  111. package/dist/testing/cross_backend/default_secrets.d.ts +40 -0
  112. package/dist/testing/cross_backend/default_secrets.d.ts.map +1 -0
  113. package/dist/testing/cross_backend/default_secrets.js +39 -0
  114. package/dist/testing/cross_backend/default_spine_surface.d.ts +64 -0
  115. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -0
  116. package/dist/testing/cross_backend/default_spine_surface.js +121 -0
  117. package/dist/testing/cross_backend/setup.d.ts +451 -0
  118. package/dist/testing/cross_backend/setup.d.ts.map +1 -0
  119. package/dist/testing/cross_backend/setup.js +581 -0
  120. package/dist/testing/cross_backend/spawn_backend.d.ts +58 -0
  121. package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -0
  122. package/dist/testing/cross_backend/spawn_backend.js +229 -0
  123. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts +66 -0
  124. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts.map +1 -0
  125. package/dist/testing/cross_backend/spine_stub_backend_config.js +49 -0
  126. package/dist/testing/cross_backend/sse_round_trip.d.ts +37 -0
  127. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -0
  128. package/dist/testing/cross_backend/sse_round_trip.js +137 -0
  129. package/dist/testing/cross_backend/standard.d.ts +96 -0
  130. package/dist/testing/cross_backend/standard.d.ts.map +1 -0
  131. package/dist/testing/cross_backend/standard.js +49 -0
  132. package/dist/testing/cross_backend/testing_reset_actions.d.ts +171 -0
  133. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -0
  134. package/dist/testing/cross_backend/testing_reset_actions.js +213 -0
  135. package/dist/testing/cross_backend/testing_server_bun.d.ts +5 -0
  136. package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -0
  137. package/dist/testing/cross_backend/testing_server_bun.js +59 -0
  138. package/dist/testing/cross_backend/testing_server_core.d.ts +140 -0
  139. package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -0
  140. package/dist/testing/cross_backend/testing_server_core.js +68 -0
  141. package/dist/testing/cross_backend/testing_server_deno.d.ts +5 -0
  142. package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -0
  143. package/dist/testing/cross_backend/testing_server_deno.js +37 -0
  144. package/dist/testing/cross_backend/testing_server_node.d.ts +5 -0
  145. package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -0
  146. package/dist/testing/cross_backend/testing_server_node.js +50 -0
  147. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +72 -0
  148. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -0
  149. package/dist/testing/cross_backend/ts_spine_backend_config.js +112 -0
  150. package/dist/testing/cross_backend/ws_round_trip.d.ts +35 -0
  151. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -0
  152. package/dist/testing/cross_backend/ws_round_trip.js +113 -0
  153. package/dist/testing/data_exposure.d.ts +11 -14
  154. package/dist/testing/data_exposure.d.ts.map +1 -1
  155. package/dist/testing/data_exposure.js +123 -146
  156. package/dist/testing/db_entities.d.ts +22 -1
  157. package/dist/testing/db_entities.d.ts.map +1 -1
  158. package/dist/testing/db_entities.js +24 -1
  159. package/dist/testing/integration.d.ts +56 -21
  160. package/dist/testing/integration.d.ts.map +1 -1
  161. package/dist/testing/integration.js +294 -319
  162. package/dist/testing/integration_helpers.d.ts +16 -6
  163. package/dist/testing/integration_helpers.d.ts.map +1 -1
  164. package/dist/testing/integration_helpers.js +7 -7
  165. package/dist/testing/mock_fs.d.ts.map +1 -1
  166. package/dist/testing/mock_fs.js +0 -2
  167. package/dist/testing/rate_limiting.d.ts.map +1 -1
  168. package/dist/testing/rate_limiting.js +9 -0
  169. package/dist/testing/role_grant_helpers.d.ts +31 -0
  170. package/dist/testing/role_grant_helpers.d.ts.map +1 -0
  171. package/dist/testing/role_grant_helpers.js +46 -0
  172. package/dist/testing/round_trip.d.ts +20 -16
  173. package/dist/testing/round_trip.d.ts.map +1 -1
  174. package/dist/testing/round_trip.js +61 -86
  175. package/dist/testing/rpc_helpers.d.ts +10 -4
  176. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  177. package/dist/testing/rpc_helpers.js +1 -1
  178. package/dist/testing/rpc_round_trip.d.ts +24 -21
  179. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  180. package/dist/testing/rpc_round_trip.js +87 -104
  181. package/dist/testing/schema_introspect.d.ts +106 -0
  182. package/dist/testing/schema_introspect.d.ts.map +1 -0
  183. package/dist/testing/schema_introspect.js +123 -0
  184. package/dist/testing/schema_parity.d.ts +144 -0
  185. package/dist/testing/schema_parity.d.ts.map +1 -0
  186. package/dist/testing/schema_parity.js +233 -0
  187. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  188. package/dist/testing/sse_round_trip.js +1 -68
  189. package/dist/testing/standard.d.ts +56 -25
  190. package/dist/testing/standard.d.ts.map +1 -1
  191. package/dist/testing/standard.js +62 -5
  192. package/dist/testing/stubs.d.ts +21 -6
  193. package/dist/testing/stubs.d.ts.map +1 -1
  194. package/dist/testing/stubs.js +33 -23
  195. package/dist/testing/testing_rate_limiter.d.ts +59 -0
  196. package/dist/testing/testing_rate_limiter.d.ts.map +1 -0
  197. package/dist/testing/testing_rate_limiter.js +74 -0
  198. package/dist/testing/transports/bootstrap.d.ts +52 -0
  199. package/dist/testing/transports/bootstrap.d.ts.map +1 -0
  200. package/dist/testing/transports/bootstrap.js +70 -0
  201. package/dist/testing/transports/fetch_transport.d.ts +81 -0
  202. package/dist/testing/transports/fetch_transport.d.ts.map +1 -0
  203. package/dist/testing/transports/fetch_transport.js +74 -0
  204. package/dist/testing/transports/sse_frame_reader.d.ts +41 -0
  205. package/dist/testing/transports/sse_frame_reader.d.ts.map +1 -0
  206. package/dist/testing/transports/sse_frame_reader.js +84 -0
  207. package/dist/testing/transports/sse_transport.d.ts +54 -0
  208. package/dist/testing/transports/sse_transport.d.ts.map +1 -0
  209. package/dist/testing/transports/sse_transport.js +51 -0
  210. package/dist/testing/transports/ws_client.d.ts +108 -0
  211. package/dist/testing/transports/ws_client.d.ts.map +1 -0
  212. package/dist/testing/transports/ws_client.js +56 -0
  213. package/dist/testing/transports/ws_transport.d.ts +43 -0
  214. package/dist/testing/transports/ws_transport.d.ts.map +1 -0
  215. package/dist/testing/transports/ws_transport.js +169 -0
  216. package/dist/testing/ws_round_trip.d.ts +21 -103
  217. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  218. package/dist/testing/ws_round_trip.js +42 -40
  219. package/dist/ui/CLAUDE.md +5 -3
  220. package/dist/ui/MenuLink.svelte +16 -16
  221. package/dist/ui/MenuLink.svelte.d.ts +13 -4
  222. package/dist/ui/MenuLink.svelte.d.ts.map +1 -1
  223. package/package.json +10 -4
@@ -5,12 +5,11 @@ attack-surface generators, middleware mocks, integration suites, and RPC/SSE/WS
5
5
  round-trip harnesses. Consumers import these to assemble their own test suites
6
6
  against a fuz_app-derived server.
7
7
 
8
- For narrative wiring examples (how to call these from a consumer's vitest
9
- setup), see ../../../docs/testing.md. For fuz_app's own test suite
10
- conventions, see ../../test/CLAUDE.md. For shared testing conventions
11
- (`.db.test.ts`, `assert` from vitest, `assert_rejects`, `vi.mock` caveats),
12
- see Skill(fuz-stack) testing-patterns. This file is a reference index for
13
- the helpers themselves.
8
+ For narrative wiring examples (consumer vitest setup), see
9
+ ../../../docs/testing.md. For fuz_app's own suite conventions, see
10
+ ../../test/CLAUDE.md. For shared testing conventions (`.db.test.ts`, `assert`
11
+ from vitest, `assert_rejects`, `vi.mock` caveats), see Skill(fuz-stack)
12
+ testing-patterns. This file is a reference index for the helpers themselves.
14
13
 
15
14
  ## Production guard — always the first import
16
15
 
@@ -22,84 +21,92 @@ Enforced by grep, not a linter; make this the first line in new modules.
22
21
 
23
22
  ### `stubs.ts` — `AppDeps` + `AppServerContext` stubs
24
23
 
25
- | Helper | Role |
26
- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
27
- | `create_throwing_stub<T>(label)` | Proxy whose every property access throws `Throwing stub 'label' — unexpected access to 'prop'`; JS-internal probes return `undefined`; `toJSON` returns `"[throwing_stub:label]"` so accidental serialization is visible rather than `{}`. |
28
- | `create_noop_stub<T>(label, overrides?)` | Proxy whose every method returns `async () => undefined`; `overrides` lets callers pin specific props. |
29
- | `stub` | Pre-built throwing stub labelled `'stub'`. |
30
- | `create_stub_db()` | Returns a real `Db` whose `client.query` yields `{rows: []}` and whose `transaction(fn)` synchronously calls `fn(inner_stub_db)`. Safe for `apply_route_specs`'s declarative transaction wrapper. |
31
- | `stub_handler()` | Returns a fresh `Response('stub')`. |
32
- | `stub_mw` | Pass-through middleware handler (`async (_c, next) => next()`). |
33
- | `stub_app_deps` | Frozen `AppDeps` every capability is a throwing stub, `audit` is a no-op `AuditEmitter` from `create_test_audit_emitter`. |
34
- | `create_stub_app_deps()` | Factory returning fresh `AppDeps` with no-op FS/keyring/password, a `create_noop_stub` DB, silent `Logger`, no-op `audit`. |
35
- | `create_test_audit_emitter()` | No-op `AuditEmitter` for tests that don't assert on audit fan-out. `emit` / `emit_role_grant_target` are no-ops; `emit_pool` resolves immediately; `notify` is a no-op; `on_event_chain` is empty. |
36
- | `create_stub_audit_sse()` | No-op `AuditLogSse` for surface-test wiring without booting real SSE. `subscribe` returns a no-op cleanup; `on_audit_event` is a no-op; the `registry` is a fresh `SubscriberRegistry` (live `.size` / `.close_*` for tests touching registry state, isolated per call). For real SSE plumbing, build via `create_audit_log_sse` against `create_test_app`. |
37
- | `create_stub_api_middleware({include_daemon_token?})` | Stub `MiddlewareSpec[]` matching `create_auth_middleware_specs`'s output (origin/session/request_context/bearer_auth, optional daemon_token) for surface generation without booting real auth. See `auth/CLAUDE.md` §Middleware for the real stack. |
38
- | `create_stub_app_server_context(session_options)` | Stub `AppServerContext` — rate limiters null, `bootstrap_status.available: false`, `app_settings.open_signup: false`. |
39
- | `create_test_app_surface_spec(options)` | Builds an `AppSurfaceSpec` that mirrors `create_app_server`'s route assembly: consumer routes + factory-managed bootstrap routes (prefixed via `bootstrap_route_prefix`, default `'/api/account'`) + stub middleware + surface generation. `CreateTestAppSurfaceSpecOptions` accepts `session_options`, `create_route_specs`, `env_schema?`, `event_specs?`, `rpc_endpoints?`, `ws_endpoints?`, `transform_middleware?`, `bootstrap_route_prefix?`. Single source of truth for attack-surface tests — track `create_app_server` wiring changes here. |
24
+ - `create_throwing_stub<T>(label)` — Proxy whose every property access throws `Throwing stub 'label' — unexpected access to 'prop'`. JS-internal probes return `undefined`; `toJSON` returns `"[throwing_stub:label]"` so accidental serialization is visible rather than `{}`.
25
+ - `create_noop_stub<T>(label, overrides?)` Proxy whose every method returns `async () => undefined`; `overrides` pins specific props.
26
+ - `stub` pre-built throwing stub labelled `'stub'`.
27
+ - `create_stub_db()` real `Db` whose `client.query` yields `{rows: []}` and `transaction(fn)` synchronously calls `fn(inner_stub_db)`. Safe for `apply_route_specs`'s declarative transaction wrapper.
28
+ - `stub_handler()` fresh `Response('stub')`.
29
+ - `stub_mw` pass-through middleware (`async (_c, next) => next()`).
30
+ - `stub_app_deps` frozen `AppDeps`, every capability throwing, `audit` a no-op `AuditEmitter` from `create_test_audit_emitter`.
31
+ - `create_stub_app_deps()` — factory: fresh `AppDeps` with no-op FS/keyring/password, a `create_noop_stub` DB, silent `Logger`, no-op `audit`.
32
+ - `create_test_audit_emitter()` no-op `AuditEmitter`; `emit` / `emit_role_grant_target` no-op, `emit_pool` resolves immediately, `notify` no-op, `on_event_chain` empty.
33
+ - `create_stub_audit_sse()` no-op `AuditLogSse` for surface-test wiring without booting real SSE. `subscribe` returns a no-op cleanup; `on_audit_event` no-op; `registry` is a fresh `SubscriberRegistry` (live `.size` / `.close_*` for registry-state tests, isolated per call). For real SSE plumbing build via `create_audit_log_sse` against `create_test_app`.
34
+ - `create_stub_api_middleware({include_daemon_token?})` stub `MiddlewareSpec[]` matching `create_auth_middleware_specs`'s output (origin/session/request_context/bearer_auth, optional daemon_token) for surface generation without booting real auth. See `auth/CLAUDE.md` §Middleware for the real stack.
35
+ - `create_stub_app_server_context(session_options)` stub `AppServerContext`; rate limiters null, `bootstrap_status.available: false`, `app_settings.open_signup: false`.
36
+ - `create_test_app_surface_spec(options)` builds an `AppSurfaceSpec` mirroring `create_app_server`'s route assembly (consumer routes + stub middleware + surface generation). `CreateTestAppSurfaceSpecOptions`: `session_options`, `create_route_specs`, `env_schema?`, `event_specs?`, `rpc_endpoints?`, `ws_endpoints?`, `transform_middleware?`, `bootstrap?`. Bootstrap is opt-in (symmetric with `create_app_server` — omit to skip; pass the same value as prod to mount routes at `bootstrap.route_prefix ?? '/api/account'`). Single source of truth for attack-surface tests — track `create_app_server` wiring changes here.
40
37
 
41
38
  Throwing stubs surface mock escape: a test that accidentally reaches into
42
39
  stub territory breaks immediately with a label-scoped error rather than
43
40
  silently returning `undefined` or `{}`. Use throwing stubs by default;
44
- use no-op stubs only when a dep is known to be reached with a don't-care
45
- result.
41
+ no-op stubs only when a dep is known to be reached with a don't-care result.
46
42
 
47
43
  ### `entities.ts` — test entity factories
48
44
 
49
- Plain `(overrides?) => Entity` constructors with sensible defaults —
50
- callers set only the fields the test cares about. Names prefix with
51
- `create_test_*` to avoid collisions with real `account_queries.ts`
52
- factories.
45
+ Plain `(overrides?) => Entity` constructors with sensible defaults — callers
46
+ set only the fields the test cares about. `create_test_*` prefix avoids
47
+ collisions with real `account_queries.ts` factories. Override types widen
48
+ branded `Uuid` fields to `string` so tests pass literal ids without per-site
49
+ casts — the factory brands internally. Exported as `TestAccountOverrides` /
50
+ `TestActorOverrides` / `TestRoleGrantOverrides` / `TestAuditEventOverrides`.
53
51
 
54
- Override types widen branded `Uuid` fields to `string` so tests pass
55
- literal ids without per-site caststhe factory brands internally.
56
- Exported as `TestAccountOverrides` / `TestActorOverrides` /
57
- `TestRoleGrantOverrides` / `TestAuditEventOverrides`.
58
-
59
- | Factory | Default id / role |
60
- | ------------------------------------- | --------------------------------------------------------------------------------------------- |
61
- | `create_test_account(overrides?)` | `{id: 'acct-test', username: 'test_user', …}` |
62
- | `create_test_actor(overrides?)` | `{id: 'actor-test', account_id: 'acct-test', …}` |
63
- | `create_test_role_grant(overrides?)` | `{id: 'role-grant-test', actor_id: 'actor-test', role: 'admin', scope_id: null, …}` |
64
- | `create_test_context(role_grants?)` | `{account, actor, role_grants}` — pass `[{role: 'keeper'}, {role: 'admin'}]` for multi-role. |
65
- | `create_test_audit_event(overrides?)` | `{id: 'evt-test', event_type: 'login', outcome: 'success', …}` — for SSE guard / audit tests. |
52
+ - `create_test_account(overrides?)` `{id: 'acct-test', username: 'test_user', …}`
53
+ - `create_test_actor(overrides?)``{id: 'actor-test', account_id: 'acct-test', …}`
54
+ - `create_test_role_grant(overrides?)` `{id: 'role-grant-test', actor_id: 'actor-test', role: 'admin', scope_id: null, …}`
55
+ - `create_test_context(role_grants?)` `{account, actor, role_grants}`; pass `[{role: 'keeper'}, {role: 'admin'}]` for multi-role.
56
+ - `create_test_audit_event(overrides?)` — `{id: 'evt-test', event_type: 'login', outcome: 'success', …}`, for SSE guard / audit tests.
66
57
 
67
58
  ### `mock_fs.ts` — in-memory filesystem
68
59
 
69
60
  `create_mock_fs(initial_files?) => {read_file, write_file, get_file}`.
70
61
  Missing-path reads throw an `Error` with `.code = 'ENOENT'` so callers
71
- exercise the same branches as `node:fs`. Use for DI-based filesystem
72
- tests; never replaces `node:fs` globally.
62
+ exercise the same branches as `node:fs`. DI-based filesystem tests only;
63
+ never replaces `node:fs` globally.
73
64
 
74
65
  ### `db_entities.ts` — DB-backed entity factories
75
66
 
76
67
  `create_test_account_with_actor(db, {username, password_hash?})` wraps
77
- `query_create_account_with_actor` with a default `password_hash` (`'hash'`).
68
+ `query_create_account_with_actor` with default `password_hash` (`'hash'`).
78
69
  Returns `{account, actor}`. Replaces the per-file `create_user` /
79
70
  `create_test_actor` / `create_test_account` helpers that had accumulated
80
- across the auth test suite. Use for query-level tests that need real
81
- DB rows but not a full session/token bundle. For tests that also need
82
- an API token + session cookie + role_grants, use `bootstrap_test_account`
83
- from `app_server.ts` instead.
71
+ across the auth test suite. Use for query-level tests needing real DB rows
72
+ but not a full session/token bundle. For tests also needing an API token +
73
+ session cookie + role_grants, use `bootstrap_test_keeper` from `app_server.ts`.
74
+
75
+ `create_test_role_grant_direct(db, input)` wraps `query_create_role_grant`
76
+ for tests needing an active role_grant seeded directly, bypassing the
77
+ production offer/accept consent flow. Use only when the test focuses on
78
+ revoke or isolation semantics rather than the consent path — the schema
79
+ permits null `source_offer_id` for exactly this case. For tests exercising
80
+ the production grant flow, drive `role_grant_offer_and_accept` from
81
+ `role_grant_helpers.ts` instead.
82
+
83
+ ### `role_grant_helpers.ts` — RPC-flow role_grant helpers
84
+
85
+ `role_grant_offer_and_accept({app, rpc_path, grantor, recipient, role})`
86
+ drives the full consent flow (grantor `role_grant_offer_create` → recipient
87
+ `role_grant_offer_accept`) over the production RPC surface and returns
88
+ `{offer_id, role_grant_id}`. Sibling to `create_test_role_grant_direct` —
89
+ that one bypasses the consent flow; this one exercises it end-to-end so the
90
+ suite picks up post-commit fan-out (audit, SSE broadcasts, `_supersede`
91
+ notifications) a direct DB seed would miss. `grantor` and `recipient` accept
92
+ `TestApp | TestAccount` / `TestAccount` so the call site passes the same
93
+ object that already owns the headers + account id, ruling out caller-side
94
+ mismatch.
84
95
 
85
96
  ### `audit_drift_guard.ts` — audit-emission validation
86
97
 
87
- | Helper | Role |
88
- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
89
- | `install_audit_drift_guard()` | `beforeEach` resets + `afterEach` zero-checks the `audit_metadata_validation_failures` + `audit_unknown_event_type_failures` counters from `auth/audit_log_queries.ts`. Call once at the top of any `describe_db` block that fires audit emits — production validation is fail-open, so without this any regression that ships a typo'd `event_type` or an undeclared metadata field is silent. Pair with `await_pending_effects: true` (the `create_test_app` default) so fire-and-forget audit writes have completed by response time. |
90
- | `create_emit_ordering_audit_factory<E>(seq_ref, events_ref, build_inner)` | Returns an `AuditFactory` that wraps the result of `build_inner({db, log})` so every `emit` call pushes `{kind: 'emit', at: seq.value++}` into a shared sequence + events array. Pass through `create_test_app({audit_factory: …})` — the test backend invokes it with its constructed `{db, log}` and lands the wrapped emitter on `deps.audit`. Generic `E extends {kind: string; at: number}` so the events array typechecks against the caller's own `close` / custom marker shape. Pair with `create_recording_closer(seq_ref)` (in `connection_closer_helpers.ts`) for close-vs-emit ordering tests. Scope is `emit` only — `emit_role_grant_target`, `emit_pool`, `notify` forward to the inner emitter unwrapped (same caveat as the previous `patch_audit_emit_capture`). |
91
- | `AuditEmitMarker` | `{kind: 'emit'; at: number}` — the type of marker `create_emit_ordering_audit_factory` pushes. |
92
- | `create_recording_audit_emitter(calls_ref?)` | Build a no-op `AuditEmitter` that pushes every `emit` and `emit_pool` call into `calls`. Pass `calls_ref` to write into a caller-owned array; omit to let the helper allocate one. Returns `{emitter, calls}` — destructure `emitter` as the `audit` dep and read `calls` to assert on captured metadata. Replaces per-file capturing emitters previously duplicated across `password_change.test.ts`, `audit_log.test.ts`, etc. |
93
- | `RecordingAuditEmitter` | `{emitter: AuditEmitter; calls: Array<AuditLogInput>}` — return shape of `create_recording_audit_emitter`. |
98
+ - `install_audit_drift_guard()` — `beforeEach` resets + `afterEach` zero-checks `audit_metadata_validation_failures` + `audit_unknown_event_type_failures` counters from `auth/audit_log_queries.ts`. Call once at the top of any `describe_db` block firing audit emits — production validation is fail-open, so without this any regression shipping a typo'd `event_type` or undeclared metadata field is silent. Pair with `await_pending_effects: true` (the `create_test_app` default) so fire-and-forget audit writes complete by response time.
99
+ - `create_emit_ordering_audit_factory<E>(seq_ref, events_ref, build_inner)` — returns an `AuditFactory` wrapping `build_inner({db, log})` so every `emit` pushes `{kind: 'emit', at: seq.value++}` into a shared sequence + events array. Pass through `create_test_app({audit_factory: …})` — the test backend invokes it with its `{db, log}` and lands the wrapped emitter on `deps.audit`. Generic `E extends {kind: string; at: number}` so the events array typechecks against the caller's own `close` / custom marker shape. Pair with `create_recording_closer(seq_ref)` for close-vs-emit ordering tests. Scope is `emit` only — `emit_role_grant_target`, `emit_pool`, `notify` forward to the inner emitter unwrapped.
100
+ - `AuditEmitMarker` `{kind: 'emit'; at: number}`, the marker type pushed.
101
+ - `create_recording_audit_emitter(calls_ref?)` no-op `AuditEmitter` pushing every `emit` and `emit_pool` call into `calls`. Pass `calls_ref` to write into a caller-owned array; omit to let the helper allocate. Returns `{emitter, calls}` destructure `emitter` as the `audit` dep and read `calls` to assert. Replaces per-file capturing emitters previously duplicated across `password_change.test.ts`, `audit_log.test.ts`, etc.
102
+ - `RecordingAuditEmitter` `{emitter: AuditEmitter; calls: Array<AuditLogInput>}`.
94
103
 
95
104
  ### `connection_closer_helpers.ts` — `ConnectionCloser` test doubles
96
105
 
97
- | Helper | Role |
98
- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
99
- | `create_recording_closer(seq_ref?)` | Returns `{closer, calls}` where every method on `closer` records `{method, id, at}` into `calls`. Pass `seq_ref` to share the sequence counter with `create_emit_ordering_audit_factory` so close + emit markers compose for ordering tests. |
100
- | `assert_close_call(call, method, id)` | Pins `{method, id}` on a single recorded close call without baking in the `at: N` sequence number. Use at every "did the closer fire?" assertion site; reserve `at: N` assertions for the dedicated ordering test paired with the capture helper. |
101
- | `RecordedClose` | `{method: 'session' \| 'token' \| 'account', id, at}` — recorded shape pushed by the closer. |
102
- | `RecordingCloser` | `{closer, calls}` — return shape of `create_recording_closer`. |
106
+ - `create_recording_closer(seq_ref?)` — `{closer, calls}`; every method on `closer` records `{method, id, at}` into `calls`. Pass `seq_ref` to share the sequence counter with `create_emit_ordering_audit_factory` so close + emit markers compose for ordering tests.
107
+ - `assert_close_call(call, method, id)` — pins `{method, id}` on a recorded close call without baking in the `at: N` sequence number. Use at every "did the closer fire?" site; reserve `at: N` assertions for the dedicated ordering test paired with the capture helper.
108
+ - `RecordedClose` `{method: 'session' | 'token' | 'account', id, at}`.
109
+ - `RecordingCloser` `{closer, calls}`.
103
110
 
104
111
  ## Database — `db.ts`
105
112
 
@@ -107,30 +114,27 @@ Factory builders for parameterized DB tests. Consumer projects pass their
107
114
  `init_schema` callback (which calls `run_migrations(db, [auth_migration_ns, ...app_migrations])`);
108
115
  factories accept any migration namespace set.
109
116
 
110
- | Helper | Role |
111
- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
112
- | `IS_CI` | `process.env.CI === 'true'` CI detection. |
113
- | `DbFactory` interface | `{name, create, close, skip, skip_reason?}`. |
114
- | `reset_pglite(db)` | `DROP SCHEMA public CASCADE` + recreate. Reuses a live PGlite instance. |
115
- | `create_pglite_factory(init_schema)` | In-memory; no external deps; `skip: false`. See WASM caching below. |
116
- | `create_pg_factory(init_schema, test_url?)` | PostgreSQL; `skip: true` when `test_url` is missing; drops `schema_version` before `init_schema` so migrations re-evaluate against actual tables (prevents stale tracker rows from skipping migrations when DDL changes between test sessions); pool is reused + cleaned up across `create()` calls. |
117
- | `auth_truncate_tables` | `['invite', 'api_token', 'auth_session', 'role_grant', 'role_grant_offer', 'actor', 'account']` in FK-safe order. Excludes `audit_log` unit DB tests don't need to truncate it. |
118
- | `auth_integration_truncate_tables` | `auth_truncate_tables + ['audit_log']` for integration suites that exercise the audit path. |
119
- | `auth_drop_tables` | Full set from `auth_migrations` in drop order; call `drop_auth_schema(db)` at the top of `init_schema` on persistent pg databases that may hold stale DDL from previous fuz_app versions. |
120
- | `drop_auth_schema(db)` | `DROP TABLE IF EXISTS <table> CASCADE` for every entry in `auth_drop_tables` plus `schema_version`. Safe on fresh DBs. |
121
- | `create_describe_db(factories, truncate_tables)` | Returns `describe_db(name, fn)` that runs `fn(get_db)` once per factory, inside a `describe` block with shared `beforeAll(create)` + `beforeEach(TRUNCATE)` + `afterAll(close)`. Skipped factories use `describe.skip`. |
122
- | `log_db_factory_status(factories)` | Console summary of enabled / skipped factories. |
117
+ - `IS_CI` — `process.env.CI === 'true'`.
118
+ - `DbFactory` `{name, create, close, skip, skip_reason?}`.
119
+ - `reset_pglite(db)` `DROP SCHEMA public CASCADE` + recreate. Reuses a live PGlite instance.
120
+ - `create_pglite_factory(init_schema)` in-memory; no external deps; `skip: false`. See WASM caching below.
121
+ - `create_pg_factory(init_schema, test_url?)` — PostgreSQL; `skip: true` when `test_url` missing. Drops `schema_version` before `init_schema` so migrations re-evaluate against actual tables (prevents stale tracker rows from skipping migrations when DDL changes between sessions). Pool reused + cleaned up across `create()` calls.
122
+ - `auth_truncate_tables` `['invite', 'api_token', 'auth_session', 'role_grant', 'role_grant_offer', 'actor', 'account']` in FK-safe order. Excludes `audit_log` — unit DB tests don't need to truncate it.
123
+ - `auth_integration_truncate_tables` `auth_truncate_tables + ['audit_log']` for integration suites that exercise the audit path.
124
+ - `auth_drop_tables` full set from `auth_migrations` in drop order; call `drop_auth_schema(db)` at the top of `init_schema` on persistent pg databases that may hold stale DDL from previous fuz_app versions.
125
+ - `drop_auth_schema(db)` `DROP TABLE IF EXISTS <table> CASCADE` for every entry in `auth_drop_tables` plus `schema_version`. Safe on fresh DBs.
126
+ - `create_describe_db(factories, truncate_tables)` returns `describe_db(name, fn)` running `fn(get_db)` once per factory inside a `describe` with shared `beforeAll(create)` + `beforeEach(TRUNCATE)` + `afterAll(close)`. Skipped factories use `describe.skip`.
127
+ - `log_db_factory_status(factories)` console summary of enabled / skipped factories.
123
128
 
124
129
  **PGlite WASM caching.** `create_pglite_factory` shares a single PGlite
125
130
  instance in a module-level ref (`module_db`) across all factories in the
126
131
  same vitest worker thread. Subsequent `create()` calls
127
- `DROP SCHEMA public CASCADE` instead of paying the ~500–700ms WASM
128
- cold-start cost again. Since each vitest file runs in its own worker,
129
- there is no cross-file contamination — but inside a file, suites share
130
- state until the schema is reset. The `db` vitest project (opted into by
131
- the `.db.test.ts` suffix) runs with `isolate: false` +
132
- `fileParallelism: false` to amortize the WASM boot across every DB test
133
- file in the run.
132
+ `DROP SCHEMA public CASCADE` instead of paying the ~500–700ms WASM cold-start
133
+ cost again. Each vitest file runs in its own worker, so no cross-file
134
+ contamination — but inside a file, suites share state until the schema is
135
+ reset. The `db` vitest project (opted into by the `.db.test.ts` suffix) runs
136
+ with `isolate: false` + `fileParallelism: false` to amortize WASM boot across
137
+ every DB test file.
134
138
 
135
139
  ## Test app assembly
136
140
 
@@ -143,41 +147,44 @@ fully assembled Hono app + the backend + helpers.
143
147
 
144
148
  Key module-scope values:
145
149
 
146
- - `stub_password_deps` — `PasswordHashDeps` that hashes via
147
- `stub_hash_${password}` and verifies by equality. Deterministic, no
148
- Argon2 costuse for every test that isn't specifically exercising
149
- password hashing.
150
- - `TEST_COOKIE_SECRET` 64-hex-char deterministic cookie secret.
151
- Produces a valid `Keyring` via `create_validated_keyring`. Never used
152
- in production — the stub guard plus fixed value is the contract.
153
- - `fallback_pglite_factory` — module-level PGlite factory that
154
- `create_test_app_server` uses when no `db` is passed. Reuses the WASM
155
- cache via `create_pglite_factory`.
156
-
157
- `bootstrap_test_account(options)` is extracted because both
158
- `create_test_app_server` and `TestApp.create_account` reuse the same
159
- "insert account + actor + roles + API token + session + cookie" flow.
160
- Takes `{db, keyring, session_options, password, username?, password_value?, roles?}`.
161
-
162
- | Type | Shape |
163
- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
164
- | `TestAppServer extends AppBackend` | Adds `account`, `actor`, `api_token`, `session_cookie`, `keyring`, `cleanup()`. |
165
- | `TestAppServerOptions` | `session_options` (required), optional `db`, `db_type`, `password`, `username`, `password_value`, `roles`, `audit_factory`. The optional `audit_factory` defaults to `default_audit_factory` (no-listener `create_audit_emitter` over the test backend's `{db, log}`); pass a custom factory to compose `on_audit_event` / `audit_log_config`, wrap with `emit_decorator` (via `create_emit_ordering_audit_factory`), or otherwise replace the emitter. Mirrors `CreateAppBackendOptions` end-to-end — the previous `on_audit_event` / `audit_log_config` sugar was removed alongside the production rename. |
166
- | `CreateTestAppOptions extends TestAppServerOptions` | Adds `create_route_specs` (required), `rpc_endpoints?: RpcEndpointsSuiteOption` (top-level only single source of truth, symmetric with the suite-level option), and `app_options?: SuiteAppOptions` (`Partial<AppServerOptions>` excluding the four fields the helper manages: `backend`, `session_options`, `create_route_specs`, `rpc_endpoints`). |
167
- | `TestAccount` | `{account, actor, session_cookie, api_token, create_session_headers, create_bearer_headers}`. |
168
- | `TestApp` | `{app, backend, surface_spec, surface, route_specs, create_session_headers, create_bearer_headers, create_daemon_token_headers, create_account, cleanup}`. |
150
+ - `stub_password_deps` — `PasswordHashDeps` hashing via `stub_hash_${password}` and verifying by equality. Deterministic, no Argon2 cost — use for every test not specifically exercising password hashing.
151
+ - `TEST_COOKIE_SECRET` 64-hex-char deterministic cookie secret. Produces a valid `Keyring` via `create_validated_keyring`. Never used in production — the stub guard plus fixed value is the contract.
152
+ - `fallback_pglite_factory`module-level PGlite factory `create_test_app_server` uses when no `db` is passed. Reuses the WASM cache via `create_pglite_factory`.
153
+
154
+ Two helpers share the "insert account + actor + roles + API token + session +
155
+ cookie" flow, split by intent:
156
+
157
+ - `bootstrap_test_keeper(options)` — keeper path used by `create_test_app_server`. Same body as the general helper plus a lock flip (`UPDATE bootstrap_lock SET bootstrapped = true ...`) so test DB state matches a real bootstrap completion, letting production code trust the lock as the single signal.
158
+ - `create_test_account_with_credentials(options)` general path used by `TestApp.create_account` for additional non-keeper accounts. Same body, no lock interaction (additional accounts aren't bootstraps).
159
+
160
+ Both take `{db, keyring, session_options, password, username?, password_value?, roles?}`
161
+ (shared `CreateTestAccountWithCredentialsOptions` / `BootstrapTestKeeperOptions`).
162
+
163
+ For exercising the bootstrap success path end-to-end against an empty DB (no
164
+ pre-keeper, lock unflipped), use `create_test_app_for_bootstrap` pair with
165
+ `describe_bootstrap_success_tests` for the consumer-runnable suite.
166
+
167
+ Types:
168
+
169
+ - `TestAppServer extends AppBackend` adds `account`, `actor`, `api_token`, `session_cookie`, `keyring`, `cleanup()`.
170
+ - `TestAppServerOptions` `session_options` (required), optional `db`, `db_type`, `password`, `username`, `password_value`, `roles`, `audit_factory`. The optional `audit_factory` defaults to `default_audit_factory` (no-listener `create_audit_emitter` over the test backend's `{db, log}`); pass a custom factory to compose `on_audit_event` / `audit_log_config`, wrap with `emit_decorator` (via `create_emit_ordering_audit_factory`), or otherwise replace the emitter. Mirrors `CreateAppBackendOptions` end-to-end — the previous `on_audit_event` / `audit_log_config` sugar was removed alongside the production rename.
171
+ - `CreateTestAppOptions extends TestAppServerOptions` — adds `create_route_specs` (required), `rpc_endpoints?: RpcEndpointsSuiteOption` (top-level only — single source of truth, symmetric with the suite-level option), `bootstrap?: BootstrapServerOptions` (top-level only — same precedent as `rpc_endpoints`), and `app_options?: SuiteAppOptions` (`Partial<AppServerOptions>` excluding the five fields the helper manages: `backend`, `session_options`, `create_route_specs`, `rpc_endpoints`, `bootstrap`).
172
+ - `TestAccount` `{account, actor, session_cookie, api_token, create_session_headers, create_bearer_headers}`.
173
+ - `TestApp` — `{app, backend, surface_spec, surface, route_specs, create_session_headers, create_bearer_headers, create_daemon_token_headers, create_account, cleanup}`.
174
+ - `CreateTestAppForBootstrapOptions` — `{session_options, create_route_specs, rpc_endpoints?, bootstrap: BootstrapLiveOptions, bootstrap_token, app_options?, db?, db_type?, password?, audit_factory?}`. `bootstrap` is required + narrowed to `live` mode (the helper exists for the success-path test).
175
+ - `TestAppForBootstrap` — `{app, backend, surface_spec, surface, route_specs, create_request_headers, cleanup}`. No keeper credentials (test drives bootstrap itself).
169
176
 
170
177
  `create_test_app` hard-codes the test-friendly `AppServerOptions`:
171
- `allowed_origins: [/^http:\/\/localhost/]`, stub proxy pinned to
172
- `127.0.0.1`, `env_schema: z.object({})`, every rate limiter `null`,
173
- static daemon token state (no rotation, keeper already set),
174
- **`await_pending_effects: true`** (fire-and-forget effects complete
175
- before the response returns so tests can assert on side effects inline),
176
- and silent logger. Override via `app_options`.
178
+ `allowed_origins: [/^http:\/\/localhost/]`, stub proxy pinned to `127.0.0.1`,
179
+ `env_schema: z.object({})`, every rate limiter `null`, static daemon token
180
+ state (no rotation, keeper already set),
181
+ **`await_pending_effects: true`** (fire-and-forget effects complete before
182
+ the response returns so tests can assert on side effects inline), and silent
183
+ logger. Override via `app_options`.
177
184
 
178
- A fresh Hono app is created on every call because middleware closures
179
- bind to the server's deps (db, keyring). Hono assembly is cheap
180
- (~10–50ms); PGlite WASM caching in `db.ts` is where the real savings are.
185
+ A fresh Hono app is created on every call because middleware closures bind
186
+ to the server's deps (db, keyring). Hono assembly is cheap (~10–50ms);
187
+ PGlite WASM caching in `db.ts` is where the real savings are.
181
188
 
182
189
  ### `auth_apps.ts` — adversarial-auth app factories
183
190
 
@@ -185,62 +192,86 @@ Pre-built Hono apps at each auth level (public / authed / keeper / per-role)
185
192
  for attack-surface testing. No middleware stack — a single `/*` middleware
186
193
  injects `ACCOUNT_ID_KEY` + `REQUEST_CONTEXT_KEY` + `CREDENTIAL_TYPE_KEY`
187
194
  (default `'session'`) plus the `TEST_CONTEXT_PRESET_KEY` flag (so the
188
- dispatcher's authorization phase trusts the pre-baked context and skips
189
- its DB-backed actor resolution), then hands off to `apply_route_specs`
190
- with `fuz_auth_guard_resolver` + `create_fuz_authorization_handler`.
191
- Production middleware never sets `TEST_CONTEXT_PRESET_KEY`, so the escape
192
- hatch is test-only by construction.
193
-
194
- | Helper | Role |
195
- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
196
- | `create_test_request_context(role?)` | Minimal `RequestContext` one account, one actor, one role_grant for `role` (or none). |
197
- | `create_test_app_from_specs(specs, auth_ctx?, credential_type?)` | Hono app with pre-set context + `apply_route_specs`. `credential_type` defaults to `'session'` when an auth context is supplied — override for `'daemon_token'` / `'api_token'` tests. |
198
- | `AuthTestApps` | `{public, authed, keeper, by_role: Map<string, Hono>}`. |
199
- | `create_auth_test_apps(specs, roles)` | Builds one app per auth level. Keeper app uses `credential_type: 'daemon_token'` so `require_credential_types(['daemon_token'])` passes. |
200
- | `select_auth_app(apps, auth)` | Map `RouteAuth` → matching Hono app. Throws for missing `role:*` entries. |
201
- | `resolve_test_path(path)` | `:foo` → `test_foo` — adequate for routes without format-constrained params. |
195
+ dispatcher's authorization phase trusts the pre-baked context and skips its
196
+ DB-backed actor resolution), then hands off to `apply_route_specs` with
197
+ `fuz_auth_guard_resolver` + `create_fuz_authorization_handler`. Production
198
+ middleware never sets `TEST_CONTEXT_PRESET_KEY`, so the escape hatch is
199
+ test-only by construction.
200
+
201
+ - `create_test_request_context(role?)` — minimal `RequestContext`: one account, one actor, one role_grant for `role` (or none).
202
+ - `create_test_app_from_specs(specs, auth_ctx?, credential_type?)` — Hono app with pre-set context + `apply_route_specs`. `credential_type` defaults to `'session'` when an auth context is supplied — override for `'daemon_token'` / `'api_token'` tests.
203
+ - `AuthTestApps` `{public, authed, keeper, by_role: Map<string, Hono>}`.
204
+ - `create_auth_test_apps(specs, roles)` builds one app per auth level. Keeper app uses `credential_type: 'daemon_token'` so `require_credential_types(['daemon_token'])` passes.
205
+ - `select_auth_app(apps, auth)` — map `RouteAuth` matching Hono app. Throws for missing `role:*` entries.
206
+ - `resolve_test_path(path)` `:foo` `test_foo`; adequate for routes without format-constrained params.
207
+
208
+ ## Cross-impl schema parity
209
+
210
+ ### `schema_introspect.ts` — `query_schema_snapshot`
211
+
212
+ - `query_schema_snapshot(db, options?)` — introspects a live DB into a deterministic `SchemaSnapshot` via `pg_catalog` + `information_schema`. Captures tables, columns (with `udt_name` to distinguish int4/int8), indexes (`indexdef`), constraints (`pg_get_constraintdef`), sequences, and `schema_version` rows.
213
+ - `SchemaSnapshot` — fully JSON-serializable; every collection deterministically sorted on capture so structural equality is stable across runs. `applied_at` is excluded from `schema_version` rows so timestamps don't drift the snapshot.
214
+
215
+ ### `schema_parity.ts` — `assert_schema_snapshots_equal`
216
+
217
+ - `diff_schema_snapshots(a, b)` — structured `Array<SchemaDiff>` between two snapshots; empty array means parity holds.
218
+ - `format_schema_diffs(diffs, labels?)` — human-readable multi-line rendering; labels name the impl on each side (e.g., `{a: 'deno', b: 'rust'}`).
219
+ - `assert_schema_snapshots_equal(a, b, labels?)` — throws on drift with a fully-formatted diff message.
220
+ - `SchemaDiff` — tagged-union per drift kind: `schema_version_only_in`, `schema_version_sequence_differs`, `table_only_in`, `column_only_in`, `column_field_differs`, `index_only_in`, `index_definition_differs`, `constraint_only_in`, `constraint_differs`, `sequence_only_in`, `sequence_data_type_differs`.
221
+
222
+ **Cross-impl gate pattern** — consumers running two backends against a
223
+ shared schema (zzz's `--backend=both`, fuz_app's cross-backend suite)
224
+ bootstrap each impl against an isolated DB, snapshot, then compare:
225
+
226
+ ```ts
227
+ await drop_recreate_db('zzz_test');
228
+ await spawn_backend(deno_config);
229
+ const snapshot_deno = await query_schema_snapshot(db);
230
+ await drop_recreate_db('zzz_test');
231
+ await spawn_backend(rust_config);
232
+ const snapshot_rust = await query_schema_snapshot(db);
233
+ assert_schema_snapshots_equal(snapshot_deno, snapshot_rust, {a: 'deno', b: 'rust'});
234
+ ```
235
+
236
+ Each impl's _own_ tests still gate its DDL correctness independently — this
237
+ pair is purely the cross-impl drift check.
202
238
 
203
239
  ## Assertions, coverage, helpers
204
240
 
205
241
  ### `assertions.ts` — surface + error-schema assertions
206
242
 
207
- | Helper | Role |
208
- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
209
- | `resolve_fixture_path(filename, import_meta_url)` | Absolute path relative to the caller's module (use `import.meta.url`). |
210
- | `assert_surface_matches_snapshot(surface, path)` | Compares live `AppSurface` against a committed JSON snapshot; failure message instructs `gro gen`. |
211
- | `assert_surface_deterministic(build_surface)` | Build twice, `deepStrictEqual` the two results catches nondeterminism in surface generation. |
212
- | `assert_only_expected_public_routes(surface, list)` | Bidirectional: no unexpected public routes, no missing expected ones. Format: `['GET /health', 'POST /api/account/login']`. |
213
- | `assert_full_middleware_stack(surface, prefix, mws)` | Every route under `prefix` has exactly `mws` as its middleware chain. |
214
- | `get_route_error_schema(lookup, route, status)` | Read out of a pre-built merged-error-schema map. |
215
- | `assert_error_schema_valid(lookup, route, status, body)` | Assert a schema exists + parses the body. |
243
+ - `resolve_fixture_path(filename, import_meta_url)` — absolute path relative to the caller's module (use `import.meta.url`).
244
+ - `assert_surface_matches_snapshot(surface, path)` compares live `AppSurface` against a committed JSON snapshot; failure message instructs `gro gen`.
245
+ - `assert_surface_deterministic(build_surface)` build twice, `deepStrictEqual` results; catches nondeterminism in surface generation.
246
+ - `assert_only_expected_public_routes(surface, list)` bidirectional: no unexpected public routes, no missing expected ones. Format: `['GET /health', 'POST /api/account/login']`.
247
+ - `assert_full_middleware_stack(surface, prefix, mws)` every route under `prefix` has exactly `mws` as its middleware chain.
248
+ - `get_route_error_schema(lookup, route, status)` reads from a pre-built merged-error-schema map.
249
+ - `assert_error_schema_valid(lookup, route, status, body)` assert a schema exists + parses the body.
216
250
 
217
251
  ### `surface_invariants.ts` — structural + policy invariants
218
252
 
219
- Structural invariants (options-free, apply universally):
220
-
221
- | Assertion | Checks |
222
- | ----------------------------------------- | ------------------------------------------------------------------------------------------------ |
223
- | `assert_protected_routes_declare_401` | Every protected route has 401 in `error_schemas`. |
224
- | `assert_role_routes_declare_403` | Every role/keeper route has 403. |
225
- | `assert_input_routes_declare_400` | Every route with input has 400. |
226
- | `assert_params_routes_declare_400` | Every route with params has 400. |
227
- | `assert_query_routes_declare_400` | Every route with query has 400. |
228
- | `assert_descriptions_present` | Every route has a non-empty description. |
229
- | `assert_no_duplicate_routes` | No duplicate method+path pairs. |
230
- | `assert_middleware_errors_propagated` | Every middleware-declared error status appears on every applicable route. |
231
- | `assert_error_schemas_structurally_valid` | Every declared error schema has an `error` property at the top level (matches `ApiError`). |
232
- | `assert_error_code_status_consistency` | The same `z.literal()` error code never appears at two different HTTP statuses. |
233
- | `assert_404_schemas_use_specific_errors` | Routes with params declaring 404 must use `z.literal()` or `z.enum()`, not generic `z.string()`. |
234
-
235
- RPC / WS structural invariants (options-free, apply universally over
236
- `surface.rpc_endpoints` + `surface.ws_endpoints`):
237
-
238
- | Assertion | Checks |
239
- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
240
- | `assert_rpc_method_descriptions_present` | Every RPC method on every endpoint has a non-empty `description`. |
241
- | `assert_ws_method_descriptions_present` | Every WS method on every endpoint has a non-empty `description`. |
242
- | `assert_ws_endpoints_include_protocol_actions` | Every WS endpoint includes `heartbeat` + `cancel` (the `protocol_actions` spread from `actions/protocol.js`). |
243
- | `assert_ws_notifications_have_null_auth` | WS method `kind === 'remote_notification' ⟺ auth === null` — guards against drift between spec union and surface emitter. |
253
+ Structural invariants (options-free, universal):
254
+
255
+ - `assert_protected_routes_declare_401` — every protected route has 401 in `error_schemas`.
256
+ - `assert_role_routes_declare_403` every role/keeper route has 403.
257
+ - `assert_input_routes_declare_400` every route with input has 400.
258
+ - `assert_params_routes_declare_400` every route with params has 400.
259
+ - `assert_query_routes_declare_400` every route with query has 400.
260
+ - `assert_descriptions_present` every route has a non-empty description.
261
+ - `assert_no_duplicate_routes` no duplicate method+path pairs.
262
+ - `assert_middleware_errors_propagated` every middleware-declared error status appears on every applicable route.
263
+ - `assert_error_schemas_structurally_valid` every declared error schema has an `error` property at the top level (matches `ApiError`).
264
+ - `assert_error_code_status_consistency` the same `z.literal()` error code never appears at two different HTTP statuses.
265
+ - `assert_404_schemas_use_specific_errors` routes with params declaring 404 must use `z.literal()` or `z.enum()`, not generic `z.string()`.
266
+
267
+ RPC / WS structural invariants (options-free, apply over `surface.rpc_endpoints`
268
+
269
+ - `surface.ws_endpoints`):
270
+
271
+ * `assert_rpc_method_descriptions_present` — every RPC method on every endpoint has a non-empty `description`.
272
+ * `assert_ws_method_descriptions_present` — every WS method on every endpoint has a non-empty `description`.
273
+ * `assert_ws_endpoints_include_protocol_actions` every WS endpoint includes `heartbeat` + `cancel` (the `protocol_actions` spread from `actions/protocol.js`).
274
+ * `assert_ws_notifications_have_null_auth` WS method `kind === 'remote_notification' auth === null`; guards against drift between spec union and surface emitter.
244
275
 
245
276
  Per-endpoint duplicate method names and the auth-shape biconditional are
246
277
  already enforced at startup by `compile_action_registry` (see
@@ -249,36 +280,18 @@ contract-surface concerns a runtime registration check cannot reach.
249
280
 
250
281
  Policy invariants (configurable, sensible defaults):
251
282
 
252
- | Assertion | Checks |
253
- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
254
- | `assert_sensitive_routes_rate_limited` | Routes matching `sensitive_route_patterns` (default: `/login`, `/password`, `/bootstrap`, `/tokens/create`) declare rate limiting or a 429 schema. |
255
- | `assert_no_unexpected_public_mutations` | Public mutation routes must be in `public_mutation_allowlist`. |
256
- | `assert_mutation_routes_use_post` | Routes with input schemas must not be GET (bypasses browser GET idempotency assumptions). |
257
- | `assert_keeper_routes_under_prefix` | Keeper routes must be under `keeper_route_prefixes` (default `['/api/']`). |
283
+ - `assert_sensitive_routes_rate_limited` — routes matching `sensitive_route_patterns` (default: `/login`, `/password`, `/bootstrap`, `/tokens/create`) declare rate limiting or a 429 schema.
284
+ - `assert_no_unexpected_public_mutations` public mutation routes must be in `public_mutation_allowlist`.
285
+ - `assert_mutation_routes_use_post` routes with input schemas must not be GET (bypasses browser GET idempotency assumptions).
286
+ - `assert_keeper_routes_under_prefix` keeper routes must be under `keeper_route_prefixes` (default `['/api/']`).
258
287
 
259
288
  Tightness audit:
260
289
 
261
- - `audit_error_schema_tightness(surface) => Array<ErrorSchemaAuditEntry>` —
262
- classifies every route × status combination as `'literal' | 'enum' | 'generic'`.
263
- - `assert_error_schema_tightness(surface, options?)` fails routes below a
264
- threshold (`min_specificity`, default `'enum'`) with `allowlist` + `ignore_statuses` escape hatches.
265
- - `fuz_app_stock_route_tightness_allowlist` currently empty. Every
266
- fuz_app-shipped route (account login/password/bootstrap/signup, db
267
- health/tables/:name/tables/:name/rows/:id) has been tightened in place to
268
- `z.enum([...])` / `z.literal(...)` against every emit-site code. Kept as a
269
- forward-compatibility hook for future stock routes that need an interim
270
- exemption; paths assume the standard `/api/account` + `/api/db` prefixes.
271
- - `default_error_schema_tightness` — `{ignore_statuses: [401, 403, 429], allowlist: fuz_app_stock_route_tightness_allowlist}`.
272
- Applied by `describe_standard_attack_surface_tests` when
273
- `error_schema_tightness` is omitted; pass an override config or `null` to
274
- opt out.
275
- - **Merge semantics in `describe_standard_attack_surface_tests`**:
276
- consumer-supplied `allowlist` and `ignore_statuses` are concatenated
277
- underneath the defaults (stock entries first, consumer entries last),
278
- so consumer allowlists are additive rather than replacing. Scalar fields
279
- like `min_specificity` are overwritten by the consumer. Exported as
280
- `resolve_standard_error_schema_tightness(consumer_options)` for consumers
281
- calling `assert_error_schema_tightness` directly outside the suite.
290
+ - `audit_error_schema_tightness(surface) => Array<ErrorSchemaAuditEntry>` — classifies every route × status combination as `'literal' | 'enum' | 'generic'`.
291
+ - `assert_error_schema_tightness(surface, options?)` fails routes below a threshold (`min_specificity`, default `'enum'`) with `allowlist` + `ignore_statuses` escape hatches.
292
+ - `fuz_app_stock_route_tightness_allowlist` — currently empty. Every fuz_app-shipped route (account login/password/bootstrap/signup, db health/tables/:name/tables/:name/rows/:id) has been tightened in place to `z.enum([...])` / `z.literal(...)` against every emit-site code. Kept as a forward-compatibility hook for future stock routes that need an interim exemption; paths assume the standard `/api/account` + `/api/db` prefixes.
293
+ - `default_error_schema_tightness` `{ignore_statuses: [401, 403, 429], allowlist: fuz_app_stock_route_tightness_allowlist}`. Applied by `describe_standard_attack_surface_tests` when `error_schema_tightness` is omitted; pass an override config or `null` to opt out.
294
+ - **Merge semantics in `describe_standard_attack_surface_tests`**: consumer-supplied `allowlist` and `ignore_statuses` are concatenated underneath the defaults (stock entries first, consumer entries last), so consumer allowlists are additive rather than replacing. Scalar fields like `min_specificity` are overwritten by the consumer. Exported as `resolve_standard_error_schema_tightness(consumer_options)` for consumers calling `assert_error_schema_tightness` directly outside the suite.
282
295
 
283
296
  Aggregate runners (called by the standard attack-surface suite):
284
297
 
@@ -290,80 +303,52 @@ Aggregate runners (called by the standard attack-surface suite):
290
303
 
291
304
  `ErrorCoverageCollector` tracks which declared error paths get exercised.
292
305
  Observations live in a `Set<string>` keyed by `"METHOD /spec-path:STATUS"` or
293
- `"METHOD /spec-path:STATUS:CODE"` — the two shapes coexist and a
294
- status-only observation satisfies the "any-code" coverage rule for all
295
- declared codes at that status.
306
+ `"METHOD /spec-path:STATUS:CODE"` — the two shapes coexist and a status-only
307
+ observation satisfies the "any-code" coverage rule for all declared codes at
308
+ that status.
296
309
 
297
310
  Methods:
298
311
 
299
- - `record(specs, method, path, status, code?)` — resolves concrete paths
300
- back to spec templates (e.g. `/api/accounts/abc` `/api/accounts/:id`).
301
- - `assert_and_record(specs, method, path, response, code?)` wraps
302
- `assert_response_matches_spec` and auto-extracts `body.error` from the
303
- JSON body via `response.clone()`. Pass an explicit `code` when the
304
- body was already consumed.
305
- - `uncovered(specs, options?)` — per-status rows for generic schemas,
306
- per-code rows for `z.literal` / `z.enum` schemas.
312
+ - `record(specs, method, path, status, code?)` — resolves concrete paths back to spec templates (e.g. `/api/accounts/abc` → `/api/accounts/:id`).
313
+ - `assert_and_record(specs, method, path, response, code?)` — wraps `assert_response_matches_spec` and auto-extracts `body.error` from the JSON body via `response.clone()`. Pass an explicit `code` when the body was already consumed.
314
+ - `uncovered(specs, options?)` per-status rows for generic schemas, per-code rows for `z.literal` / `z.enum` schemas.
307
315
 
308
316
  Support functions:
309
317
 
310
- - `extract_declared_error_codes(schema)` — reads `schema.shape.error`;
311
- returns the literal value(s) for `z.literal` / `z.enum`, `null`
312
- otherwise.
313
- - `assert_error_coverage(collector, specs, options?)` — logs
314
- `[error coverage] covered/total (N.M%)` with uncovered list; fails
315
- when `min_coverage > 0` and the ratio falls below.
316
- - `DEFAULT_INTEGRATION_ERROR_COVERAGE = 0.2` — conservative baseline
317
- for the standard integration/admin suites; consumers tighten as
318
- their own test coverage matures.
318
+ - `extract_declared_error_codes(schema)` — reads `schema.shape.error`; returns the literal value(s) for `z.literal` / `z.enum`, `null` otherwise.
319
+ - `assert_error_coverage(collector, specs, options?)` logs `[error coverage] covered/total (N.M%)` with uncovered list; fails when `min_coverage > 0` and the ratio falls below.
320
+ - `DEFAULT_INTEGRATION_ERROR_COVERAGE = 0.2` — conservative baseline for the standard integration/admin suites; consumers tighten as their own test coverage matures.
319
321
 
320
322
  ### `schema_generators.ts` — valid-value generation
321
323
 
322
324
  Walks Zod schemas to generate valid values for adversarial/round-trip tests.
323
325
 
324
- - `detect_format(field_schema)` — reads `format` / `pattern` from the
325
- JSON Schema representation.
326
- - `generate_valid_value(field, field_schema)` — base-type switch
327
- producing a valid sample (UUIDs nil UUID, strings `'xxxxxxxxxx'`,
328
- numbers → `1`, objects → recurse, enums → first entry, etc.).
329
- For branded-string refinements, walks a fallback chain synthesized
330
- from the `pattern` string the JSON Schema representation exposes:
331
- fixed-length hex (`^[0-9a-f]{N}$` — blake3 / sha256 / md5 digests;
332
- `0`.repeat(N)), prefix-lengthed slug (`^<prefix>_[A-Za-z0-9_-]{N}$`
333
- — `ApiTokenId`-style ids; `<prefix>_` + `x`.repeat(N)), absolute
334
- path prefix, URL prefix. First candidate that `safeParse` accepts
335
- is used.
336
- - `resolve_valid_path(path, params_schema?)` — swaps `:param` for
337
- valid-format values (nil UUID for UUID params, `test_param` otherwise).
338
- - `generate_valid_body(input_schema) => Record<string, unknown> | undefined` —
339
- builds a body that satisfies the input schema. Throws with Zod
340
- `issues` if the generated body fails validation — surfaces broken
341
- generation logic with a descriptive error rather than a confusing 400
342
- downstream.
326
+ - `detect_format(field_schema)` — reads `format` / `pattern` from the JSON Schema representation.
327
+ - `generate_valid_value(field, field_schema)` — base-type switch producing a valid sample (UUIDs → nil UUID, strings → `'xxxxxxxxxx'`, numbers → `1`, objects → recurse, enums → first entry, etc.). For branded-string refinements, walks a fallback chain synthesized from the `pattern` string the JSON Schema representation exposes: fixed-length hex (`^[0-9a-f]{N}$` — blake3 / sha256 / md5 digests; `0`.repeat(N)), prefix-lengthed slug (`^<prefix>_[A-Za-z0-9_-]{N}$` — `ApiTokenId`-style ids; `<prefix>_` + `x`.repeat(N)), absolute path prefix, URL prefix. First candidate that `safeParse` accepts is used.
328
+ - `resolve_valid_path(path, params_schema?)` — swaps `:param` for valid-format values (nil UUID for UUID params, `test_param` otherwise).
329
+ - `generate_valid_body(input_schema) => Record<string, unknown> | undefined` — builds a body satisfying the input schema. Throws with Zod `issues` if the generated body fails validation — surfaces broken generation logic with a descriptive error rather than a confusing 400 downstream.
343
330
 
344
331
  ### `integration_helpers.ts` — route lookup + body checks
345
332
 
346
- | Helper | Role |
347
- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
348
- | `find_route_spec(specs, method, path)` | Exact match then parameterized match (`:foo` matches any segment). |
349
- | `find_auth_route(specs, suffix, method)` | Suffix-ending match for REST auth routes decouples tests from consumer prefix. `suffix` is typed as `RestAuthRouteSuffix` and throws at runtime on unknown values (only login/logout/password/verify/signup/bootstrap remain on REST). |
350
- | `assert_response_matches_spec(specs, method, path, response)` | 2xx validates against `spec.output`; non-2xx validates against merged error schemas for that status. Non-JSON responses allowed only when no schema applies. |
351
- | `create_expired_test_cookie(keyring, session_options)` | Validly signed cookie with `expires_at` in 1970. |
352
- | `check_error_response_fields(body)` | Returns the list of fields outside `KNOWN_SAFE_ERROR_FIELDS` (`error`, `issues`, `required_roles`, `required_credential_types`, `retry_after`, `has_references`, `ok`). |
353
- | `assert_no_error_info_leakage(body, context)` | Rejects field-name patterns (`stack`, `trace`, `sql`, …) + value patterns (`node_modules`, stack-like `at …`, `.ts:NN`). |
354
- | `assert_rate_limit_retry_after_header(response, body)` | `Retry-After` numeric header equals `Math.ceil(body.retry_after)`. |
355
- | `sensitive_field_blocklist` | `['password_hash', 'token_hash']` never in any response body. |
356
- | `admin_only_field_blocklist` | `['updated_by', 'created_by']` — never in non-admin response bodies. |
357
- | `collect_json_keys_recursive(value)` | Deep walk; returns `Set<string>` of every key at every nesting depth. |
358
- | `assert_no_sensitive_fields_in_json(body, blocklist, context)` | Rejects any key in the blocklist at any depth. |
359
- | `pick_auth_headers(spec, test_app, authed_account, admin_account)` | `RouteAuth` → appropriate test credentials; role `admin` uses `admin_account`, other roles use bootstrapped keeper, `keeper` uses daemon token. |
333
+ - `find_route_spec(specs, method, path)` — exact match then parameterized match (`:foo` matches any segment).
334
+ - `find_auth_route(specs, suffix, method)` — suffix-ending match for REST auth routes; decouples tests from consumer prefix. `suffix` is typed as `RestAuthRouteSuffix` and throws at runtime on unknown values (only login/logout/password/verify/signup/bootstrap remain on REST).
335
+ - `assert_response_matches_spec(specs, method, path, response)` 2xx validates against `spec.output`; non-2xx validates against merged error schemas for that status. Non-JSON responses allowed only when no schema applies.
336
+ - `create_expired_test_cookie(keyring, session_options)` — validly signed cookie with `expires_at` in 1970.
337
+ - `check_error_response_fields(body)` returns the list of fields outside `KNOWN_SAFE_ERROR_FIELDS` (`error`, `issues`, `required_roles`, `required_credential_types`, `retry_after`, `has_references`, `ok`).
338
+ - `assert_no_error_info_leakage(body, context)` rejects field-name patterns (`stack`, `trace`, `sql`, …) + value patterns (`node_modules`, stack-like `at …`, `.ts:NN`).
339
+ - `assert_rate_limit_retry_after_header(response, body)` `Retry-After` numeric header equals `Math.ceil(body.retry_after)`.
340
+ - `sensitive_field_blocklist` `['password_hash', 'token_hash']`; never in any response body.
341
+ - `admin_only_field_blocklist` `['updated_by', 'created_by']`; never in non-admin response bodies.
342
+ - `collect_json_keys_recursive(value)` deep walk; returns `Set<string>` of every key at every nesting depth.
343
+ - `assert_no_sensitive_fields_in_json(body, blocklist, context)` — rejects any key in the blocklist at any depth.
344
+ - `pick_auth_headers(spec, test_app, authed_account, admin_account)` `RouteAuth` → appropriate test credentials; role `admin` uses `admin_account`, other roles use bootstrapped keeper, `keeper` uses daemon token.
360
345
 
361
346
  ## Attack surface suites
362
347
 
363
348
  ### `attack_surface.ts` — `describe_standard_attack_surface_tests`
364
349
 
365
- Single-call bundle of 5 top-level groups (10 named tests + every
366
- adversarial case per route):
350
+ Single-call bundle of 5 top-level groups (10 named tests + every adversarial
351
+ case per route):
367
352
 
368
353
  1. **attack surface snapshot** — `matches committed snapshot`, `is deterministic`.
369
354
  2. **attack surface structure** — `only expected public routes`, `full middleware stack on API routes`, `surface invariants`, `rpc/ws surface invariants`, `security policy`, `error schema tightness` (logs counts and asserts against `default_error_schema_tightness` by default; pass an override config or `null` via `error_schema_tightness`).
@@ -373,41 +358,35 @@ adversarial case per route):
373
358
 
374
359
  Options: `{build: () => AppSurfaceSpec, snapshot_path, expected_public_routes, expected_api_middleware, roles, api_path_prefix?, security_policy?, error_schema_tightness?}`.
375
360
 
376
- Also exported: `describe_adversarial_auth(options)` (groups 3 on its own)
377
- and `build_error_schema_lookup(specs, middleware_specs?)` (pre-built
361
+ Also exported: `describe_adversarial_auth(options)` (group 3 on its own) and
362
+ `build_error_schema_lookup(specs, middleware_specs?)` (pre-built
378
363
  `Map<string, RouteErrorSchemas>` for per-response validation).
379
364
 
380
365
  ### `adversarial_input.ts` — schema-walk payload generation
381
366
 
382
367
  `describe_adversarial_input({build, roles})` — fires input body / params /
383
- query validation failures at every route with correct-auth credentials
384
- so validation middleware is actually exercised (not short-circuited by
385
- 401). All cases expect 400 with one of `ERROR_INVALID_REQUEST_BODY` /
368
+ query validation failures at every route with correct-auth credentials so
369
+ validation middleware is actually exercised (not short-circuited by 401).
370
+ All cases expect 400 with one of `ERROR_INVALID_REQUEST_BODY` /
386
371
  `_INVALID_JSON_BODY` / `_INVALID_ROUTE_PARAMS` / `_INVALID_QUERY_PARAMS`.
387
372
 
388
373
  Exported generators:
389
374
 
390
- - `generate_input_test_cases(input_schema)` — whole-body structural
391
- (non-object, extra key when `strictObject`), missing required fields,
392
- one wrong-type per field, null for required non-nullable, one format
393
- violation per constrained field, numeric/array/string boundary cases
394
- via JSON Schema introspection.
395
- - `generate_params_test_cases(params_schema)` — format violations only
396
- (unconstrained string params accept anything).
397
- - `generate_query_test_cases(query_schema)` — missing required +
398
- format violations.
375
+ - `generate_input_test_cases(input_schema)` — whole-body structural (non-object, extra key when `strictObject`), missing required fields, one wrong-type per field, null for required non-nullable, one format violation per constrained field, numeric/array/string boundary cases via JSON Schema introspection.
376
+ - `generate_params_test_cases(params_schema)` format violations only (unconstrained string params accept anything).
377
+ - `generate_query_test_cases(query_schema)` missing required + format violations.
399
378
 
400
- GET-with-input routes hit the RPC `?params=` query convention; invalid-
401
- JSON arrays there collapse to `ERROR_INVALID_REQUEST_BODY` (schema
402
- failure) rather than `ERROR_INVALID_JSON_BODY`.
379
+ GET-with-input routes hit the RPC `?params=` query convention; invalid-JSON
380
+ arrays there collapse to `ERROR_INVALID_REQUEST_BODY` (schema failure)
381
+ rather than `ERROR_INVALID_JSON_BODY`.
403
382
 
404
383
  ### `adversarial_404.ts` — 404 schema conformance
405
384
 
406
- `describe_adversarial_404({build, roles})` — for every route with
407
- `params` + 404 in `error_schemas` + an extractable error code
408
- (`z.literal` or first `z.enum`), replaces the handler with a stub
409
- returning `{error: <code>}`, fires with nil-UUID params, asserts 404 +
410
- body matches the declared 404 Zod schema. No DB needed.
385
+ `describe_adversarial_404({build, roles})` — for every route with `params` +
386
+ 404 in `error_schemas` + an extractable error code (`z.literal` or first
387
+ `z.enum`), replaces the handler with a stub returning `{error: <code>}`,
388
+ fires with nil-UUID params, asserts 404 + body matches the declared 404 Zod
389
+ schema. No DB needed.
411
390
 
412
391
  ### `adversarial_headers.ts` — header injection suite
413
392
 
@@ -430,56 +409,51 @@ validation. Extra cases append to the standard list.
430
409
 
431
410
  Module-level `vi.mock()` for the four query modules bearer auth touches:
432
411
  `api_token_queries`, `account_queries`, `role_grant_queries`. Because
433
- `vi.mock()` is hoisted, these run before any imports resolve — so any
434
- test file that imports from `middleware.ts` gets these mocks globally.
435
- Pair with `vi.restoreAllMocks()` in `afterEach` when mixing into
436
- `.db.test.ts` files (see DB test caveat below).
437
-
438
- | Helper | Role |
439
- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
440
- | `BearerAuthTestOptions`, `BearerAuthTestCase` | Test-case table shape for the bearer auth runner. |
441
- | `create_bearer_auth_mocks(tc)` | Configures the module-level mocks per test case; returns spy references. |
442
- | `TEST_CLIENT_IP = '127.0.0.1'` | IP set by the proxy stub in `create_bearer_auth_test_app`. |
443
- | `create_bearer_auth_test_app(tc, ip_rate_limiter?)` | Hono app with bearer middleware + echo route at `/api/test` returning `{ok, account_id, credential_type, api_token_id, request_context_set}` — the account-grain identity bearer auth writes, plus a flag for tests that pre-populate `REQUEST_CONTEXT_KEY` via `pre_context`. |
444
- | `describe_bearer_auth_cases(suite_name, cases, ip_rate_limiter?)` | Table-driven runner — one `test()` per case; asserts status, error, body fields, `api_token_id`, context preservation. |
445
- | `TEST_MIDDLEWARE_PATH = '/api/test'` | Path used by the echo route in the stack factory. |
446
- | `create_test_middleware_stack_app(options?)` | Real proxy + origin + bearer middleware for integration-shape testing. Echo route returns `{ok, client_ip, has_context}`. |
412
+ `vi.mock()` is hoisted, these run before any imports resolve — so any test
413
+ file that imports from `middleware.ts` gets these mocks globally. Pair with
414
+ `vi.restoreAllMocks()` in `afterEach` when mixing into `.db.test.ts` files.
415
+
416
+ - `BearerAuthTestOptions`, `BearerAuthTestCase` — test-case table shape for the bearer auth runner.
417
+ - `create_bearer_auth_mocks(tc)` — configures the module-level mocks per test case; returns spy references.
418
+ - `TEST_CLIENT_IP = '127.0.0.1'` — IP set by the proxy stub in `create_bearer_auth_test_app`.
419
+ - `create_bearer_auth_test_app(tc, ip_rate_limiter?)` — Hono app with bearer middleware + echo route at `/api/test` returning `{ok, account_id, credential_type, api_token_id, request_context_set}` — the account-grain identity bearer auth writes, plus a flag for tests that pre-populate `REQUEST_CONTEXT_KEY` via `pre_context`.
420
+ - `describe_bearer_auth_cases(suite_name, cases, ip_rate_limiter?)` table-driven runner; one `test()` per case; asserts status, error, body fields, `api_token_id`, context preservation.
421
+ - `TEST_MIDDLEWARE_PATH = '/api/test'` path used by the echo route in the stack factory.
422
+ - `create_test_middleware_stack_app(options?)` real proxy + origin + bearer middleware for integration-shape testing. Echo route returns `{ok, client_ip, has_context}`.
447
423
 
448
424
  The echo route under `create_bearer_auth_test_app` deliberately surfaces
449
425
  every middleware-written context variable (`ACCOUNT_ID_KEY`,
450
- `CREDENTIAL_TYPE_KEY`, `AUTH_API_TOKEN_ID_KEY`) — bearer middleware
451
- writes account-grain identity only; the dispatcher's authorization phase
452
- owns `REQUEST_CONTEXT_KEY`. The `request_context_set` flag covers the
453
- test-only `pre_context` injection path. When public auth surface gains a
454
- new context variable, header, or field, update this echo alongside the
455
- assertions in `src/test/auth/*.test.ts` — the two move together.
426
+ `CREDENTIAL_TYPE_KEY`, `AUTH_API_TOKEN_ID_KEY`) — bearer middleware writes
427
+ account-grain identity only; the dispatcher's authorization phase owns
428
+ `REQUEST_CONTEXT_KEY`. The `request_context_set` flag covers the test-only
429
+ `pre_context` injection path. When public auth surface gains a new context
430
+ variable, header, or field, update this echo alongside the assertions in
431
+ `src/test/auth/*.test.ts` — the two move together.
456
432
 
457
433
  ## Round-trip suites
458
434
 
459
435
  ### `round_trip.ts` — `describe_round_trip_validation`
460
436
 
461
- For every route spec, fires a valid request with matching auth and
462
- validates the response against declared schemas. DB-backed via
463
- `create_test_app`. Per-route test (`test.each`) — one line per route
464
- in the vitest output.
437
+ For every route spec, fires a valid request with matching auth and validates
438
+ the response against declared schemas. DB-backed via `create_test_app`.
439
+ Per-route test (`test.each`) — one line per route in the vitest output.
465
440
 
466
- Options: `{session_options, create_route_specs, app_options?, db_factories?, skip_routes?, input_overrides?}`.
441
+ Options: `{setup_test, surface_source, capabilities, skip_routes?, input_overrides?}`.
467
442
  `input_overrides` is a `Map<"METHOD /path", body>` — override generated
468
- bodies for routes whose input schema can't round-trip cleanly (e.g.
469
- fields that must reference DB state).
443
+ bodies for routes whose input schema can't round-trip cleanly (e.g. fields
444
+ that must reference DB state).
470
445
 
471
446
  SSE routes are skipped by Content-Type sniff; `describe_sse_route_tests`
472
447
  picks them up separately.
473
448
 
474
449
  ### `rpc_round_trip.ts` — `describe_rpc_round_trip_tests`
475
450
 
476
- DB-backed round-trip for RPC: one POST test for all methods, one GET
477
- test for `side_effects: false` methods. Successful responses validate
478
- against `action.spec.output`; error responses validate as well-formed
479
- JSON-RPC error envelopes. Required: `{session_options, create_route_specs, rpc_endpoints, ...}`.
480
- The admin RPC auth test picks a session-based identity (`authed` /
481
- `admin` / bootstrapped keeper) based on `method.auth`; keeper uses the
482
- daemon token.
451
+ DB-backed round-trip for RPC: one POST test for all methods, one GET test
452
+ for `side_effects: false` methods. Successful responses validate against
453
+ `action.spec.output`; error responses validate as well-formed JSON-RPC error
454
+ envelopes. Options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints, skip_methods?, input_overrides?}`.
455
+ The admin RPC auth test picks a session-based identity (`authed` / `admin` /
456
+ bootstrapped keeper) based on `method.auth`; keeper uses the daemon token.
483
457
 
484
458
  ### `sse_round_trip.ts` — `describe_sse_route_tests`
485
459
 
@@ -490,46 +464,45 @@ validate the next `data:` frame as `{method, params}` against declared
490
464
  and assert the stream closes within 2s.
491
465
 
492
466
  `SseRouteTestSpec` per route: `{path, trigger, event_specs?, assert_closes_on_revoke?}`.
493
- Pass `on_audit_event` on the suite options to wire a close-on-revoke
494
- guard (e.g. via `create_sse_auth_guard`) for consumer SSE registries —
495
- without it, the revoke assertion hangs because the guard never fires.
467
+ Pass `on_audit_event` on the suite options to wire a close-on-revoke guard
468
+ (e.g. via `create_sse_auth_guard`) for consumer SSE registries — without it,
469
+ the revoke assertion hangs because the guard never fires.
496
470
 
497
- Frame reader (`create_sse_frame_reader`) is internal but handles
498
- `\n\n` framing, a 2s per-read timeout (prevents vitest hangs), and
499
- `wait_for_close` for the revocation check.
471
+ Frame reading is delegated to the shared `create_sse_frame_reader`
472
+ (`transports/sse_frame_reader.ts`) — `\n\n` framing, a 2s per-read timeout
473
+ (prevents vitest hangs), and `wait_for_close` for the revocation check. The
474
+ cross-process `transports/sse_transport.ts` reuses the same reader over a
475
+ streaming `fetch` body.
500
476
 
501
477
  ### `ws_round_trip.ts` — WebSocket harness (non-HTTP)
502
478
 
503
479
  In-process test driver for `register_action_ws`. Consumers pass specs +
504
- handlers, receive `{transport, connect()}` back. The full dispatch path
505
- is exercised (per-action auth, input validation, `ctx.notify`,
506
- broadcast via `BackendWebsocketTransport`, close-on-revoke), but Hono's
507
- wire upgrade is skipped (the Node test runtime has no
508
- `@hono/node-ws` adapter).
480
+ handlers, receive `{transport, connect()}` back. The full dispatch path is
481
+ exercised (per-action auth, input validation, `ctx.notify`, broadcast via
482
+ `BackendWebsocketTransport`, close-on-revoke), but Hono's wire upgrade is
483
+ skipped (the Node test runtime has no `@hono/node-ws` adapter).
509
484
 
510
485
  Three layers:
511
486
 
512
- 1. **Primitives** — `create_fake_ws()`, `create_fake_hono_context(opts)`,
513
- `create_stub_upgrade()`, `MinimalActionEnvironment`,
514
- `dispatch_ws_message(on_message, event, ws)`.
487
+ 1. **Primitives** — `create_fake_ws()`, `create_fake_hono_context(opts)`, `create_stub_upgrade()`, `MinimalActionEnvironment`, `dispatch_ws_message(on_message, event, ws)`.
515
488
  2. **Harness** — `create_ws_test_harness({actions, transport?, heartbeat?, log?, on_socket_open?, on_socket_close?})` → `WsTestHarness`. `connect(identity?)` is async and resolves after `on_socket_open` completes, so broadcasts sent immediately after `await harness.connect()` reach the client. The harness threads its own `create_stub_db()` into the dispatcher's `db` slot so handlers declaring `side_effects: true` execute under the same transaction wrap they would in production (the stub's `transaction(fn)` synchronously calls `fn(stub_db)`); domain deps reach handlers via factory closures, the same way HTTP RPC factories already wire them. Audit fan-out runs through whatever `audit` emitter the consumer supplied to its action factory closure (typically `create_test_audit_emitter()` for unit harnesses).
516
- 3. **Round-trip helpers** — `is_notification(method)`,
517
- `is_notification_with<P>(method, match)` (type-guard combinator —
518
- narrows `wait_for` return type), `is_response_for(id)`.
519
- `JsonrpcNotificationFrame<P>` / `JsonrpcSuccessResponseFrame<R>` /
520
- `JsonrpcErrorResponseFrame<D>` typed wire-frame shapes distinct
521
- from the runtime Zod schemas in `http/jsonrpc.ts` (generic over
522
- `params` / `result` / `data` so tests narrow without casts).
523
- `build_broadcast_api<TApi>({harness, specs})` wires a typed
524
- broadcast API against the harness transport.
525
-
526
- `MockWsClient`: `{send, request<R>, close, messages, wait_for}`.
527
- `request` throws with code + message + data on error frames (so
528
- asserting `result.foo` on a failed request surfaces the real cause,
529
- not a `Cannot read property 'foo' of undefined`). `wait_for(predicate,
530
- timeout_ms?)` checks already-received messages first, then waits for
531
- new arrivals (default 1000ms); drops the waiter on timeout so the
532
- `waiters` array doesn't grow.
489
+ 3. **Round-trip helpers** — predicates + wire-frame types live in `transports/ws_client.ts` (shared with the cross-process `ws_transport.ts` impl): `is_notification(method)`, `is_notification_with<P>(method, match)` (type-guard combinator — narrows `wait_for` return type), `is_response_for(id)`, `JsonrpcNotificationFrame<P>` / `JsonrpcSuccessResponseFrame<R>` / `JsonrpcErrorResponseFrame<D>` (typed wire-frame shapes distinct from the runtime Zod schemas in `http/jsonrpc.ts` — generic over `params` / `result` / `data` so tests narrow without casts). `build_broadcast_api<TApi>({harness, specs})` (in `ws_round_trip.ts`) wires a typed broadcast API against the harness transport.
490
+
491
+ `WsClient` (in `transports/ws_client.ts`):
492
+ `{send, request<R>, close, messages, wait_for, wait_for_close}`. The
493
+ harness's `connect()` returns this shape; the cross-process
494
+ `create_ws_transport` in `transports/ws_transport.ts` implements the same
495
+ interface so assertion helpers and suite bodies work against either impl.
496
+ `wait_for_close(timeout_ms?)` resolves `true` if the server closes the
497
+ socket within the timeout, `false` on timeout (and `true` immediately when
498
+ already closed) — the signal for server-initiated close (e.g. an auth-guard
499
+ revocation), distinct from client-initiated `close()`. Mirrors the SSE frame
500
+ reader's `wait_for_close`. `request` throws with
501
+ code + message + data on error frames (so asserting `result.foo` on a
502
+ failed request surfaces the real cause, not a `Cannot read property 'foo'
503
+ of undefined`). `wait_for(predicate, timeout_ms?)` checks already-received
504
+ messages first, then waits for new arrivals (default 1000ms); drops the
505
+ waiter on timeout so the `waiters` array doesn't grow.
533
506
 
534
507
  `keeper_identity()` — convenience for `{credential_type: 'daemon_token', roles: [ROLE_KEEPER]}`.
535
508
 
@@ -553,21 +526,27 @@ Support functions: `collect_json_schema_property_names(schema)` (walks
553
526
  `assert_output_schemas_no_sensitive_fields(surface, fields?)`,
554
527
  `assert_non_admin_schemas_no_admin_fields(surface, fields?)`.
555
528
 
556
- Options: `{build, session_options, create_route_specs, sensitive_fields?, admin_only_fields?, app_options?, db_factories?, skip_routes?}`.
529
+ Options: `{setup_test, surface_source, capabilities, sensitive_fields?, admin_only_fields?, skip_routes?}`.
557
530
 
558
531
  ### `rate_limiting.ts` — `describe_rate_limiting_tests`
559
532
 
560
533
  Three test groups:
561
534
 
562
- 1. IP rate limiting on login — fires `max_attempts + 1` requests; last one should be 429 with `RateLimitError` body + valid `Retry-After` header.
535
+ 1. IP rate limiting on login — fires `max_attempts + 1` requests; last should be 429 with `RateLimitError` body + valid `Retry-After` header.
563
536
  2. Per-account rate limiting on login — same username exhausts the bucket; a different username is not blocked.
564
537
  3. Bearer auth IP rate limiting — invalid bearer tokens exhaust the IP bucket via the `account_verify` RPC method.
565
538
 
566
- Each group asserts its required route exists with a descriptive
567
- message. Creates a tight rate limiter (default `max_attempts: 2`,
568
- `window_ms: 60_000`) per test and disposes it in `finally`.
539
+ Each group asserts its required route exists with a descriptive message.
540
+ Creates a tight rate limiter (default `max_attempts: 2`, `window_ms: 60_000`)
541
+ per test and disposes it in `finally`.
569
542
 
570
- Options: `{session_options, create_route_specs, app_options?, db_factories?, max_attempts?}`.
543
+ Options: `{session_options, create_route_specs, rpc_endpoints, app_options?, db_factories?, max_attempts?}`.
544
+ Reads inputs directly from the options bag instead of going through the
545
+ `setup_test` fixture protocol — the per-test rate-limiter overrides need a
546
+ fresh `TestApp` per test that the single-fixture model can't carry.
547
+ Consumers still pass `default_in_process_suite_options(...)` for shape
548
+ uniformity; the extra `{setup_test, surface_source, capabilities}` fields on
549
+ the spread are ignored by the suite.
571
550
 
572
551
  ## Integration suites
573
552
 
@@ -587,24 +566,39 @@ these thematic areas:
587
566
  8. Bearer auth + browser-context discard on mutations
588
567
  9. Token revocation + cross-account isolation
589
568
  10. Response body schema validation + error-response information leakage
590
- 11. Signup invite edge cases + rate-limiting smoke + expired credential rejection + error-coverage breadth
569
+ 11. Signup invite edge cases + expired credential rejection + error-coverage breadth
591
570
 
592
571
  An `ErrorCoverageCollector` runs across groups; `afterAll` filters to
593
- auth-related routes (login/logout/verify/sessions/tokens/password/
594
- signup/bootstrap) and asserts `DEFAULT_INTEGRATION_ERROR_COVERAGE`
595
- (20%). Consumer-specific routes aren't exercised herethey don't
596
- count against the baseline.
572
+ auth-related routes (login/logout/verify/sessions/tokens/password/signup)
573
+ and asserts `DEFAULT_INTEGRATION_ERROR_COVERAGE` (20%). Bootstrap is
574
+ excluded because no describe block in this suite drives it its declared
575
+ codes would always be uncovered. Consumer-specific routes aren't exercised
576
+ here either — they don't count against the baseline. Override with
577
+ `error_coverage_min?: number` (set to `0` to skip the assertion — useful for
578
+ minimal route sets whose declared error codes outpace the suite's
579
+ denial-path drivers).
597
580
 
598
- Options: `{session_options, create_route_specs, app_options?, db_factories?}`.
581
+ Options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints, error_coverage_min?}`.
599
582
 
600
583
  ### `admin_integration.ts` — `describe_standard_admin_integration_tests`
601
584
 
602
585
  7 test groups covering admin surface: account listing, role_grant grant
603
- lifecycle (via `role_grant_offer_create` + `role_grant_revoke` RPC flows —
604
- **not** REST; see `auth/CLAUDE.md` for `role_grant_offer_action_specs.ts` + `role_grant_offer_actions.ts`), session / token management, audit log reads (RPC),
605
- admin-to-admin isolation, error coverage, response schema validation.
606
-
607
- Required options: `{session_options, create_route_specs, roles: RoleSchemaResult, rpc_endpoints: RpcEndpointsSuiteOption, admin_prefix?, app_options?, db_factories?}`.
586
+ lifecycle (via `role_grant_offer_create` + `role_grant_offer_accept` +
587
+ `role_grant_revoke` RPC flows **not** REST, **not** direct
588
+ `query_accept_offer`; see `auth/CLAUDE.md` for
589
+ `role_grant_offer_action_specs.ts` + `role_grant_offer_actions.ts`),
590
+ session / token management, audit log reads (RPC), admin-to-admin
591
+ isolation, error coverage, response schema validation.
592
+
593
+ The shared `role_grant_offer_and_accept` helper (`role_grant_helpers.ts`)
594
+ composes both RPCs end-to-end and takes
595
+ `{grantor: TestApp | TestAccount, recipient: TestAccount}` — closing the
596
+ headers/account loop on a single object per party rules out caller-side
597
+ header/account mismatch. Direct-grant fixtures (test focuses on revoke or
598
+ isolation, not the consent path) go through `create_test_role_grant_direct`
599
+ from `db_entities.ts`.
600
+
601
+ Required options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints: RpcEndpointsSuiteOption, roles: RoleSchemaResult, admin_prefix?}`.
608
602
 
609
603
  `rpc_endpoints` is `Array<RpcEndpointSpec> | ((ctx: AppServerContext) => Array<RpcEndpointSpec>)` —
610
604
  the same `RpcEndpointsSuiteOption` union every DB-backed suite accepts
@@ -615,86 +609,125 @@ raw to the top-level `rpc_endpoints` slot on `CreateTestAppOptions` so
615
609
  action handlers can close over
616
610
  `ctx.deps` / `ctx.app_settings` (e.g. `create_standard_rpc_actions(ctx.deps,
617
611
  {app_settings: ctx.app_settings})`). Factory must return the same endpoint
618
- `path` regardless of ctx — `resolve_rpc_endpoints_for_setup` invokes it
619
- once with a stub ctx for path lookup and `create_app_server` invokes it
620
- again per-test for live dispatch.
612
+ `path` regardless of ctx — `resolve_rpc_endpoints_for_setup` invokes it once
613
+ with a stub ctx for path lookup and `create_app_server` invokes it again
614
+ per-test for live dispatch.
621
615
 
622
616
  **Hard-fails via `require_rpc_endpoint_path`** at setup time when
623
617
  `rpc_endpoints` is empty — admin role_grant grant/revoke plus session/token
624
618
  revoke-all plus audit-log list/history are RPC-only. A confusing test
625
619
  failure mid-suite is worse than a clear setup error.
626
620
 
627
- The suite also exercises `account_token_create` (and
628
- `account_token_revoke`) for the cross-admin isolation + audit-trail
629
- scenarios. Wire the account actions alongside admin / role-grant-offer —
630
- the easiest path is `create_standard_rpc_actions`, which bundles all
631
- three. Consumers that only wire admin will hit `method not found:
632
- account_token_create` on first run.
633
-
634
- Error-coverage scope is narrowed to the REST suffixes still on the
635
- admin surface (`/audit/stream`); the RPC surface is covered by
636
- `describe_rpc_round_trip_tests`. The scoped REST surface is 0–1
637
- routes when the scoped count is ≤1, the `afterAll` hook logs
638
- `[error coverage] skipped admin REST coverage assertion — …` and
639
- does not fail. The 20% `DEFAULT_INTEGRATION_ERROR_COVERAGE` baseline
640
- is a REST-era threshold; the RPC surface has its own coverage via
641
- `describe_rpc_round_trip_tests`. TODO: move this error-coverage
642
- collector to the RPC round-trip suite entirely and delete this skip
643
- branch.
621
+ The suite also exercises `account_token_create` (and `account_token_revoke`)
622
+ for the cross-admin isolation + audit-trail scenarios. Wire the account
623
+ actions alongside admin / role-grant-offer — easiest is
624
+ `create_standard_rpc_actions`, which bundles all three. Consumers that only
625
+ wire admin will hit `method not found: account_token_create` on first run.
626
+
627
+ Error-coverage scope is narrowed to the REST suffixes still on the admin
628
+ surface (`/audit/stream`); the RPC surface is covered by
629
+ `describe_rpc_round_trip_tests`. The scoped REST surface is 0–1 routes —
630
+ when the scoped count is 1, the `afterAll` hook logs
631
+ `[error coverage] skipped admin REST coverage assertion …` and does not
632
+ fail. The 20% `DEFAULT_INTEGRATION_ERROR_COVERAGE` baseline is a REST-era
633
+ threshold; the RPC surface has its own coverage via
634
+ `describe_rpc_round_trip_tests`. TODO: move this error-coverage collector
635
+ to the RPC round-trip suite entirely and delete this skip branch.
644
636
 
645
637
  ### `audit_completeness.ts` — `describe_audit_completeness_tests`
646
638
 
647
- Verifies every auth mutation produces the expected `audit_log` row by
648
- querying the table after each request. Uses the real middleware stack.
639
+ Verifies every auth mutation produces the expected `audit_log` row.
640
+ Mutations fire over the real middleware stack; reads go back through the
641
+ `audit_log_list` RPC (the same path the admin UI consumes) — intentional
642
+ end-to-end coverage of emit → persist → query → wire response. For
643
+ unit-level "did the handler emit?" assertions without the persistence path,
644
+ use `create_recording_audit_emitter` from `audit_drift_guard.ts`.
645
+
649
646
  Same `rpc_endpoints` hard-fail as the admin suite — the mutation-audit
650
647
  tests drive role_grant flow, session/token revoke-all, and invite
651
648
  create/delete through `role_grant_offer_create_action_spec` /
652
- `role_grant_revoke_action_spec` / `admin_session_revoke_all_action_spec` /
649
+ `role_grant_offer_accept_action_spec` / `role_grant_revoke_action_spec` /
650
+ `admin_session_revoke_all_action_spec` /
653
651
  `admin_token_revoke_all_action_spec` / `app_settings_update_action_spec` /
654
652
  `invite_create_action_spec` / `invite_delete_action_spec`.
655
653
 
654
+ **Observer-account pattern.** Each audit-touching test mints a dedicated
655
+ admin account (`create_admin_observer`) whose sole job is reading the audit
656
+ log via RPC. Decoupling the observer from the subject keeps the helper
657
+ shape uniform across every test — even mutations that revoke the
658
+ bootstrapped admin's credentials (logout, session_revoke, password_change).
659
+
656
660
  Bootstrap audit logging is excluded because `create_test_app` doesn't
657
661
  provide the filesystem token state; covered separately in
658
662
  `bootstrap_account.db.test.ts`.
659
663
 
660
664
  ### `standard.ts` — `describe_standard_tests`
661
665
 
662
- Convenience wrapper: always runs `describe_standard_integration_tests`;
663
- runs `describe_standard_admin_integration_tests` only when `roles` is
664
- provided. `rpc_endpoints: RpcEndpointsSuiteOption` is a required field on
665
- `StandardTestOptions` — the admin suite's requirement is enforced at the
666
- type level, so a missing `rpc_endpoints` is a compile error rather than a
667
- runtime throw. Round-trips the union through unchanged so consumers can
668
- pass either an eager array or the factory form.
666
+ Bundles every DB-backed suite carrying the standard option shape, each
667
+ gated on its relevant config — silent-skip when the gate isn't met:
668
+
669
+ - `integration` — always
670
+ - `admin` `roles` provided
671
+ - `audit_completeness` `roles` provided (proxy for consumer admin wiring; `rpc_endpoints` is bundle-required)
672
+ - `bootstrap_success` `bootstrap.mode === 'live'`
673
+ - `round_trip` — always
674
+ - `rpc_round_trip` — `rpc_endpoints` provided
675
+ - `data_exposure` — always
676
+ - `rate_limiting` — always (owns its own per-test setup, bypasses the fixture protocol — needs `create_route_specs` directly)
677
+
678
+ Realization that lifted the bundle from 2 suites to 8: fold-in cost between
679
+ suites is zero because each `describe_*` block owns its own setup via the
680
+ `{setup_test, surface_source, capabilities}` protocol, so suites whose
681
+ tests need opposite-shaped default DB state (e.g. the bootstrap-success
682
+ suite needs an empty DB while the integration suite needs the
683
+ pre-bootstrapped keeper) coexist in one bundle without cost. Each test
684
+ invokes the right per-test fixture. Consumers wiring the standard surface
685
+ call once instead of seven times; forgetting a suite no longer silently
686
+ loses coverage.
687
+
688
+ `StandardTestOptions` requires `create_route_specs` (for rate_limiting) and
689
+ `rpc_endpoints` (for admin/audit_completeness/rpc_round_trip); the admin
690
+ suite's requirement is enforced at the type level so a missing
691
+ `rpc_endpoints` is a compile error rather than a runtime throw. Optional
692
+ `bootstrap` (top-level, same precedent as `rpc_endpoints`) feeds both the
693
+ disabled/surface_only/live wire-shape gating and the bootstrap-success
694
+ suite gate.
695
+
696
+ Attack surface suites stay separate — their option shape is
697
+ `{build, snapshot_path, expected_public_routes, ...}` rather than the
698
+ shared `{setup_test, surface_source, capabilities}`. A peer
699
+ `describe_standard_surface_tests` bundler lives for that side if/when
700
+ needed.
701
+
702
+ Cross-process counterpart: `cross_backend/standard.ts` —
703
+ `describe_standard_cross_process_tests`. Different bundle because three of
704
+ the eight in-process suites don't survive a process boundary
705
+ (`rate_limiting` needs a fresh per-test `TestApp`, `audit_completeness`
706
+ reads FK structure, `bootstrap_success` is one-shot per backend lifecycle);
707
+ the cross-process bundle documents the omissions once upstream so
708
+ per-consumer files don't repeat the bookkeeping. See the Cross-backend
709
+ integration layer §`cross_backend/standard.ts` below.
669
710
 
670
711
  ## RPC helpers
671
712
 
672
713
  ### `rpc_helpers.ts` — envelope construction + response assertions
673
714
 
674
- Shared by `rpc_attack_surface.ts`, `rpc_round_trip.ts`, the admin and
675
- audit integration suites, and consumer tests that hit RPC methods
676
- directly.
715
+ Shared by `rpc_attack_surface.ts`, `rpc_round_trip.ts`, the admin and audit
716
+ integration suites, and consumer tests that hit RPC methods directly.
677
717
 
678
718
  Request builders:
679
719
 
680
- - `create_rpc_post_init(method, params?, id?)` — `RequestInit` with
681
- JSON-RPC envelope body. `params === undefined || params === null` →
682
- envelope has no `params` field (JSON-RPC doesn't accept
683
- `"params": null`).
684
- - `create_rpc_get_url(endpoint_path, method, params?, id?)` — GET URL
685
- with `?method=&id=&params=<JSON>`.
720
+ - `create_rpc_post_init(method, params?, id?)` — `RequestInit` with JSON-RPC envelope body. `params === undefined || params === null` → envelope has no `params` field (JSON-RPC doesn't accept `"params": null`).
721
+ - `create_rpc_get_url(endpoint_path, method, params?, id?)` GET URL with `?method=&id=&params=<JSON>`.
686
722
 
687
723
  Response assertions:
688
724
 
689
- - `assert_jsonrpc_error_response(body, expected_code?)` — validates
690
- `JsonrpcErrorResponse`; optional code check.
691
- - `assert_jsonrpc_success_response(body, output_schema?)` — validates
692
- `JsonrpcResponse`; optional `output_schema.safeParse(result)`.
725
+ - `assert_jsonrpc_error_response(body, expected_code?)` — validates `JsonrpcErrorResponse`; optional code check.
726
+ - `assert_jsonrpc_success_response(body, output_schema?)` — validates `JsonrpcResponse`; optional `output_schema.safeParse(result)`.
693
727
 
694
728
  One-shot transport:
695
729
 
696
- - `RpcTestTransport = (url, init) => Promise<Response>` — duck type
697
- `Hono.request` already satisfies.
730
+ - `RpcTestTransport = (url, init) => Promise<Response>` — duck type `Hono.request` already satisfies.
698
731
  - `http_transport(app)` — adapter for anything with a `request()` method.
699
732
  - `RpcCallResult` — discriminated `{ok: true, status, result}` / `{ok: false, status, error: {code, message, data?}}`.
700
733
  - `RpcCallArgs` — `{app, path, method, params?, headers?, id?, verb?}`. `verb` defaults to `'POST'`; use `'GET'` for `side_effects: false` methods.
@@ -718,21 +751,21 @@ Registry lookups:
718
751
  - unauthenticated → `unauthenticated` (code -32001)
719
752
  - wrong role → `forbidden` (-32002)
720
753
  - authenticated without role → `forbidden`
721
- - **keeper rejects non-daemon credentials** — session and api_token credentials are rejected even when the account has the keeper role (only `daemon_token` passes). The credential-type gate fires before the role gate (see `auth/CLAUDE.md` §`request_context.ts` for `require_credential_types`).
754
+ - **keeper rejects non-daemon credentials** — session and api_token credentials are rejected even when the account has the keeper role (only `daemon_token` passes). The credential-type gate fires before the role gate (see `auth/CLAUDE.md` §Keeper auth shape).
722
755
  - correct auth passes (not 401/403)
723
756
  - GET unauthenticated for `side_effects: false` reads
724
757
  2. **RPC adversarial envelopes** — fixed set exercising dispatcher steps 1–2: non-JSON body, wrong `jsonrpc` version, missing `jsonrpc` / `method` / `id`, batch array, unknown method, GET missing `method`/`id`, GET invalid JSON params, GET non-object params, GET mutation method → `invalid_request`.
725
758
  3. **RPC adversarial params** — reuses `generate_input_test_cases` but filters out structural cases (those hit envelope validation at step 1, not params validation at step 5). Every case expects 400 `invalid_params`.
726
759
 
727
- Skips silently when `surface.rpc_endpoints` is empty. Uses stub
728
- deps — no DB needed.
760
+ Skips silently when `surface.rpc_endpoints` is empty. Uses stub deps — no
761
+ DB needed.
729
762
 
730
763
  Options: `{build: () => AppSurfaceSpec, roles: Array<string>}`.
731
764
 
732
- **Opt-in bundles need their own per-bundle suite file.** Action bundles
733
- not folded into `create_standard_rpc_actions` (today `self_service_role_actions`,
734
- `actor_lookup_actions`, and `actor_search_actions`) get zero adversarial
735
- / round-trip coverage from `describe_rpc_attack_surface_tests` +
765
+ **Opt-in bundles need their own per-bundle suite file.** Action bundles not
766
+ folded into `create_standard_rpc_actions` (today `self_service_role_actions`,
767
+ `actor_lookup_actions`, and `actor_search_actions`) get zero adversarial /
768
+ round-trip coverage from `describe_rpc_attack_surface_tests` +
736
769
  `describe_rpc_round_trip_tests` unless the consumer ships a
737
770
  `<module>.rpc_suites.db.test.ts` mounting the opt-in factory on the RPC
738
771
  endpoint and calling both suites. See ../../test/CLAUDE.md §Composable
@@ -743,24 +776,313 @@ Test Suites for the obligation note; existing
743
776
 
744
777
  Shared conventions (`.db.test.ts` suffix, `isolate: false` semantics,
745
778
  `assert` from vitest, `assert_rejects`, `vi.mock` avoidance under
746
- `isolate: false`) live in Skill(fuz-stack) testing-patterns.
747
- fuz_app-specific points:
748
-
749
- - **`await_pending_effects: true`** is set by `create_test_app`.
750
- Fire-and-forget effects (audit logs, session touches, WS fan-out via
751
- `emit_after_commit`) resolve before the response returns, so tests
752
- can assert on side effects inline without manual flushing.
753
- - **Deep-path imports only.** Import from the canonical module
754
- (`testing/db.js`, `testing/rpc_helpers.js`, etc.); fuz_app's `dist/` ships no
755
- barrel.
756
- - **DI via small `*Deps` interfaces.** Stub factories accept the same
757
- narrow `*Deps` contracts production code uses never
758
- `Pick<GodType, ...>`. New helpers needing env/fs/logger take
759
- `EnvDeps` / `FsReadDeps` / `Logger` from `runtime/deps.ts` or
760
- `@fuzdev/fuz_util/log.js`.
761
- - **Keep the shared echo routes in sync with public surface.** When
762
- middleware or public API gains a new context variable, header, or
763
- field, update the echo in `middleware.ts`
764
- (`create_bearer_auth_test_app`, `create_test_middleware_stack_app`)
765
- alongside the assertions in `src/test/auth/*.test.ts`. Drift surfaces
766
- as a missed assertion, not a test failure.
779
+ `isolate: false`) live in Skill(fuz-stack) testing-patterns. fuz_app-specific
780
+ points:
781
+
782
+ - **`await_pending_effects: true`** is set by `create_test_app`. Fire-and-forget effects (audit logs, session touches, WS fan-out via `emit_after_commit`) resolve before the response returns, so tests can assert on side effects inline without manual flushing.
783
+ - **Deep-path imports only.** Import from the canonical module (`testing/db.js`, `testing/rpc_helpers.js`, etc.); fuz_app's `dist/` ships no barrel.
784
+ - **DI via small `*Deps` interfaces.** Stub factories accept the same narrow `*Deps` contracts production code uses — never `Pick<GodType, ...>`. New helpers needing env/fs/logger take `EnvDeps` / `FsReadDeps` / `Logger` from `runtime/deps.ts` or `@fuzdev/fuz_util/log.js`.
785
+ - **Keep the shared echo routes in sync with public surface.** When middleware or public API gains a new context variable, header, or field, update the echo in `middleware.ts` (`create_bearer_auth_test_app`, `create_test_middleware_stack_app`) alongside the assertions in `src/test/auth/*.test.ts`. Drift surfaces as a missed assertion, not a test failure.
786
+
787
+ ## Cross-backend integration layer
788
+
789
+ The standard test suites take a unified
790
+ `{setup_test, surface_source, capabilities}` shape so the same suite bodies
791
+ run against an in-process Hono harness today and against a spawned backend
792
+ over real HTTP either the Rust spine (`zzz_server`, `fuz_forge_server`, or
793
+ the non-domain `testing_spine_stub`) or a **TS** spine binary built on the
794
+ test-server core below (fuz_app's own domain-free `testing_spine_server`, run
795
+ on Node + Deno + Bun). In-process is the fast feedback path; cross-process is the
796
+ source of truth for wire-shape conformance.
797
+
798
+ ### Fixture protocol + capabilities
799
+
800
+ - `testing/cross_backend/setup.ts` — `SetupTest` / `TestFixture` /
801
+ `TestAccountFixture` / `CreateTestAccountOptions` types,
802
+ `default_in_process_setup(options)` (wraps `create_test_app`), and
803
+ `default_in_process_suite_options(options)` (emits the full Tier 1 suite
804
+ options bag: the `{setup_test, surface_source, capabilities}` triple plus
805
+ `session_options` / `create_route_specs` / `rpc_endpoints` pass-through;
806
+ call sites pass the output directly or spread it alongside
807
+ suite-specific extras like `roles`, `skip_routes`, `input_overrides`,
808
+ `db_factories`). Also exports `BootstrappedBackendHandle` (a
809
+ `BackendHandle` enriched with the keeper's captured credentials) and
810
+ `default_cross_process_setup(handle, options?)` — full runtime body.
811
+ Every per-test invocation unconditionally fires `_testing_reset` over the
812
+ keeper's daemon-token channel: wipes every auth-namespace row (no
813
+ keeper-preserve filter), resets `app_settings` + `bootstrap_lock`, and
814
+ inline-seeds a fresh keeper (`[ROLE_KEEPER, ROLE_ADMIN, ...extra_keeper_roles]`)
815
+ plus any declared `extra_accounts`. The fixture's `account` / `actor` /
816
+ cookies refresh to the new keeper on every call — in-process and
817
+ cross-process both run against a freshly bootstrapped keeper per test.
818
+ `fixture.create_account()` keeps a separate path: keeper-driven
819
+ `invite_create` (username-scoped) → signup → login → `account_token_create`
820
+ over the production RPC surface, so the invite-gated mint keeps
821
+ `open_signup` at its production default (`false`) and the per-test
822
+ secondary holds real session + bearer credentials.
823
+ `create_account({roles: [...]})` then drives `role_grant_offer_create`
824
+ (keeper) + `role_grant_offer_accept` (per-test) for each role — roles
825
+ whose `RoleSpec.grant_paths` don't include `'admin'` reject loudly at
826
+ offer-create time (`ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE`); those
827
+ roles must be seeded via `extra_accounts` at bootstrap-equivalent time
828
+ instead. Caller-supplied `username`s pass through _as-is_ now that the DB
829
+ wipes between tests — hardcoded names like `'user_two'` work and the
830
+ earlier uniquification prefix is gone. Every `TestFixture` also exposes
831
+ `fresh_transport({origin?: string | null})` — cookie-jar-free probe;
832
+ pass `{origin: null}` for bearer-only paths.
833
+
834
+ **Keeper ≠ admin.** `fixture.account` is the keeper account holding
835
+ `[ROLE_KEEPER, ROLE_ADMIN]` — the role split mirrors production
836
+ `bootstrap_account`. `ROLE_KEEPER` does not grant admin reach; bootstrap
837
+ just happens to land both as separate grants. Probing the separation (a
838
+ keeper-only account must 403 on admin RPCs) requires declaring
839
+ `extra_accounts: [{username, roles: [ROLE_KEEPER]}]` — `ROLE_KEEPER`'s
840
+ `grant_paths` is bootstrap-only, so a post-bootstrap offer/accept can't
841
+ deliver it.
842
+
843
+ - `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
844
+ (`bearer_auth` / `trusted_proxy` / `login_rate_limit` / `ws` / `sse` /
845
+ `in_process_only`), `test_if(cond, name, fn)` for capability-gated cases,
846
+ and `in_process_capabilities` preset.
847
+
848
+ ### `cross_backend/standard.ts` — `describe_standard_cross_process_tests`
849
+
850
+ Cross-process counterpart to `describe_standard_tests`. Wires the
851
+ cross-process-safe subset in one call:
852
+
853
+ - `integration` — always
854
+ - `admin` — `roles` provided
855
+ - `round_trip` — always
856
+ - `rpc_round_trip` — always
857
+ - `data_exposure` — always
858
+
859
+ Three suites from the in-process bundle are omitted by design:
860
+ `rate_limiting` (needs a fresh per-test `TestApp` for tight rate-limiter
861
+ overrides; the spawned binary has no restart-per-test budget),
862
+ `audit_completeness` (reads FK structure that only the in-process backend
863
+ exposes; wire-level audit observability lives in the consumer's own audit
864
+ `.cross.test.ts`), `bootstrap_success` (bootstrap is one-shot per backend
865
+ lifecycle, already consumed by the consumer's `globalSetup`). The omission
866
+ rationale lives in the module doc once instead of repeating in each
867
+ consumer's `*.cross.test.ts`.
868
+
869
+ Hard-codes the cross-process-safe set with no `skip` knob; if a future
870
+ consumer needs partial opt-out, add the knob then.
871
+ `StandardCrossProcessTestOptions` is shape-aligned with
872
+ `StandardTestOptions` minus the in-process-only knobs (`create_route_specs`,
873
+ `bootstrap`, `rate_limiting_app_options`, `bootstrap_token`) — those drive
874
+ the omitted suites.
875
+
876
+ ### `cross_backend/ws_round_trip.ts` — `describe_cross_process_ws_tests`
877
+
878
+ Real-upgrade WebSocket coverage of a spawned backend — the cross-process
879
+ counterpart to the in-process `ws_round_trip.ts` harness, kept a separate
880
+ call (not folded into `describe_standard_cross_process_tests`) because it
881
+ needs raw `base_url` / `ws_path` the standard bundle doesn't carry, mirroring
882
+ how `describe_ws_round_trip_tests` sits beside `describe_standard_tests`
883
+ in-process. `describe_cross_process_ws_tests({setup_test, capabilities,
884
+ base_url, ws_path, origin?, rpc_path?})` opens a live `WebSocket` via
885
+ `create_ws_transport` (the `ws` npm package) and asserts up to four cases
886
+ against the upgrade stack `register_ws_endpoint` wires (origin →
887
+ `require_auth` → dispatch): authed upgrade round-trips `heartbeat`,
888
+ anonymous upgrade refused, disallowed-origin upgrade refused, and — gated on
889
+ `rpc_path` — a live socket drops when the account's sessions are revoked
890
+ mid-connection (`account_session_revoke_all` over the keeper session channel
891
+ emits `session_revoke_all`, which `create_ws_auth_guard` closes on; asserted
892
+ via `WsClient.wait_for_close`). Per-connection auth is enforced **at upgrade
893
+ time**, so the negative upgrade cases assert the upgrade itself rejects, not
894
+ a per-message error; the close-on-revoke case proves the audit-fed guard is
895
+ the revocation seam for an already-open socket, since per-message dispatch
896
+ never re-checks credential validity. Omit `rpc_path` to skip the close case
897
+ (consumers without the standard account actions on their RPC endpoint).
898
+ **Consumer-agnostic** — it drives only the `heartbeat` protocol action
899
+ (guaranteed on every WS endpoint by `assert_ws_endpoints_include_protocol_actions`),
900
+ so it validates the transport without touching domain WS methods. Gated on
901
+ `capabilities.ws`; cross-process only (needs a real bound socket — wire from
902
+ a `*.cross.test.ts`, never an in-process setup). Authed cookies come from the
903
+ fresh-per-test keeper via `fixture.transport.cookies()`, not the stale
904
+ globalSetup handle. fuz_app's own wiring is `src/test/cross_backend/ws.cross.test.ts`.
905
+
906
+ ### `cross_backend/sse_round_trip.ts` — `describe_cross_process_sse_tests`
907
+
908
+ Cross-process counterpart to the in-process `sse_round_trip.ts` harness —
909
+ opens a **real** streaming `fetch` against a spawned backend's audit-log SSE
910
+ endpoint via `create_sse_transport` (built-in `fetch` + `TextDecoder`, no
911
+ dep), threading the fresh-per-test keeper's session cookie. Kept a separate
912
+ call (not folded into `describe_standard_cross_process_tests`) for the same
913
+ reason the WS suite is — it needs raw `base_url` / `sse_path` the standard
914
+ bundle doesn't carry. Up to three cases, mirroring the in-process audit-log
915
+ self-test: the stream emits the `: connected` comment; a minted secondary's
916
+ sessions are revoked over the keeper's admin channel (`admin_session_revoke_all`),
917
+ broadcasting one `session_revoke_all` audit `data:` frame **without** closing
918
+ the keeper's stream (target ≠ subscriber — secondary minted before the stream
919
+ opens so its `create_account` audit events stay off it); and the subscriber's
920
+ _own_ sessions are revoked (`account_session_revoke_all`) so the audit guard
921
+ drops the live stream (asserted via `SseTransport.wait_for_close`). The
922
+ data-frame + close cases gate on `rpc_path` (they drive the standard
923
+ account/admin actions); all cases gate on `capabilities.sse`. Cross-process
924
+ only — wire from a `*.cross.test.ts`. fuz_app's own wiring is
925
+ `src/test/cross_backend/sse.cross.test.ts`; only the TS spines advertise
926
+ `sse` (they wire `audit_log_sse`), so the Rust `spine_stub` cases `.skip`.
927
+
928
+ ### Cross-process plumbing (consumed by `*.cross.test.ts` suites)
929
+
930
+ - `testing/cross_backend/backend_config.ts` — `BackendConfig` +
931
+ `BackendBootstrapConfig` interfaces. Consumer factories
932
+ (`deno_backend_config()`, `rust_backend_config()`,
933
+ `spine_stub_backend_config()`) produce these; fuz_app ships
934
+ `spine_stub_backend_config()` as a convenience preset for the non-domain
935
+ third spine consumer, but otherwise backend-specific paths and env are a
936
+ consumer concern.
937
+ - `testing/cross_backend/spawn_backend.ts` — `spawn_backend(config) => BackendHandle`.
938
+ Writes the bootstrap token, spawns `detached: true` in its own process
939
+ group (so SIGTERM to the negative pid tears down descendants), polls
940
+ health, reads the deterministic daemon token from the binary-written
941
+ file. Registers exit-time + signal cleanup so vitest worker death or
942
+ Ctrl+C kills children before they strand ports.
943
+ - `testing/transports/fetch_transport.ts` — cookie-threading HTTP transport
944
+ satisfying `RpcTestTransport`. Carries a name-keyed cookie jar that
945
+ updates on every response's `Set-Cookie` and re-sends on every request;
946
+ `Origin` defaults to `base_url` (`origin: null` disables for bearer-only
947
+ paths). Exposes `cookies()` so `ws_transport` can thread the session
948
+ cookie onto the WS upgrade.
949
+ - `testing/transports/bootstrap.ts` — stateless `bootstrap({transport, config})`
950
+ POSTs `/api/account/bootstrap` against the running binary, parses the
951
+ `{ok, account, actor}` envelope, returns the keeper credentials. The
952
+ transport carries the keeper session cookie in its jar after this call
953
+ resolves.
954
+ - `testing/transports/ws_client.ts` — shared `WsClient` interface (`send` /
955
+ `request` / `close` / `messages` / `wait_for` / `wait_for_close`),
956
+ wire-frame types, and
957
+ predicates (`is_notification`, `is_response_for`, ...). Both in-process
958
+ (`ws_round_trip.ts`) and cross-process (`ws_transport.ts`) impls satisfy
959
+ this interface.
960
+ - `testing/transports/ws_transport.ts` — `create_ws_transport({base_url, ws_path, cookies, origin?})`
961
+ builds a real-upgrade WS client using the `ws` npm package (optional
962
+ peerDep; consumers wiring cross-process tests `npm install --save-dev ws`).
963
+ Threads the keeper cookie onto the upgrade so per-action auth succeeds on
964
+ the first message.
965
+ - `testing/transports/sse_frame_reader.ts` — `create_sse_frame_reader(reader, default_timeout_ms?)`,
966
+ the transport-agnostic SSE framing core over a
967
+ `ReadableStreamDefaultReader<Uint8Array>`: `\n\n` framing, per-read timeout,
968
+ `read_frame` / `wait_for_close` / `cancel`. Shared by the in-process route
969
+ suite (`sse_round_trip.ts`, over a Hono `Response.body`) and the
970
+ cross-process transport below (over a streaming `fetch` body).
971
+ - `testing/transports/sse_transport.ts` — `create_sse_transport({base_url, sse_path, cookies, origin?})`
972
+ opens a real streaming `fetch` (threading the keeper cookie), validates the
973
+ `text/event-stream` connect, then delegates frame reading to
974
+ `create_sse_frame_reader`. Built-in `fetch` + `TextDecoder` — no dep.
975
+ - `surface_source: AppSurfaceSpec` — the same shape both in-process and
976
+ cross-process tests pass. Constructed in TS via
977
+ `create_test_app_surface_spec` (or a consumer's equivalent like
978
+ `create_zzz_app_surface_spec`) — same builder both modes use. The
979
+ cross-process-ness lives in `setup_test: default_cross_process_setup(handle)`
980
+ — the `FetchTransport`, not the schema source. The on-disk
981
+ `*_attack_surface.json` snapshot is an observability artifact for human
982
+ inspection + gen-time drift detection
983
+ (`assert_surface_matches_snapshot`); it is not consumed at test runtime.
984
+ - `testing/cross_backend/testing_reset_actions.ts` —
985
+ `create_testing_actions(deps, options)` factory returning the
986
+ `_testing_reset` RPC action. Test binaries register it on their RPC
987
+ endpoint; `default_cross_process_setup` fires it unconditionally per
988
+ test. Handler DELETEs every auth-namespace row (no keeper-preserve
989
+ filter), resets `app_settings` to production defaults, flips
990
+ `bootstrap_lock.bootstrapped = true`, inline-seeds a fresh keeper via
991
+ `create_test_account_with_credentials` (same primitive in-process uses,
992
+ keeping write semantics in parity), seeds any `extra_accounts` at the
993
+ same bootstrap-equivalent step (the only path for roles like
994
+ `ROLE_KEEPER` whose `grant_paths` is bootstrap-only), refreshes
995
+ `DaemonTokenState.keeper_account_id` to the new row, then fires the
996
+ consumer-supplied `reset_state` callback for domain-state reset. Auth
997
+ gates on `credential_types: ['daemon_token']` — effectively keeper-only
998
+ without forcing the `actor: 'required'` ⟺ `acting?: ActingActor`
999
+ biconditional. No free-form runtime grant action exists — see the
1000
+ `testing_reset_actions.ts` TSDoc for the audit + WS fan-out rationale
1001
+ that rejected a `_testing_seed_role_grant` shape.
1002
+
1003
+ ### Building a TS test-server binary — `testing_server_core.ts` + adapters
1004
+
1005
+ The reusable shape for standing up a **spawnable TS** cross-process test
1006
+ binary (the TS analog of the Rust `testing_spine_stub`), so consumers don't
1007
+ re-roll the serve / daemon-info / WS-attach / drain boilerplate:
1008
+
1009
+ - `testing/cross_backend/testing_server_core.ts` — `start_testing_server({adapter, daemon_name, host, port, app_version?, build_app})`. Owns the runtime-neutral orchestration: open-host refusal, stale-daemon check, daemon-info write, `serve`, post-serve WS attach, graceful drain. Domain-free — the app is the caller's `build_app(): Promise<BuiltTestingApp>` seam (`{app, close, mount_websocket?}`). `mount_websocket(upgrade)` is invoked after the app exists + the adapter prepared WS (the mount-after-app order Node's `@hono/node-ws` forces — `create_app_server`'s `ws_endpoints` auto-mount can't be used on Node). Exports the `TestingServerAdapter` / `ServeHandle` / `PreparedWebsocket` interfaces.
1010
+ - `testing/cross_backend/testing_server_node.ts` — `create_node_testing_adapter()` (`@hono/node-server` + `@hono/node-ws`). Optional peer deps (like `ws`); only test binaries import them.
1011
+ - `testing/cross_backend/testing_server_deno.ts` — `create_deno_testing_adapter()` (`Deno.serve` + `hono/deno`; `Deno` declared locally so it typechecks under the Node toolchain). Spawn the entry with `--sloppy-imports` (Deno doesn't do `.js`→`.ts`; Gro's loader does, so the Node path needs no flag).
1012
+ - `testing/cross_backend/testing_server_bun.ts` — `create_bun_testing_adapter()` (`Bun.serve` + `hono/bun`'s module-level `upgradeWebSocket` + `websocket`; `Bun.serve` declared locally so it typechecks under the Node toolchain). **No extra deps** (`hono/bun` ships with `hono`; `Bun.serve` is built in, unlike Node's `@hono/node-server` + `@hono/node-ws`), and Bun resolves `.js`→`.ts` natively (no flag, unlike Deno). Reuses `create_node_runtime` (Bun implements the `node:fs`/`node:process` surface). WS is module-level + stateless (like Deno) — the `websocket` handler is threaded into `serve`, where `Bun.serve` wants it, so no post-serve attach.
1013
+ - `testing/cross_backend/default_spine_surface.ts` — the canonical no-domain spine surface (account/admin/audit/signup + bootstrap): `spine_session_options`, `spine_roles`, `create_spine_route_specs`, `spine_rpc_endpoints`, `create_spine_surface_spec`. `$lib`-free (it's reached by the spawned binary under Gro's loader, which doesn't resolve `$lib`), so keep it on relative imports. Shared by the spine_stub cross test, the TS cross tests, and the binary.
1014
+ - `testing/cross_backend/ts_spine_backend_config.ts` — `ts_spine_node_backend_config()` / `ts_spine_deno_backend_config()` / `ts_spine_bun_backend_config()` presets (in-memory PGlite, no external infra), the TS analog of `spine_stub_backend_config()`.
1015
+
1016
+ fuz_app's own binary wiring (`src/test/cross_backend/testing_spine_server{,_node,_deno,_bun}.ts`) is the worked example: ~one `build_app` over `create_app_backend` + `create_app_server` + `_testing_reset` + a WS mount, reusing `default_spine_surface`. The `_node`/`_deno`/`_bun` entries differ only in which adapter they wire — `build_spine_app` is runtime-agnostic.
1017
+
1018
+ The in-process `ws_round_trip` harness stays (it drives the dispatcher
1019
+ against a fake upgrade, no wire), but the real-upgrade coverage now lives in
1020
+ the cross-process `cross_backend/ws_round_trip.ts` suite below — including
1021
+ close-on-revoke (`WsClient.wait_for_close` asserts the audit-guard drops a
1022
+ live socket on `session_revoke_all`).
1023
+
1024
+ `audit_completeness` is in-process by design (FK-structural introspection
1025
+ beyond the `audit_log_list` RPC reads — structurally in-process).
1026
+
1027
+ **Cross-process SSE** is wired (see §`cross_backend/sse_round_trip.ts`
1028
+ above). The TS spine binary serves `GET /api/admin/audit/stream` —
1029
+ `build_spine_app` passes `audit_log_sse: true` and `create_spine_route_specs`
1030
+ mounts `create_audit_log_route_specs({stream: ctx.audit_sse})` when
1031
+ `ctx.audit_sse` is set (keeps `default_spine_surface.ts` `$lib`-free and the
1032
+ shared surface snapshot SSE-free, since the surface stub ctx has
1033
+ `audit_sse: null`). `capabilities.sse` is scoped to the TS spine configs
1034
+ (`ts_spine_backend_config.ts`), not the shared `ts_default_capabilities`,
1035
+ which stays honest for consumers who don't wire `audit_log_sse`. The
1036
+ real-HTTP `transports/sse_transport.ts` feeds `describe_cross_process_sse_tests`.
1037
+
1038
+ The auth-cost handling for cross-process testing is consumer-side: each
1039
+ consumer ships a separate test binary wiring a fast-params
1040
+ `TestingArgon2idHasher` from a sibling Rust testing crate. Cross-process
1041
+ `bootstrap` + `create_account` are then plain RPC calls against the test
1042
+ binary — no DB-direct surgery in fuz_app's testing library, no runtime
1043
+ knobs in production code, no shared cookie key with the backend.
1044
+
1045
+ ### cross_backend/bench/ — cross-impl measurement
1046
+
1047
+ Generic primitive for cross-impl **measurement**: drive identical wire
1048
+ scenarios across several spawned backends and time each round trip so a
1049
+ TS impl and a Rust impl compare apples-to-apples (both cross-process over
1050
+ real HTTP). A thin
1051
+ scenario→task→report adapter over `@fuzdev/fuz_util`'s benchmark library
1052
+ (`Benchmark`, `benchmark_stats_compare`, `benchmark_format_markdown`) —
1053
+ no stats engine reinvented. fuz_app ships the primitive; consumers wire
1054
+ scenarios + the run (zzz's `npm run benchmark:cross-impl` was the first).
1055
+ fuz_app also ships its **own** `npm run benchmark:cross-impl`
1056
+ (`src/benchmarks/cross_impl.bench.ts`) on the back of its TS spine binary —
1057
+ ts-node + ts-deno + ts-bun (+ the Rust `spine_stub` when `FUZ_TESTING_SPINE_STUB_BIN`
1058
+ is set). The three TS runtimes are apples-to-apples with each other (same
1059
+ PGlite driver); TS-vs-Rust carries the PGlite-vs-Postgres DB-layer caveat
1060
+ (documented in the run). The artifact (`*.latest.json`) is gitignored.
1061
+
1062
+ - `bench/scenario.ts` — `BenchScenario` (`{name, requires?, run}`) +
1063
+ `BenchScenarioContext` (pre-authed `transport`, `rpc_path`,
1064
+ `capabilities`). The `run` body is the timed task; it `throw`s on a
1065
+ non-success envelope so the benchmark records a failed iteration.
1066
+ `default_bench_scenarios` are read-only spine-surface calls
1067
+ (`account_verify` dispatch floor, `account_session_list`,
1068
+ `audit_log_list`) — idempotent, so safe to repeat against one
1069
+ bootstrapped keeper without a per-iteration `_testing_reset`. `login`
1070
+ is omitted on purpose (test binaries use a fast hasher, so it'd measure
1071
+ dispatch not real Argon2).
1072
+ - `bench/run_cross_impl_bench.ts` — `run_cross_impl_bench({handles, scenarios, config?})`
1073
+ bootstraps each backend once (uses `handle.keeper_transport`; **no reset
1074
+ in the hot loop**), runs each scenario as a one-task `Benchmark` named by
1075
+ the backend, returns `CrossImplBenchResult` (`{backends, scenarios, entries}`).
1076
+ Network-tuned defaults override fuz_util's micro defaults
1077
+ (warmup 20 / min 100 / duration 3000ms).
1078
+ - `bench/bench_report.ts` — `format_cross_impl_markdown` (per-scenario
1079
+ table, backend rows), `compare_cross_impl` (Welch verdict per scenario
1080
+ vs a reference backend) + `format_cross_impl_comparison`, and
1081
+ `format_cross_impl_json` (self-describing artifact: per backend×scenario
1082
+ percentiles off the raw-sample tail, budget, iteration count).
1083
+
1084
+ Tail honesty depends on a fuz_util change: `BenchmarkStats` computes order
1085
+ statistics (min/max/p50–p99) on raw samples while central-tendency stats
1086
+ stay MAD-cleaned — so p99 reflects real tail events. Deeper tiers (resource
1087
+ sampling, workload corpora, load/soak) and the static-docs dashboard with
1088
+ committed historical fixtures stay deferred until CI automation exists.