@fuzdev/fuz_app 0.65.0 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/actions/CLAUDE.md +65 -86
  2. package/dist/actions/action_codegen.d.ts +1 -1
  3. package/dist/actions/action_codegen.js +1 -1
  4. package/dist/actions/action_event_data.d.ts +1 -1
  5. package/dist/auth/CLAUDE.md +83 -104
  6. package/dist/auth/audit_log_schema.js +2 -2
  7. package/dist/auth/daemon_token_middleware.d.ts +15 -5
  8. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  9. package/dist/auth/daemon_token_middleware.js +24 -15
  10. package/dist/auth/invite_queries.d.ts +17 -7
  11. package/dist/auth/invite_queries.d.ts.map +1 -1
  12. package/dist/auth/invite_queries.js +19 -8
  13. package/dist/auth/signup_routes.d.ts +47 -1
  14. package/dist/auth/signup_routes.d.ts.map +1 -1
  15. package/dist/auth/signup_routes.js +103 -52
  16. package/dist/env/resolve.d.ts +44 -7
  17. package/dist/env/resolve.d.ts.map +1 -1
  18. package/dist/env/resolve.js +94 -27
  19. package/dist/http/CLAUDE.md +47 -52
  20. package/dist/http/jsonrpc.d.ts +23 -7
  21. package/dist/http/jsonrpc.d.ts.map +1 -1
  22. package/dist/http/jsonrpc.js +19 -3
  23. package/dist/http/surface.d.ts +9 -2
  24. package/dist/http/surface.d.ts.map +1 -1
  25. package/dist/runtime/mock.d.ts +1 -1
  26. package/dist/runtime/mock.js +1 -1
  27. package/dist/testing/CLAUDE.md +659 -511
  28. package/dist/testing/admin_integration.d.ts +5 -5
  29. package/dist/testing/admin_integration.d.ts.map +1 -1
  30. package/dist/testing/admin_integration.js +95 -39
  31. package/dist/testing/app_server.d.ts +16 -1
  32. package/dist/testing/app_server.d.ts.map +1 -1
  33. package/dist/testing/app_server.js +18 -3
  34. package/dist/testing/audit_completeness.d.ts +7 -5
  35. package/dist/testing/audit_completeness.d.ts.map +1 -1
  36. package/dist/testing/audit_completeness.js +5 -9
  37. package/dist/testing/bootstrap_success.js +2 -2
  38. package/dist/testing/cross_backend/backend_config.d.ts +113 -0
  39. package/dist/testing/cross_backend/backend_config.d.ts.map +1 -0
  40. package/dist/testing/cross_backend/backend_config.js +1 -0
  41. package/dist/testing/cross_backend/bench/bench_report.d.ts +46 -0
  42. package/dist/testing/cross_backend/bench/bench_report.d.ts.map +1 -0
  43. package/dist/testing/cross_backend/bench/bench_report.js +83 -0
  44. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +44 -0
  45. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts.map +1 -0
  46. package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +38 -0
  47. package/dist/testing/cross_backend/bench/scenario.d.ts +57 -0
  48. package/dist/testing/cross_backend/bench/scenario.d.ts.map +1 -0
  49. package/dist/testing/cross_backend/bench/scenario.js +28 -0
  50. package/dist/testing/cross_backend/bootstrap_backend.d.ts +41 -0
  51. package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -0
  52. package/dist/testing/cross_backend/bootstrap_backend.js +34 -0
  53. package/dist/testing/cross_backend/build_test_backend_paths.d.ts +24 -0
  54. package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -0
  55. package/dist/testing/cross_backend/build_test_backend_paths.js +33 -0
  56. package/dist/testing/cross_backend/capabilities.d.ts +3 -2
  57. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  58. package/dist/testing/cross_backend/default_backend_configs.d.ts +122 -0
  59. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -0
  60. package/dist/testing/cross_backend/default_backend_configs.js +111 -0
  61. package/dist/testing/cross_backend/default_secrets.d.ts +40 -0
  62. package/dist/testing/cross_backend/default_secrets.d.ts.map +1 -0
  63. package/dist/testing/cross_backend/default_secrets.js +39 -0
  64. package/dist/testing/cross_backend/default_spine_surface.d.ts +64 -0
  65. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -0
  66. package/dist/testing/cross_backend/default_spine_surface.js +121 -0
  67. package/dist/testing/cross_backend/setup.d.ts +270 -34
  68. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  69. package/dist/testing/cross_backend/setup.js +495 -15
  70. package/dist/testing/cross_backend/spawn_backend.d.ts +58 -0
  71. package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -0
  72. package/dist/testing/cross_backend/spawn_backend.js +229 -0
  73. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts +66 -0
  74. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts.map +1 -0
  75. package/dist/testing/cross_backend/spine_stub_backend_config.js +49 -0
  76. package/dist/testing/cross_backend/sse_round_trip.d.ts +37 -0
  77. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -0
  78. package/dist/testing/cross_backend/sse_round_trip.js +137 -0
  79. package/dist/testing/cross_backend/standard.d.ts +96 -0
  80. package/dist/testing/cross_backend/standard.d.ts.map +1 -0
  81. package/dist/testing/cross_backend/standard.js +49 -0
  82. package/dist/testing/cross_backend/testing_reset_actions.d.ts +171 -0
  83. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -0
  84. package/dist/testing/cross_backend/testing_reset_actions.js +213 -0
  85. package/dist/testing/cross_backend/testing_server_bun.d.ts +5 -0
  86. package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -0
  87. package/dist/testing/cross_backend/testing_server_bun.js +59 -0
  88. package/dist/testing/cross_backend/testing_server_core.d.ts +140 -0
  89. package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -0
  90. package/dist/testing/cross_backend/testing_server_core.js +68 -0
  91. package/dist/testing/cross_backend/testing_server_deno.d.ts +5 -0
  92. package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -0
  93. package/dist/testing/cross_backend/testing_server_deno.js +37 -0
  94. package/dist/testing/cross_backend/testing_server_node.d.ts +5 -0
  95. package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -0
  96. package/dist/testing/cross_backend/testing_server_node.js +50 -0
  97. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +72 -0
  98. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -0
  99. package/dist/testing/cross_backend/ts_spine_backend_config.js +112 -0
  100. package/dist/testing/cross_backend/ws_round_trip.d.ts +35 -0
  101. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -0
  102. package/dist/testing/cross_backend/ws_round_trip.js +113 -0
  103. package/dist/testing/data_exposure.d.ts +4 -6
  104. package/dist/testing/data_exposure.d.ts.map +1 -1
  105. package/dist/testing/data_exposure.js +1 -5
  106. package/dist/testing/db_entities.d.ts +18 -7
  107. package/dist/testing/db_entities.d.ts.map +1 -1
  108. package/dist/testing/db_entities.js +18 -7
  109. package/dist/testing/integration.d.ts +27 -6
  110. package/dist/testing/integration.d.ts.map +1 -1
  111. package/dist/testing/integration.js +93 -58
  112. package/dist/testing/round_trip.d.ts +4 -5
  113. package/dist/testing/round_trip.d.ts.map +1 -1
  114. package/dist/testing/round_trip.js +1 -5
  115. package/dist/testing/rpc_helpers.d.ts +10 -4
  116. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  117. package/dist/testing/rpc_helpers.js +1 -1
  118. package/dist/testing/rpc_round_trip.d.ts +5 -5
  119. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  120. package/dist/testing/rpc_round_trip.js +1 -5
  121. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  122. package/dist/testing/sse_round_trip.js +1 -68
  123. package/dist/testing/standard.d.ts +4 -5
  124. package/dist/testing/standard.d.ts.map +1 -1
  125. package/dist/testing/stubs.d.ts +10 -3
  126. package/dist/testing/stubs.d.ts.map +1 -1
  127. package/dist/testing/stubs.js +9 -2
  128. package/dist/testing/testing_rate_limiter.d.ts +59 -0
  129. package/dist/testing/testing_rate_limiter.d.ts.map +1 -0
  130. package/dist/testing/testing_rate_limiter.js +74 -0
  131. package/dist/testing/transports/bootstrap.d.ts +52 -0
  132. package/dist/testing/transports/bootstrap.d.ts.map +1 -0
  133. package/dist/testing/transports/bootstrap.js +70 -0
  134. package/dist/testing/transports/fetch_transport.d.ts +81 -0
  135. package/dist/testing/transports/fetch_transport.d.ts.map +1 -0
  136. package/dist/testing/transports/fetch_transport.js +74 -0
  137. package/dist/testing/transports/sse_frame_reader.d.ts +41 -0
  138. package/dist/testing/transports/sse_frame_reader.d.ts.map +1 -0
  139. package/dist/testing/transports/sse_frame_reader.js +84 -0
  140. package/dist/testing/transports/sse_transport.d.ts +54 -0
  141. package/dist/testing/transports/sse_transport.d.ts.map +1 -0
  142. package/dist/testing/transports/sse_transport.js +51 -0
  143. package/dist/testing/transports/ws_client.d.ts +108 -0
  144. package/dist/testing/transports/ws_client.d.ts.map +1 -0
  145. package/dist/testing/transports/ws_client.js +56 -0
  146. package/dist/testing/transports/ws_transport.d.ts +43 -0
  147. package/dist/testing/transports/ws_transport.d.ts.map +1 -0
  148. package/dist/testing/transports/ws_transport.js +169 -0
  149. package/dist/testing/ws_round_trip.d.ts +21 -103
  150. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  151. package/dist/testing/ws_round_trip.js +42 -40
  152. package/dist/ui/CLAUDE.md +5 -3
  153. package/dist/ui/MenuLink.svelte +16 -16
  154. package/dist/ui/MenuLink.svelte.d.ts +13 -4
  155. package/dist/ui/MenuLink.svelte.d.ts.map +1 -1
  156. package/package.json +7 -1
  157. package/dist/testing/transports/surface_source.d.ts +0 -51
  158. package/dist/testing/transports/surface_source.d.ts.map +0 -1
  159. package/dist/testing/transports/surface_source.js +0 -19
@@ -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,106 +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 + stub middleware + surface generation. `CreateTestAppSurfaceSpecOptions` accepts `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 you'd pass in production to mount the routes at `bootstrap.route_prefix ?? '/api/account'`). 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_keeper`
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`.
84
74
 
85
75
  `create_test_role_grant_direct(db, input)` wraps `query_create_role_grant`
86
- for tests that need an active role_grant seeded directly, bypassing the
76
+ for tests needing an active role_grant seeded directly, bypassing the
87
77
  production offer/accept consent flow. Use only when the test focuses on
88
- revoke or isolation semantics rather than the consent path itself — the
89
- schema permits null `source_offer_id` for exactly this case. For tests
90
- that exercise the production grant flow, drive
91
- `role_grant_offer_and_accept` from `role_grant_helpers.ts` instead.
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.
92
82
 
93
83
  ### `role_grant_helpers.ts` — RPC-flow role_grant helpers
94
84
 
95
85
  `role_grant_offer_and_accept({app, rpc_path, grantor, recipient, role})`
96
- drives the full consent flow (grantor `role_grant_offer_create` →
97
- recipient `role_grant_offer_accept`) over the production RPC surface and
98
- returns `{offer_id, role_grant_id}`. Sibling to
99
- `create_test_role_grant_direct` in `db_entities.ts` that one bypasses
100
- the consent flow; this one exercises it end-to-end so the suite picks up
101
- post-commit fan-out (audit, SSE broadcasts, `_supersede` notifications)
102
- that a direct DB seed would miss. `grantor` and `recipient` accept
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
103
92
  `TestApp | TestAccount` / `TestAccount` so the call site passes the same
104
93
  object that already owns the headers + account id, ruling out caller-side
105
94
  mismatch.
106
95
 
107
96
  ### `audit_drift_guard.ts` — audit-emission validation
108
97
 
109
- | Helper | Role |
110
- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
111
- | `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. |
112
- | `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`). |
113
- | `AuditEmitMarker` | `{kind: 'emit'; at: number}` — the type of marker `create_emit_ordering_audit_factory` pushes. |
114
- | `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. |
115
- | `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>}`.
116
103
 
117
104
  ### `connection_closer_helpers.ts` — `ConnectionCloser` test doubles
118
105
 
119
- | Helper | Role |
120
- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
121
- | `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. |
122
- | `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. |
123
- | `RecordedClose` | `{method: 'session' \| 'token' \| 'account', id, at}` — recorded shape pushed by the closer. |
124
- | `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}`.
125
110
 
126
111
  ## Database — `db.ts`
127
112
 
@@ -129,30 +114,27 @@ Factory builders for parameterized DB tests. Consumer projects pass their
129
114
  `init_schema` callback (which calls `run_migrations(db, [auth_migration_ns, ...app_migrations])`);
130
115
  factories accept any migration namespace set.
131
116
 
132
- | Helper | Role |
133
- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
134
- | `IS_CI` | `process.env.CI === 'true'` CI detection. |
135
- | `DbFactory` interface | `{name, create, close, skip, skip_reason?}`. |
136
- | `reset_pglite(db)` | `DROP SCHEMA public CASCADE` + recreate. Reuses a live PGlite instance. |
137
- | `create_pglite_factory(init_schema)` | In-memory; no external deps; `skip: false`. See WASM caching below. |
138
- | `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. |
139
- | `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. |
140
- | `auth_integration_truncate_tables` | `auth_truncate_tables + ['audit_log']` for integration suites that exercise the audit path. |
141
- | `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. |
142
- | `drop_auth_schema(db)` | `DROP TABLE IF EXISTS <table> CASCADE` for every entry in `auth_drop_tables` plus `schema_version`. Safe on fresh DBs. |
143
- | `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`. |
144
- | `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.
145
128
 
146
129
  **PGlite WASM caching.** `create_pglite_factory` shares a single PGlite
147
130
  instance in a module-level ref (`module_db`) across all factories in the
148
131
  same vitest worker thread. Subsequent `create()` calls
149
- `DROP SCHEMA public CASCADE` instead of paying the ~500–700ms WASM
150
- cold-start cost again. Since each vitest file runs in its own worker,
151
- there is no cross-file contamination — but inside a file, suites share
152
- state until the schema is reset. The `db` vitest project (opted into by
153
- the `.db.test.ts` suffix) runs with `isolate: false` +
154
- `fileParallelism: false` to amortize the WASM boot across every DB test
155
- 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.
156
138
 
157
139
  ## Test app assembly
158
140
 
@@ -165,58 +147,44 @@ fully assembled Hono app + the backend + helpers.
165
147
 
166
148
  Key module-scope values:
167
149
 
168
- - `stub_password_deps` — `PasswordHashDeps` that hashes via
169
- `stub_hash_${password}` and verifies by equality. Deterministic, no
170
- Argon2 costuse for every test that isn't specifically exercising
171
- password hashing.
172
- - `TEST_COOKIE_SECRET` 64-hex-char deterministic cookie secret.
173
- Produces a valid `Keyring` via `create_validated_keyring`. Never used
174
- in production — the stub guard plus fixed value is the contract.
175
- - `fallback_pglite_factory` — module-level PGlite factory that
176
- `create_test_app_server` uses when no `db` is passed. Reuses the WASM
177
- cache via `create_pglite_factory`.
178
-
179
- Two helpers share the "insert account + actor + roles + API token +
180
- session + cookie" flow, split by intent:
181
-
182
- - `bootstrap_test_keeper(options)` — keeper path used by
183
- `create_test_app_server`. Same body as the general helper plus a
184
- lock flip (`UPDATE bootstrap_lock SET bootstrapped = true ...`) so
185
- test DB state matches a real bootstrap completion, letting
186
- production code trust the lock as the single signal.
187
- - `create_test_account_with_credentials(options)` — general path used
188
- by `TestApp.create_account` for additional non-keeper accounts. Same
189
- body, no lock interaction (additional accounts aren't bootstraps).
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).
190
159
 
191
160
  Both take `{db, keyring, session_options, password, username?, password_value?, roles?}`
192
- (the shared `CreateTestAccountWithCredentialsOptions` / `BootstrapTestKeeperOptions`).
193
-
194
- For exercising the bootstrap success path end-to-end against an empty
195
- DB (no pre-keeper, lock unflipped), use `create_test_app_for_bootstrap`
196
- instead — pair with `describe_bootstrap_success_tests` for the
197
- consumer-runnable suite.
198
-
199
- | Type | Shape |
200
- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
201
- | `TestAppServer extends AppBackend` | Adds `account`, `actor`, `api_token`, `session_cookie`, `keyring`, `cleanup()`. |
202
- | `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. |
203
- | `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`). |
204
- | `TestAccount` | `{account, actor, session_cookie, api_token, create_session_headers, create_bearer_headers}`. |
205
- | `TestApp` | `{app, backend, surface_spec, surface, route_specs, create_session_headers, create_bearer_headers, create_daemon_token_headers, create_account, cleanup}`. |
206
- | `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). |
207
- | `TestAppForBootstrap` | `{app, backend, surface_spec, surface, route_specs, create_request_headers, cleanup}`. No keeper credentials (test drives bootstrap itself). |
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).
208
176
 
209
177
  `create_test_app` hard-codes the test-friendly `AppServerOptions`:
210
- `allowed_origins: [/^http:\/\/localhost/]`, stub proxy pinned to
211
- `127.0.0.1`, `env_schema: z.object({})`, every rate limiter `null`,
212
- static daemon token state (no rotation, keeper already set),
213
- **`await_pending_effects: true`** (fire-and-forget effects complete
214
- before the response returns so tests can assert on side effects inline),
215
- 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`.
216
184
 
217
- A fresh Hono app is created on every call because middleware closures
218
- bind to the server's deps (db, keyring). Hono assembly is cheap
219
- (~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.
220
188
 
221
189
  ### `auth_apps.ts` — adversarial-auth app factories
222
190
 
@@ -224,38 +192,32 @@ Pre-built Hono apps at each auth level (public / authed / keeper / per-role)
224
192
  for attack-surface testing. No middleware stack — a single `/*` middleware
225
193
  injects `ACCOUNT_ID_KEY` + `REQUEST_CONTEXT_KEY` + `CREDENTIAL_TYPE_KEY`
226
194
  (default `'session'`) plus the `TEST_CONTEXT_PRESET_KEY` flag (so the
227
- dispatcher's authorization phase trusts the pre-baked context and skips
228
- its DB-backed actor resolution), then hands off to `apply_route_specs`
229
- with `fuz_auth_guard_resolver` + `create_fuz_authorization_handler`.
230
- Production middleware never sets `TEST_CONTEXT_PRESET_KEY`, so the escape
231
- hatch is test-only by construction.
232
-
233
- | Helper | Role |
234
- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
235
- | `create_test_request_context(role?)` | Minimal `RequestContext` one account, one actor, one role_grant for `role` (or none). |
236
- | `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. |
237
- | `AuthTestApps` | `{public, authed, keeper, by_role: Map<string, Hono>}`. |
238
- | `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. |
239
- | `select_auth_app(apps, auth)` | Map `RouteAuth` → matching Hono app. Throws for missing `role:*` entries. |
240
- | `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.
241
207
 
242
208
  ## Cross-impl schema parity
243
209
 
244
210
  ### `schema_introspect.ts` — `query_schema_snapshot`
245
211
 
246
- | Helper | Role |
247
- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
248
- | `query_schema_snapshot(db, options?)` | Introspect 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. |
249
- | `SchemaSnapshot` | Fully JSON-serializable shape — every collection is 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. |
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.
250
214
 
251
215
  ### `schema_parity.ts` — `assert_schema_snapshots_equal`
252
216
 
253
- | Helper | Role |
254
- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
255
- | `diff_schema_snapshots(a, b)` | Structured `Array<SchemaDiff>` between two snapshots empty array means parity holds. |
256
- | `format_schema_diffs(diffs, labels?)` | Human-readable multi-line rendering; labels name the impl on each side (e.g., `{a: 'deno', b: 'rust'}`). |
257
- | `assert_schema_snapshots_equal(a, b, labels?)` | Throw on drift with a fully-formatted diff message. |
258
- | `SchemaDiff` | Tagged-union for each 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`. |
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`.
259
221
 
260
222
  **Cross-impl gate pattern** — consumers running two backends against a
261
223
  shared schema (zzz's `--backend=both`, fuz_app's cross-backend suite)
@@ -271,50 +233,45 @@ const snapshot_rust = await query_schema_snapshot(db);
271
233
  assert_schema_snapshots_equal(snapshot_deno, snapshot_rust, {a: 'deno', b: 'rust'});
272
234
  ```
273
235
 
274
- Each impl's _own_ tests still gate its DDL correctness independently —
275
- this pair is purely the cross-impl drift check.
236
+ Each impl's _own_ tests still gate its DDL correctness independently — this
237
+ pair is purely the cross-impl drift check.
276
238
 
277
239
  ## Assertions, coverage, helpers
278
240
 
279
241
  ### `assertions.ts` — surface + error-schema assertions
280
242
 
281
- | Helper | Role |
282
- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
283
- | `resolve_fixture_path(filename, import_meta_url)` | Absolute path relative to the caller's module (use `import.meta.url`). |
284
- | `assert_surface_matches_snapshot(surface, path)` | Compares live `AppSurface` against a committed JSON snapshot; failure message instructs `gro gen`. |
285
- | `assert_surface_deterministic(build_surface)` | Build twice, `deepStrictEqual` the two results catches nondeterminism in surface generation. |
286
- | `assert_only_expected_public_routes(surface, list)` | Bidirectional: no unexpected public routes, no missing expected ones. Format: `['GET /health', 'POST /api/account/login']`. |
287
- | `assert_full_middleware_stack(surface, prefix, mws)` | Every route under `prefix` has exactly `mws` as its middleware chain. |
288
- | `get_route_error_schema(lookup, route, status)` | Read out of a pre-built merged-error-schema map. |
289
- | `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.
290
250
 
291
251
  ### `surface_invariants.ts` — structural + policy invariants
292
252
 
293
- Structural invariants (options-free, apply universally):
294
-
295
- | Assertion | Checks |
296
- | ----------------------------------------- | ------------------------------------------------------------------------------------------------ |
297
- | `assert_protected_routes_declare_401` | Every protected route has 401 in `error_schemas`. |
298
- | `assert_role_routes_declare_403` | Every role/keeper route has 403. |
299
- | `assert_input_routes_declare_400` | Every route with input has 400. |
300
- | `assert_params_routes_declare_400` | Every route with params has 400. |
301
- | `assert_query_routes_declare_400` | Every route with query has 400. |
302
- | `assert_descriptions_present` | Every route has a non-empty description. |
303
- | `assert_no_duplicate_routes` | No duplicate method+path pairs. |
304
- | `assert_middleware_errors_propagated` | Every middleware-declared error status appears on every applicable route. |
305
- | `assert_error_schemas_structurally_valid` | Every declared error schema has an `error` property at the top level (matches `ApiError`). |
306
- | `assert_error_code_status_consistency` | The same `z.literal()` error code never appears at two different HTTP statuses. |
307
- | `assert_404_schemas_use_specific_errors` | Routes with params declaring 404 must use `z.literal()` or `z.enum()`, not generic `z.string()`. |
308
-
309
- RPC / WS structural invariants (options-free, apply universally over
310
- `surface.rpc_endpoints` + `surface.ws_endpoints`):
311
-
312
- | Assertion | Checks |
313
- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
314
- | `assert_rpc_method_descriptions_present` | Every RPC method on every endpoint has a non-empty `description`. |
315
- | `assert_ws_method_descriptions_present` | Every WS method on every endpoint has a non-empty `description`. |
316
- | `assert_ws_endpoints_include_protocol_actions` | Every WS endpoint includes `heartbeat` + `cancel` (the `protocol_actions` spread from `actions/protocol.js`). |
317
- | `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.
318
275
 
319
276
  Per-endpoint duplicate method names and the auth-shape biconditional are
320
277
  already enforced at startup by `compile_action_registry` (see
@@ -323,36 +280,18 @@ contract-surface concerns a runtime registration check cannot reach.
323
280
 
324
281
  Policy invariants (configurable, sensible defaults):
325
282
 
326
- | Assertion | Checks |
327
- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
328
- | `assert_sensitive_routes_rate_limited` | Routes matching `sensitive_route_patterns` (default: `/login`, `/password`, `/bootstrap`, `/tokens/create`) declare rate limiting or a 429 schema. |
329
- | `assert_no_unexpected_public_mutations` | Public mutation routes must be in `public_mutation_allowlist`. |
330
- | `assert_mutation_routes_use_post` | Routes with input schemas must not be GET (bypasses browser GET idempotency assumptions). |
331
- | `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/']`).
332
287
 
333
288
  Tightness audit:
334
289
 
335
- - `audit_error_schema_tightness(surface) => Array<ErrorSchemaAuditEntry>` —
336
- classifies every route × status combination as `'literal' | 'enum' | 'generic'`.
337
- - `assert_error_schema_tightness(surface, options?)` fails routes below a
338
- threshold (`min_specificity`, default `'enum'`) with `allowlist` + `ignore_statuses` escape hatches.
339
- - `fuz_app_stock_route_tightness_allowlist` currently empty. Every
340
- fuz_app-shipped route (account login/password/bootstrap/signup, db
341
- health/tables/:name/tables/:name/rows/:id) has been tightened in place to
342
- `z.enum([...])` / `z.literal(...)` against every emit-site code. Kept as a
343
- forward-compatibility hook for future stock routes that need an interim
344
- exemption; paths assume the standard `/api/account` + `/api/db` prefixes.
345
- - `default_error_schema_tightness` — `{ignore_statuses: [401, 403, 429], allowlist: fuz_app_stock_route_tightness_allowlist}`.
346
- Applied by `describe_standard_attack_surface_tests` when
347
- `error_schema_tightness` is omitted; pass an override config or `null` to
348
- opt out.
349
- - **Merge semantics in `describe_standard_attack_surface_tests`**:
350
- consumer-supplied `allowlist` and `ignore_statuses` are concatenated
351
- underneath the defaults (stock entries first, consumer entries last),
352
- so consumer allowlists are additive rather than replacing. Scalar fields
353
- like `min_specificity` are overwritten by the consumer. Exported as
354
- `resolve_standard_error_schema_tightness(consumer_options)` for consumers
355
- 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.
356
295
 
357
296
  Aggregate runners (called by the standard attack-surface suite):
358
297
 
@@ -364,80 +303,52 @@ Aggregate runners (called by the standard attack-surface suite):
364
303
 
365
304
  `ErrorCoverageCollector` tracks which declared error paths get exercised.
366
305
  Observations live in a `Set<string>` keyed by `"METHOD /spec-path:STATUS"` or
367
- `"METHOD /spec-path:STATUS:CODE"` — the two shapes coexist and a
368
- status-only observation satisfies the "any-code" coverage rule for all
369
- 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.
370
309
 
371
310
  Methods:
372
311
 
373
- - `record(specs, method, path, status, code?)` — resolves concrete paths
374
- back to spec templates (e.g. `/api/accounts/abc` `/api/accounts/:id`).
375
- - `assert_and_record(specs, method, path, response, code?)` wraps
376
- `assert_response_matches_spec` and auto-extracts `body.error` from the
377
- JSON body via `response.clone()`. Pass an explicit `code` when the
378
- body was already consumed.
379
- - `uncovered(specs, options?)` — per-status rows for generic schemas,
380
- 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.
381
315
 
382
316
  Support functions:
383
317
 
384
- - `extract_declared_error_codes(schema)` — reads `schema.shape.error`;
385
- returns the literal value(s) for `z.literal` / `z.enum`, `null`
386
- otherwise.
387
- - `assert_error_coverage(collector, specs, options?)` — logs
388
- `[error coverage] covered/total (N.M%)` with uncovered list; fails
389
- when `min_coverage > 0` and the ratio falls below.
390
- - `DEFAULT_INTEGRATION_ERROR_COVERAGE = 0.2` — conservative baseline
391
- for the standard integration/admin suites; consumers tighten as
392
- 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.
393
321
 
394
322
  ### `schema_generators.ts` — valid-value generation
395
323
 
396
324
  Walks Zod schemas to generate valid values for adversarial/round-trip tests.
397
325
 
398
- - `detect_format(field_schema)` — reads `format` / `pattern` from the
399
- JSON Schema representation.
400
- - `generate_valid_value(field, field_schema)` — base-type switch
401
- producing a valid sample (UUIDs nil UUID, strings `'xxxxxxxxxx'`,
402
- numbers → `1`, objects → recurse, enums → first entry, etc.).
403
- For branded-string refinements, walks a fallback chain synthesized
404
- from the `pattern` string the JSON Schema representation exposes:
405
- fixed-length hex (`^[0-9a-f]{N}$` — blake3 / sha256 / md5 digests;
406
- `0`.repeat(N)), prefix-lengthed slug (`^<prefix>_[A-Za-z0-9_-]{N}$`
407
- — `ApiTokenId`-style ids; `<prefix>_` + `x`.repeat(N)), absolute
408
- path prefix, URL prefix. First candidate that `safeParse` accepts
409
- is used.
410
- - `resolve_valid_path(path, params_schema?)` — swaps `:param` for
411
- valid-format values (nil UUID for UUID params, `test_param` otherwise).
412
- - `generate_valid_body(input_schema) => Record<string, unknown> | undefined` —
413
- builds a body that satisfies the input schema. Throws with Zod
414
- `issues` if the generated body fails validation — surfaces broken
415
- generation logic with a descriptive error rather than a confusing 400
416
- 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.
417
330
 
418
331
  ### `integration_helpers.ts` — route lookup + body checks
419
332
 
420
- | Helper | Role |
421
- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
422
- | `find_route_spec(specs, method, path)` | Exact match then parameterized match (`:foo` matches any segment). |
423
- | `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). |
424
- | `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. |
425
- | `create_expired_test_cookie(keyring, session_options)` | Validly signed cookie with `expires_at` in 1970. |
426
- | `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`). |
427
- | `assert_no_error_info_leakage(body, context)` | Rejects field-name patterns (`stack`, `trace`, `sql`, …) + value patterns (`node_modules`, stack-like `at …`, `.ts:NN`). |
428
- | `assert_rate_limit_retry_after_header(response, body)` | `Retry-After` numeric header equals `Math.ceil(body.retry_after)`. |
429
- | `sensitive_field_blocklist` | `['password_hash', 'token_hash']` never in any response body. |
430
- | `admin_only_field_blocklist` | `['updated_by', 'created_by']` — never in non-admin response bodies. |
431
- | `collect_json_keys_recursive(value)` | Deep walk; returns `Set<string>` of every key at every nesting depth. |
432
- | `assert_no_sensitive_fields_in_json(body, blocklist, context)` | Rejects any key in the blocklist at any depth. |
433
- | `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.
434
345
 
435
346
  ## Attack surface suites
436
347
 
437
348
  ### `attack_surface.ts` — `describe_standard_attack_surface_tests`
438
349
 
439
- Single-call bundle of 5 top-level groups (10 named tests + every
440
- adversarial case per route):
350
+ Single-call bundle of 5 top-level groups (10 named tests + every adversarial
351
+ case per route):
441
352
 
442
353
  1. **attack surface snapshot** — `matches committed snapshot`, `is deterministic`.
443
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`).
@@ -447,41 +358,35 @@ adversarial case per route):
447
358
 
448
359
  Options: `{build: () => AppSurfaceSpec, snapshot_path, expected_public_routes, expected_api_middleware, roles, api_path_prefix?, security_policy?, error_schema_tightness?}`.
449
360
 
450
- Also exported: `describe_adversarial_auth(options)` (groups 3 on its own)
451
- 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
452
363
  `Map<string, RouteErrorSchemas>` for per-response validation).
453
364
 
454
365
  ### `adversarial_input.ts` — schema-walk payload generation
455
366
 
456
367
  `describe_adversarial_input({build, roles})` — fires input body / params /
457
- query validation failures at every route with correct-auth credentials
458
- so validation middleware is actually exercised (not short-circuited by
459
- 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` /
460
371
  `_INVALID_JSON_BODY` / `_INVALID_ROUTE_PARAMS` / `_INVALID_QUERY_PARAMS`.
461
372
 
462
373
  Exported generators:
463
374
 
464
- - `generate_input_test_cases(input_schema)` — whole-body structural
465
- (non-object, extra key when `strictObject`), missing required fields,
466
- one wrong-type per field, null for required non-nullable, one format
467
- violation per constrained field, numeric/array/string boundary cases
468
- via JSON Schema introspection.
469
- - `generate_params_test_cases(params_schema)` — format violations only
470
- (unconstrained string params accept anything).
471
- - `generate_query_test_cases(query_schema)` — missing required +
472
- 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.
473
378
 
474
- GET-with-input routes hit the RPC `?params=` query convention; invalid-
475
- JSON arrays there collapse to `ERROR_INVALID_REQUEST_BODY` (schema
476
- 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`.
477
382
 
478
383
  ### `adversarial_404.ts` — 404 schema conformance
479
384
 
480
- `describe_adversarial_404({build, roles})` — for every route with
481
- `params` + 404 in `error_schemas` + an extractable error code
482
- (`z.literal` or first `z.enum`), replaces the handler with a stub
483
- returning `{error: <code>}`, fires with nil-UUID params, asserts 404 +
484
- 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.
485
390
 
486
391
  ### `adversarial_headers.ts` — header injection suite
487
392
 
@@ -504,56 +409,51 @@ validation. Extra cases append to the standard list.
504
409
 
505
410
  Module-level `vi.mock()` for the four query modules bearer auth touches:
506
411
  `api_token_queries`, `account_queries`, `role_grant_queries`. Because
507
- `vi.mock()` is hoisted, these run before any imports resolve — so any
508
- test file that imports from `middleware.ts` gets these mocks globally.
509
- Pair with `vi.restoreAllMocks()` in `afterEach` when mixing into
510
- `.db.test.ts` files (see DB test caveat below).
511
-
512
- | Helper | Role |
513
- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
514
- | `BearerAuthTestOptions`, `BearerAuthTestCase` | Test-case table shape for the bearer auth runner. |
515
- | `create_bearer_auth_mocks(tc)` | Configures the module-level mocks per test case; returns spy references. |
516
- | `TEST_CLIENT_IP = '127.0.0.1'` | IP set by the proxy stub in `create_bearer_auth_test_app`. |
517
- | `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`. |
518
- | `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. |
519
- | `TEST_MIDDLEWARE_PATH = '/api/test'` | Path used by the echo route in the stack factory. |
520
- | `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}`.
521
423
 
522
424
  The echo route under `create_bearer_auth_test_app` deliberately surfaces
523
425
  every middleware-written context variable (`ACCOUNT_ID_KEY`,
524
- `CREDENTIAL_TYPE_KEY`, `AUTH_API_TOKEN_ID_KEY`) — bearer middleware
525
- writes account-grain identity only; the dispatcher's authorization phase
526
- owns `REQUEST_CONTEXT_KEY`. The `request_context_set` flag covers the
527
- test-only `pre_context` injection path. When public auth surface gains a
528
- new context variable, header, or field, update this echo alongside the
529
- 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.
530
432
 
531
433
  ## Round-trip suites
532
434
 
533
435
  ### `round_trip.ts` — `describe_round_trip_validation`
534
436
 
535
- For every route spec, fires a valid request with matching auth and
536
- validates the response against declared schemas. DB-backed via
537
- `create_test_app`. Per-route test (`test.each`) — one line per route
538
- 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.
539
440
 
540
441
  Options: `{setup_test, surface_source, capabilities, skip_routes?, input_overrides?}`.
541
442
  `input_overrides` is a `Map<"METHOD /path", body>` — override generated
542
- bodies for routes whose input schema can't round-trip cleanly (e.g.
543
- 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).
544
445
 
545
446
  SSE routes are skipped by Content-Type sniff; `describe_sse_route_tests`
546
447
  picks them up separately.
547
448
 
548
449
  ### `rpc_round_trip.ts` — `describe_rpc_round_trip_tests`
549
450
 
550
- DB-backed round-trip for RPC: one POST test for all methods, one GET
551
- test for `side_effects: false` methods. Successful responses validate
552
- against `action.spec.output`; error responses validate as well-formed
553
- JSON-RPC error envelopes. Options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints, skip_methods?, input_overrides?}`.
554
- The admin RPC auth test picks a session-based identity (`authed` /
555
- `admin` / bootstrapped keeper) based on `method.auth`; keeper uses the
556
- 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.
557
457
 
558
458
  ### `sse_round_trip.ts` — `describe_sse_route_tests`
559
459
 
@@ -564,46 +464,45 @@ validate the next `data:` frame as `{method, params}` against declared
564
464
  and assert the stream closes within 2s.
565
465
 
566
466
  `SseRouteTestSpec` per route: `{path, trigger, event_specs?, assert_closes_on_revoke?}`.
567
- Pass `on_audit_event` on the suite options to wire a close-on-revoke
568
- guard (e.g. via `create_sse_auth_guard`) for consumer SSE registries —
569
- 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.
570
470
 
571
- Frame reader (`create_sse_frame_reader`) is internal but handles
572
- `\n\n` framing, a 2s per-read timeout (prevents vitest hangs), and
573
- `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.
574
476
 
575
477
  ### `ws_round_trip.ts` — WebSocket harness (non-HTTP)
576
478
 
577
479
  In-process test driver for `register_action_ws`. Consumers pass specs +
578
- handlers, receive `{transport, connect()}` back. The full dispatch path
579
- is exercised (per-action auth, input validation, `ctx.notify`,
580
- broadcast via `BackendWebsocketTransport`, close-on-revoke), but Hono's
581
- wire upgrade is skipped (the Node test runtime has no
582
- `@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).
583
484
 
584
485
  Three layers:
585
486
 
586
- 1. **Primitives** — `create_fake_ws()`, `create_fake_hono_context(opts)`,
587
- `create_stub_upgrade()`, `MinimalActionEnvironment`,
588
- `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)`.
589
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).
590
- 3. **Round-trip helpers** — `is_notification(method)`,
591
- `is_notification_with<P>(method, match)` (type-guard combinator —
592
- narrows `wait_for` return type), `is_response_for(id)`.
593
- `JsonrpcNotificationFrame<P>` / `JsonrpcSuccessResponseFrame<R>` /
594
- `JsonrpcErrorResponseFrame<D>` typed wire-frame shapes distinct
595
- from the runtime Zod schemas in `http/jsonrpc.ts` (generic over
596
- `params` / `result` / `data` so tests narrow without casts).
597
- `build_broadcast_api<TApi>({harness, specs})` wires a typed
598
- broadcast API against the harness transport.
599
-
600
- `MockWsClient`: `{send, request<R>, close, messages, wait_for}`.
601
- `request` throws with code + message + data on error frames (so
602
- asserting `result.foo` on a failed request surfaces the real cause,
603
- not a `Cannot read property 'foo' of undefined`). `wait_for(predicate,
604
- timeout_ms?)` checks already-received messages first, then waits for
605
- new arrivals (default 1000ms); drops the waiter on timeout so the
606
- `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.
607
506
 
608
507
  `keeper_identity()` — convenience for `{credential_type: 'daemon_token', roles: [ROLE_KEEPER]}`.
609
508
 
@@ -633,15 +532,21 @@ Options: `{setup_test, surface_source, capabilities, sensitive_fields?, admin_on
633
532
 
634
533
  Three test groups:
635
534
 
636
- 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.
637
536
  2. Per-account rate limiting on login — same username exhausts the bucket; a different username is not blocked.
638
537
  3. Bearer auth IP rate limiting — invalid bearer tokens exhaust the IP bucket via the `account_verify` RPC method.
639
538
 
640
- Each group asserts its required route exists with a descriptive
641
- message. Creates a tight rate limiter (default `max_attempts: 2`,
642
- `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`.
643
542
 
644
- Options: `{session_options, create_route_specs, rpc_endpoints, app_options?, db_factories?, max_attempts?}`. Reads inputs directly from the options bag instead of going through the `setup_test` fixture protocol — the per-test rate-limiter overrides need a fresh `TestApp` per test that the single-fixture model can't carry. Consumers still pass `default_in_process_suite_options(...)` for shape uniformity; the extra `{setup_test, surface_source, capabilities}` fields on the spread are ignored by the suite.
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.
645
550
 
646
551
  ## Integration suites
647
552
 
@@ -664,14 +569,14 @@ these thematic areas:
664
569
  11. Signup invite edge cases + expired credential rejection + error-coverage breadth
665
570
 
666
571
  An `ErrorCoverageCollector` runs across groups; `afterAll` filters to
667
- auth-related routes (login/logout/verify/sessions/tokens/password/
668
- signup) and asserts `DEFAULT_INTEGRATION_ERROR_COVERAGE` (20%). Bootstrap
669
- is excluded because no describe block in this suite drives it — its
670
- declared codes would always be uncovered. Consumer-specific routes
671
- aren't exercised here either — they don't count against the baseline.
672
- Override the threshold with the `error_coverage_min?: number` option
673
- (set to `0` to skip the assertion entirely useful for minimal route
674
- sets whose declared error codes outpace the suite's denial-path drivers).
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).
675
580
 
676
581
  Options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints, error_coverage_min?}`.
677
582
 
@@ -687,11 +592,11 @@ isolation, error coverage, response schema validation.
687
592
 
688
593
  The shared `role_grant_offer_and_accept` helper (`role_grant_helpers.ts`)
689
594
  composes both RPCs end-to-end and takes
690
- `{grantor: TestApp | TestAccount, recipient: TestAccount}` — closing
691
- the headers/account loop on a single object per party rules out caller-side
692
- header/account mismatch. Direct-grant fixtures (where the test focuses on
693
- revoke or isolation, not the consent path) go through
694
- `create_test_role_grant_direct` from `db_entities.ts`.
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`.
695
600
 
696
601
  Required options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints: RpcEndpointsSuiteOption, roles: RoleSchemaResult, admin_prefix?}`.
697
602
 
@@ -704,32 +609,30 @@ raw to the top-level `rpc_endpoints` slot on `CreateTestAppOptions` so
704
609
  action handlers can close over
705
610
  `ctx.deps` / `ctx.app_settings` (e.g. `create_standard_rpc_actions(ctx.deps,
706
611
  {app_settings: ctx.app_settings})`). Factory must return the same endpoint
707
- `path` regardless of ctx — `resolve_rpc_endpoints_for_setup` invokes it
708
- once with a stub ctx for path lookup and `create_app_server` invokes it
709
- 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.
710
615
 
711
616
  **Hard-fails via `require_rpc_endpoint_path`** at setup time when
712
617
  `rpc_endpoints` is empty — admin role_grant grant/revoke plus session/token
713
618
  revoke-all plus audit-log list/history are RPC-only. A confusing test
714
619
  failure mid-suite is worse than a clear setup error.
715
620
 
716
- The suite also exercises `account_token_create` (and
717
- `account_token_revoke`) for the cross-admin isolation + audit-trail
718
- scenarios. Wire the account actions alongside admin / role-grant-offer —
719
- the easiest path is `create_standard_rpc_actions`, which bundles all
720
- three. Consumers that only wire admin will hit `method not found:
721
- account_token_create` on first run.
722
-
723
- Error-coverage scope is narrowed to the REST suffixes still on the
724
- admin surface (`/audit/stream`); the RPC surface is covered by
725
- `describe_rpc_round_trip_tests`. The scoped REST surface is 0–1
726
- routes when the scoped count is ≤1, the `afterAll` hook logs
727
- `[error coverage] skipped admin REST coverage assertion — …` and
728
- does not fail. The 20% `DEFAULT_INTEGRATION_ERROR_COVERAGE` baseline
729
- is a REST-era threshold; the RPC surface has its own coverage via
730
- `describe_rpc_round_trip_tests`. TODO: move this error-coverage
731
- collector to the RPC round-trip suite entirely and delete this skip
732
- 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.
733
636
 
734
637
  ### `audit_completeness.ts` — `describe_audit_completeness_tests`
735
638
 
@@ -737,8 +640,8 @@ Verifies every auth mutation produces the expected `audit_log` row.
737
640
  Mutations fire over the real middleware stack; reads go back through the
738
641
  `audit_log_list` RPC (the same path the admin UI consumes) — intentional
739
642
  end-to-end coverage of emit → persist → query → wire response. For
740
- unit-level "did the handler emit?" assertions without the persistence
741
- path, use `create_recording_audit_emitter` from `audit_drift_guard.ts`.
643
+ unit-level "did the handler emit?" assertions without the persistence path,
644
+ use `create_recording_audit_emitter` from `audit_drift_guard.ts`.
742
645
 
743
646
  Same `rpc_endpoints` hard-fail as the admin suite — the mutation-audit
744
647
  tests drive role_grant flow, session/token revoke-all, and invite
@@ -749,9 +652,9 @@ create/delete through `role_grant_offer_create_action_spec` /
749
652
  `invite_create_action_spec` / `invite_delete_action_spec`.
750
653
 
751
654
  **Observer-account pattern.** Each audit-touching test mints a dedicated
752
- admin account (`create_admin_observer`) whose sole job is reading the
753
- audit log via RPC. Decoupling the observer from the subject keeps the
754
- helper shape uniform across every test — even mutations that revoke the
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
755
658
  bootstrapped admin's credentials (logout, session_revoke, password_change).
756
659
 
757
660
  Bootstrap audit logging is excluded because `create_test_app` doesn't
@@ -763,34 +666,32 @@ provide the filesystem token state; covered separately in
763
666
  Bundles every DB-backed suite carrying the standard option shape, each
764
667
  gated on its relevant config — silent-skip when the gate isn't met:
765
668
 
766
- | Suite | Gate |
767
- | -------------------- | --------------------------------------------------------------------------------------------------------- |
768
- | `integration` | always |
769
- | `admin` | `roles` provided |
770
- | `audit_completeness` | `roles` provided (proxy for consumer admin wiring; `rpc_endpoints` is bundle-required) |
771
- | `bootstrap_success` | `bootstrap.mode === 'live'` |
772
- | `round_trip` | always |
773
- | `rpc_round_trip` | `rpc_endpoints` provided |
774
- | `data_exposure` | always |
775
- | `rate_limiting` | always (owns its own per-test setup, bypasses the fixture protocol — needs `create_route_specs` directly) |
776
-
777
- Realization that lifted the bundle from 2 suites to 8: fold-in cost
778
- between suites is zero because each `describe_*` block owns its own
779
- setup via the `{setup_test, surface_source, capabilities}` protocol, so
780
- suites whose tests need opposite-shaped default DB state (e.g. the
781
- bootstrap-success suite needs an empty DB while the integration suite
782
- needs the pre-bootstrapped keeper) coexist in one bundle without cost.
783
- Each test invokes the right per-test fixture. Consumers wiring the
784
- standard surface call once instead of seven times; forgetting a suite
785
- no longer silently loses coverage.
786
-
787
- `StandardTestOptions` requires `create_route_specs` (for rate_limiting)
788
- and `rpc_endpoints` (for admin/audit_completeness/rpc_round_trip); the
789
- admin suite's requirement is enforced at the type level so a missing
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
790
691
  `rpc_endpoints` is a compile error rather than a runtime throw. Optional
791
- `bootstrap` (top-level, same precedent as `rpc_endpoints`) feeds both
792
- the disabled/surface_only/live wire-shape gating and the
793
- bootstrap-success suite gate.
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.
794
695
 
795
696
  Attack surface suites stay separate — their option shape is
796
697
  `{build, snapshot_path, expected_public_routes, ...}` rather than the
@@ -798,34 +699,35 @@ shared `{setup_test, surface_source, capabilities}`. A peer
798
699
  `describe_standard_surface_tests` bundler lives for that side if/when
799
700
  needed.
800
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.
710
+
801
711
  ## RPC helpers
802
712
 
803
713
  ### `rpc_helpers.ts` — envelope construction + response assertions
804
714
 
805
- Shared by `rpc_attack_surface.ts`, `rpc_round_trip.ts`, the admin and
806
- audit integration suites, and consumer tests that hit RPC methods
807
- 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.
808
717
 
809
718
  Request builders:
810
719
 
811
- - `create_rpc_post_init(method, params?, id?)` — `RequestInit` with
812
- JSON-RPC envelope body. `params === undefined || params === null` →
813
- envelope has no `params` field (JSON-RPC doesn't accept
814
- `"params": null`).
815
- - `create_rpc_get_url(endpoint_path, method, params?, id?)` — GET URL
816
- 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>`.
817
722
 
818
723
  Response assertions:
819
724
 
820
- - `assert_jsonrpc_error_response(body, expected_code?)` — validates
821
- `JsonrpcErrorResponse`; optional code check.
822
- - `assert_jsonrpc_success_response(body, output_schema?)` — validates
823
- `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)`.
824
727
 
825
728
  One-shot transport:
826
729
 
827
- - `RpcTestTransport = (url, init) => Promise<Response>` — duck type
828
- `Hono.request` already satisfies.
730
+ - `RpcTestTransport = (url, init) => Promise<Response>` — duck type `Hono.request` already satisfies.
829
731
  - `http_transport(app)` — adapter for anything with a `request()` method.
830
732
  - `RpcCallResult` — discriminated `{ok: true, status, result}` / `{ok: false, status, error: {code, message, data?}}`.
831
733
  - `RpcCallArgs` — `{app, path, method, params?, headers?, id?, verb?}`. `verb` defaults to `'POST'`; use `'GET'` for `side_effects: false` methods.
@@ -855,15 +757,15 @@ Registry lookups:
855
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`.
856
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`.
857
759
 
858
- Skips silently when `surface.rpc_endpoints` is empty. Uses stub
859
- deps — no DB needed.
760
+ Skips silently when `surface.rpc_endpoints` is empty. Uses stub deps — no
761
+ DB needed.
860
762
 
861
763
  Options: `{build: () => AppSurfaceSpec, roles: Array<string>}`.
862
764
 
863
- **Opt-in bundles need their own per-bundle suite file.** Action bundles
864
- not folded into `create_standard_rpc_actions` (today `self_service_role_actions`,
865
- `actor_lookup_actions`, and `actor_search_actions`) get zero adversarial
866
- / 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` +
867
769
  `describe_rpc_round_trip_tests` unless the consumer ships a
868
770
  `<module>.rpc_suites.db.test.ts` mounting the opt-in factory on the RPC
869
771
  endpoint and calling both suites. See ../../test/CLAUDE.md §Composable
@@ -874,67 +776,313 @@ Test Suites for the obligation note; existing
874
776
 
875
777
  Shared conventions (`.db.test.ts` suffix, `isolate: false` semantics,
876
778
  `assert` from vitest, `assert_rejects`, `vi.mock` avoidance under
877
- `isolate: false`) live in Skill(fuz-stack) testing-patterns.
878
- fuz_app-specific points:
879
-
880
- - **`await_pending_effects: true`** is set by `create_test_app`.
881
- Fire-and-forget effects (audit logs, session touches, WS fan-out via
882
- `emit_after_commit`) resolve before the response returns, so tests
883
- can assert on side effects inline without manual flushing.
884
- - **Deep-path imports only.** Import from the canonical module
885
- (`testing/db.js`, `testing/rpc_helpers.js`, etc.); fuz_app's `dist/` ships no
886
- barrel.
887
- - **DI via small `*Deps` interfaces.** Stub factories accept the same
888
- narrow `*Deps` contracts production code uses — never
889
- `Pick<GodType, ...>`. New helpers needing env/fs/logger take
890
- `EnvDeps` / `FsReadDeps` / `Logger` from `runtime/deps.ts` or
891
- `@fuzdev/fuz_util/log.js`.
892
- - **Keep the shared echo routes in sync with public surface.** When
893
- middleware or public API gains a new context variable, header, or
894
- field, update the echo in `middleware.ts`
895
- (`create_bearer_auth_test_app`, `create_test_middleware_stack_app`)
896
- alongside the assertions in `src/test/auth/*.test.ts`. Drift surfaces
897
- 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.
898
786
 
899
787
  ## Cross-backend integration layer
900
788
 
901
789
  The standard test suites take a unified
902
- `{setup_test, surface_source, capabilities}` shape so the same suite
903
- bodies run against an in-process Hono harness today and against a
904
- spawned non-TS backend (Rust `zzz_server`, `fuz_webui`) over real HTTP
905
- once the cross-process transport lands. In-process is the fast feedback
906
- path; cross-process is the source of truth for wire-shape conformance.
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.
907
797
 
908
- The shape:
798
+ ### Fixture protocol + capabilities
909
799
 
910
800
  - `testing/cross_backend/setup.ts` — `SetupTest` / `TestFixture` /
911
801
  `TestAccountFixture` / `CreateTestAccountOptions` types,
912
802
  `default_in_process_setup(options)` (wraps `create_test_app`), and
913
- `default_in_process_suite_options(options)` (emits the full Tier 1
914
- suite options bag: the `{setup_test, surface_source, capabilities}`
915
- triple plus `session_options` / `create_route_specs` / `rpc_endpoints`
916
- pass-through; call sites pass the output directly or spread it
917
- alongside suite-specific extras like `roles`, `skip_routes`,
918
- `input_overrides`, `db_factories`).
919
- - `testing/cross_backend/capabilities.ts` `BackendCapabilities`
920
- vocabulary (`bearer_auth` / `trusted_proxy` / `login_rate_limit` /
921
- `ws` / `sse` / `in_process_only`), `test_if(cond, name, fn)` for
922
- capability-gated cases, and `in_process_capabilities` preset.
923
- - `testing/transports/surface_source.ts` `SurfaceSource` union
924
- (`inline` for source-of-truth route closures; `snapshot` for
925
- committed JSON read cross-process).
926
-
927
- Three suites stay in-process by design `ws_round_trip` (no HTTP
928
- transport at all), `sse_round_trip` (streaming + in-process audit
929
- hook for close-on-revoke), `audit_completeness` (FK-structural
930
- introspection beyond the `audit_log_list` RPC reads). Cross-process
931
- variants land alongside the spawned-backend work for the first two;
932
- audit_completeness's introspection is structurally in-process.
933
-
934
- The auth-cost handling for cross-process testing is consumer-side:
935
- each consumer ships a separate test binary wiring a fast-params
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
936
1040
  `TestingArgon2idHasher` from a sibling Rust testing crate. Cross-process
937
- `bootstrap` + `create_account` are then plain RPC calls against the
938
- test binary — no DB-direct surgery in fuz_app's testing library, no
939
- runtime knobs in production code, no shared cookie key with the
940
- backend.
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.