@fuzdev/fuz_app 0.64.0 → 0.65.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 (111) hide show
  1. package/dist/actions/CLAUDE.md +513 -928
  2. package/dist/actions/broadcast_api.d.ts +1 -1
  3. package/dist/actions/broadcast_api.js +1 -1
  4. package/dist/actions/cancel.d.ts +2 -2
  5. package/dist/actions/cancel.js +3 -3
  6. package/dist/actions/connection_closer.d.ts +1 -4
  7. package/dist/actions/connection_closer.d.ts.map +1 -1
  8. package/dist/actions/connection_closer.js +1 -4
  9. package/dist/actions/register_action_ws.d.ts +2 -2
  10. package/dist/actions/register_ws_endpoint.d.ts +1 -1
  11. package/dist/actions/transports_ws_auth_guard.d.ts +1 -2
  12. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  13. package/dist/actions/transports_ws_auth_guard.js +1 -2
  14. package/dist/auth/CLAUDE.md +591 -1871
  15. package/dist/auth/account_schema.d.ts +1 -1
  16. package/dist/auth/account_schema.d.ts.map +1 -1
  17. package/dist/auth/api_token_queries.js +1 -1
  18. package/dist/auth/audit_log_ddl.d.ts +1 -1
  19. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  20. package/dist/auth/audit_log_ddl.js +1 -1
  21. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  22. package/dist/auth/bootstrap_account.js +1 -5
  23. package/dist/auth/bootstrap_routes.d.ts +7 -1
  24. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  25. package/dist/auth/bootstrap_routes.js +15 -11
  26. package/dist/auth/keyring.d.ts +6 -6
  27. package/dist/auth/keyring.js +8 -8
  28. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  29. package/dist/auth/role_grant_offer_actions.js +4 -2
  30. package/dist/db/create_db.d.ts.map +1 -1
  31. package/dist/db/create_db.js +13 -0
  32. package/dist/dev/setup.d.ts +2 -2
  33. package/dist/dev/setup.js +3 -3
  34. package/dist/http/CLAUDE.md +224 -498
  35. package/dist/http/error_schemas.d.ts +0 -4
  36. package/dist/http/error_schemas.d.ts.map +1 -1
  37. package/dist/http/error_schemas.js +0 -4
  38. package/dist/http/ip_canonical.d.ts +5 -4
  39. package/dist/http/ip_canonical.d.ts.map +1 -1
  40. package/dist/http/ip_canonical.js +8 -4
  41. package/dist/http/origin.d.ts +1 -1
  42. package/dist/http/origin.js +1 -1
  43. package/dist/runtime/mock.js +1 -1
  44. package/dist/server/app_server.d.ts +41 -10
  45. package/dist/server/app_server.d.ts.map +1 -1
  46. package/dist/server/app_server.js +10 -4
  47. package/dist/server/env.d.ts +7 -7
  48. package/dist/server/env.d.ts.map +1 -1
  49. package/dist/server/env.js +14 -14
  50. package/dist/server/static.d.ts +4 -4
  51. package/dist/server/static.js +7 -7
  52. package/dist/testing/CLAUDE.md +220 -46
  53. package/dist/testing/admin_integration.d.ts +18 -23
  54. package/dist/testing/admin_integration.d.ts.map +1 -1
  55. package/dist/testing/admin_integration.js +159 -201
  56. package/dist/testing/app_server.d.ts +125 -38
  57. package/dist/testing/app_server.d.ts.map +1 -1
  58. package/dist/testing/app_server.js +140 -42
  59. package/dist/testing/audit_completeness.d.ts +23 -22
  60. package/dist/testing/audit_completeness.d.ts.map +1 -1
  61. package/dist/testing/audit_completeness.js +199 -156
  62. package/dist/testing/bootstrap_success.d.ts +28 -0
  63. package/dist/testing/bootstrap_success.d.ts.map +1 -0
  64. package/dist/testing/bootstrap_success.js +144 -0
  65. package/dist/testing/cross_backend/capabilities.d.ts +64 -0
  66. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
  67. package/dist/testing/cross_backend/capabilities.js +47 -0
  68. package/dist/testing/cross_backend/setup.d.ts +215 -0
  69. package/dist/testing/cross_backend/setup.d.ts.map +1 -0
  70. package/dist/testing/cross_backend/setup.js +101 -0
  71. package/dist/testing/data_exposure.d.ts +14 -15
  72. package/dist/testing/data_exposure.d.ts.map +1 -1
  73. package/dist/testing/data_exposure.js +127 -146
  74. package/dist/testing/db_entities.d.ts +11 -1
  75. package/dist/testing/db_entities.d.ts.map +1 -1
  76. package/dist/testing/db_entities.js +13 -1
  77. package/dist/testing/integration.d.ts +35 -21
  78. package/dist/testing/integration.d.ts.map +1 -1
  79. package/dist/testing/integration.js +231 -291
  80. package/dist/testing/integration_helpers.d.ts +16 -6
  81. package/dist/testing/integration_helpers.d.ts.map +1 -1
  82. package/dist/testing/integration_helpers.js +7 -7
  83. package/dist/testing/mock_fs.d.ts.map +1 -1
  84. package/dist/testing/mock_fs.js +0 -2
  85. package/dist/testing/rate_limiting.d.ts.map +1 -1
  86. package/dist/testing/rate_limiting.js +9 -0
  87. package/dist/testing/role_grant_helpers.d.ts +31 -0
  88. package/dist/testing/role_grant_helpers.d.ts.map +1 -0
  89. package/dist/testing/role_grant_helpers.js +46 -0
  90. package/dist/testing/round_trip.d.ts +21 -16
  91. package/dist/testing/round_trip.d.ts.map +1 -1
  92. package/dist/testing/round_trip.js +65 -86
  93. package/dist/testing/rpc_round_trip.d.ts +24 -21
  94. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  95. package/dist/testing/rpc_round_trip.js +91 -104
  96. package/dist/testing/schema_introspect.d.ts +106 -0
  97. package/dist/testing/schema_introspect.d.ts.map +1 -0
  98. package/dist/testing/schema_introspect.js +123 -0
  99. package/dist/testing/schema_parity.d.ts +144 -0
  100. package/dist/testing/schema_parity.d.ts.map +1 -0
  101. package/dist/testing/schema_parity.js +233 -0
  102. package/dist/testing/standard.d.ts +57 -25
  103. package/dist/testing/standard.d.ts.map +1 -1
  104. package/dist/testing/standard.js +62 -5
  105. package/dist/testing/stubs.d.ts +11 -3
  106. package/dist/testing/stubs.d.ts.map +1 -1
  107. package/dist/testing/stubs.js +24 -21
  108. package/dist/testing/transports/surface_source.d.ts +51 -0
  109. package/dist/testing/transports/surface_source.d.ts.map +1 -0
  110. package/dist/testing/transports/surface_source.js +19 -0
  111. package/package.json +4 -4
@@ -22,21 +22,21 @@ Enforced by grep, not a linter; make this the first line in new modules.
22
22
 
23
23
  ### `stubs.ts` — `AppDeps` + `AppServerContext` stubs
24
24
 
25
- | Helper | Role |
26
- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
27
- | `create_throwing_stub<T>(label)` | Proxy whose every property access throws `Throwing stub 'label' — unexpected access to 'prop'`; JS-internal probes return `undefined`; `toJSON` returns `"[throwing_stub:label]"` so accidental serialization is visible rather than `{}`. |
28
- | `create_noop_stub<T>(label, overrides?)` | Proxy whose every method returns `async () => undefined`; `overrides` lets callers pin specific props. |
29
- | `stub` | Pre-built throwing stub labelled `'stub'`. |
30
- | `create_stub_db()` | Returns a real `Db` whose `client.query` yields `{rows: []}` and whose `transaction(fn)` synchronously calls `fn(inner_stub_db)`. Safe for `apply_route_specs`'s declarative transaction wrapper. |
31
- | `stub_handler()` | Returns a fresh `Response('stub')`. |
32
- | `stub_mw` | Pass-through middleware handler (`async (_c, next) => next()`). |
33
- | `stub_app_deps` | Frozen `AppDeps` — every capability is a throwing stub, `audit` is a no-op `AuditEmitter` from `create_test_audit_emitter`. |
34
- | `create_stub_app_deps()` | Factory returning fresh `AppDeps` with no-op FS/keyring/password, a `create_noop_stub` DB, silent `Logger`, no-op `audit`. |
35
- | `create_test_audit_emitter()` | No-op `AuditEmitter` for tests that don't assert on audit fan-out. `emit` / `emit_role_grant_target` are no-ops; `emit_pool` resolves immediately; `notify` is a no-op; `on_event_chain` is empty. |
36
- | `create_stub_audit_sse()` | No-op `AuditLogSse` for surface-test wiring without booting real SSE. `subscribe` returns a no-op cleanup; `on_audit_event` is a no-op; the `registry` is a fresh `SubscriberRegistry` (live `.size` / `.close_*` for tests touching registry state, isolated per call). For real SSE plumbing, build via `create_audit_log_sse` against `create_test_app`. |
37
- | `create_stub_api_middleware({include_daemon_token?})` | Stub `MiddlewareSpec[]` matching `create_auth_middleware_specs`'s output (origin/session/request_context/bearer_auth, optional daemon_token) for surface generation without booting real auth. See `auth/CLAUDE.md` §Middleware for the real stack. |
38
- | `create_stub_app_server_context(session_options)` | Stub `AppServerContext` — rate limiters null, `bootstrap_status.available: false`, `app_settings.open_signup: false`. |
39
- | `create_test_app_surface_spec(options)` | Builds an `AppSurfaceSpec` that mirrors `create_app_server`'s route assembly: consumer routes + factory-managed bootstrap routes (prefixed via `bootstrap_route_prefix`, default `'/api/account'`) + stub middleware + surface generation. `CreateTestAppSurfaceSpecOptions` accepts `session_options`, `create_route_specs`, `env_schema?`, `event_specs?`, `rpc_endpoints?`, `ws_endpoints?`, `transform_middleware?`, `bootstrap_route_prefix?`. Single source of truth for attack-surface tests — track `create_app_server` wiring changes here. |
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. |
40
40
 
41
41
  Throwing stubs surface mock escape: a test that accidentally reaches into
42
42
  stub territory breaks immediately with a label-scoped error rather than
@@ -79,9 +79,31 @@ Returns `{account, actor}`. Replaces the per-file `create_user` /
79
79
  `create_test_actor` / `create_test_account` helpers that had accumulated
80
80
  across the auth test suite. Use for query-level tests that need real
81
81
  DB rows but not a full session/token bundle. For tests that also need
82
- an API token + session cookie + role_grants, use `bootstrap_test_account`
82
+ an API token + session cookie + role_grants, use `bootstrap_test_keeper`
83
83
  from `app_server.ts` instead.
84
84
 
85
+ `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
87
+ 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.
92
+
93
+ ### `role_grant_helpers.ts` — RPC-flow role_grant helpers
94
+
95
+ `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
103
+ `TestApp | TestAccount` / `TestAccount` so the call site passes the same
104
+ object that already owns the headers + account id, ruling out caller-side
105
+ mismatch.
106
+
85
107
  ### `audit_drift_guard.ts` — audit-emission validation
86
108
 
87
109
  | Helper | Role |
@@ -154,18 +176,35 @@ Key module-scope values:
154
176
  `create_test_app_server` uses when no `db` is passed. Reuses the WASM
155
177
  cache via `create_pglite_factory`.
156
178
 
157
- `bootstrap_test_account(options)` is extracted because both
158
- `create_test_app_server` and `TestApp.create_account` reuse the same
159
- "insert account + actor + roles + API token + session + cookie" flow.
160
- Takes `{db, keyring, session_options, password, username?, password_value?, roles?}`.
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).
190
+
191
+ 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.
161
198
 
162
199
  | Type | Shape |
163
200
  | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
164
201
  | `TestAppServer extends AppBackend` | Adds `account`, `actor`, `api_token`, `session_cookie`, `keyring`, `cleanup()`. |
165
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. |
166
- | `CreateTestAppOptions extends TestAppServerOptions` | Adds `create_route_specs` (required), `rpc_endpoints?: RpcEndpointsSuiteOption` (top-level only — single source of truth, symmetric with the suite-level option), and `app_options?: SuiteAppOptions` (`Partial<AppServerOptions>` excluding the four fields the helper manages: `backend`, `session_options`, `create_route_specs`, `rpc_endpoints`). |
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`). |
167
204
  | `TestAccount` | `{account, actor, session_cookie, api_token, create_session_headers, create_bearer_headers}`. |
168
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). |
169
208
 
170
209
  `create_test_app` hard-codes the test-friendly `AppServerOptions`:
171
210
  `allowed_origins: [/^http:\/\/localhost/]`, stub proxy pinned to
@@ -200,6 +239,41 @@ hatch is test-only by construction.
200
239
  | `select_auth_app(apps, auth)` | Map `RouteAuth` → matching Hono app. Throws for missing `role:*` entries. |
201
240
  | `resolve_test_path(path)` | `:foo` → `test_foo` — adequate for routes without format-constrained params. |
202
241
 
242
+ ## Cross-impl schema parity
243
+
244
+ ### `schema_introspect.ts` — `query_schema_snapshot`
245
+
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. |
250
+
251
+ ### `schema_parity.ts` — `assert_schema_snapshots_equal`
252
+
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`. |
259
+
260
+ **Cross-impl gate pattern** — consumers running two backends against a
261
+ shared schema (zzz's `--backend=both`, fuz_app's cross-backend suite)
262
+ bootstrap each impl against an isolated DB, snapshot, then compare:
263
+
264
+ ```ts
265
+ await drop_recreate_db('zzz_test');
266
+ await spawn_backend(deno_config);
267
+ const snapshot_deno = await query_schema_snapshot(db);
268
+ await drop_recreate_db('zzz_test');
269
+ await spawn_backend(rust_config);
270
+ const snapshot_rust = await query_schema_snapshot(db);
271
+ assert_schema_snapshots_equal(snapshot_deno, snapshot_rust, {a: 'deno', b: 'rust'});
272
+ ```
273
+
274
+ Each impl's _own_ tests still gate its DDL correctness independently —
275
+ this pair is purely the cross-impl drift check.
276
+
203
277
  ## Assertions, coverage, helpers
204
278
 
205
279
  ### `assertions.ts` — surface + error-schema assertions
@@ -463,7 +537,7 @@ validates the response against declared schemas. DB-backed via
463
537
  `create_test_app`. Per-route test (`test.each`) — one line per route
464
538
  in the vitest output.
465
539
 
466
- Options: `{session_options, create_route_specs, app_options?, db_factories?, skip_routes?, input_overrides?}`.
540
+ Options: `{setup_test, surface_source, capabilities, skip_routes?, input_overrides?}`.
467
541
  `input_overrides` is a `Map<"METHOD /path", body>` — override generated
468
542
  bodies for routes whose input schema can't round-trip cleanly (e.g.
469
543
  fields that must reference DB state).
@@ -476,7 +550,7 @@ picks them up separately.
476
550
  DB-backed round-trip for RPC: one POST test for all methods, one GET
477
551
  test for `side_effects: false` methods. Successful responses validate
478
552
  against `action.spec.output`; error responses validate as well-formed
479
- JSON-RPC error envelopes. Required: `{session_options, create_route_specs, rpc_endpoints, ...}`.
553
+ JSON-RPC error envelopes. Options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints, skip_methods?, input_overrides?}`.
480
554
  The admin RPC auth test picks a session-based identity (`authed` /
481
555
  `admin` / bootstrapped keeper) based on `method.auth`; keeper uses the
482
556
  daemon token.
@@ -553,7 +627,7 @@ Support functions: `collect_json_schema_property_names(schema)` (walks
553
627
  `assert_output_schemas_no_sensitive_fields(surface, fields?)`,
554
628
  `assert_non_admin_schemas_no_admin_fields(surface, fields?)`.
555
629
 
556
- Options: `{build, session_options, create_route_specs, sensitive_fields?, admin_only_fields?, app_options?, db_factories?, skip_routes?}`.
630
+ Options: `{setup_test, surface_source, capabilities, sensitive_fields?, admin_only_fields?, skip_routes?}`.
557
631
 
558
632
  ### `rate_limiting.ts` — `describe_rate_limiting_tests`
559
633
 
@@ -567,7 +641,7 @@ Each group asserts its required route exists with a descriptive
567
641
  message. Creates a tight rate limiter (default `max_attempts: 2`,
568
642
  `window_ms: 60_000`) per test and disposes it in `finally`.
569
643
 
570
- Options: `{session_options, create_route_specs, app_options?, db_factories?, max_attempts?}`.
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.
571
645
 
572
646
  ## Integration suites
573
647
 
@@ -587,24 +661,39 @@ these thematic areas:
587
661
  8. Bearer auth + browser-context discard on mutations
588
662
  9. Token revocation + cross-account isolation
589
663
  10. Response body schema validation + error-response information leakage
590
- 11. Signup invite edge cases + rate-limiting smoke + expired credential rejection + error-coverage breadth
664
+ 11. Signup invite edge cases + expired credential rejection + error-coverage breadth
591
665
 
592
666
  An `ErrorCoverageCollector` runs across groups; `afterAll` filters to
593
667
  auth-related routes (login/logout/verify/sessions/tokens/password/
594
- signup/bootstrap) and asserts `DEFAULT_INTEGRATION_ERROR_COVERAGE`
595
- (20%). Consumer-specific routes aren't exercised herethey don't
596
- count against the baseline.
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).
597
675
 
598
- Options: `{session_options, create_route_specs, app_options?, db_factories?}`.
676
+ Options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints, error_coverage_min?}`.
599
677
 
600
678
  ### `admin_integration.ts` — `describe_standard_admin_integration_tests`
601
679
 
602
680
  7 test groups covering admin surface: account listing, role_grant grant
603
- lifecycle (via `role_grant_offer_create` + `role_grant_revoke` RPC flows —
604
- **not** REST; see `auth/CLAUDE.md` for `role_grant_offer_action_specs.ts` + `role_grant_offer_actions.ts`), session / token management, audit log reads (RPC),
605
- admin-to-admin isolation, error coverage, response schema validation.
606
-
607
- Required options: `{session_options, create_route_specs, roles: RoleSchemaResult, rpc_endpoints: RpcEndpointsSuiteOption, admin_prefix?, app_options?, db_factories?}`.
681
+ lifecycle (via `role_grant_offer_create` + `role_grant_offer_accept` +
682
+ `role_grant_revoke` RPC flows **not** REST, **not** direct
683
+ `query_accept_offer`; see `auth/CLAUDE.md` for
684
+ `role_grant_offer_action_specs.ts` + `role_grant_offer_actions.ts`),
685
+ session / token management, audit log reads (RPC), admin-to-admin
686
+ isolation, error coverage, response schema validation.
687
+
688
+ The shared `role_grant_offer_and_accept` helper (`role_grant_helpers.ts`)
689
+ 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`.
695
+
696
+ Required options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints: RpcEndpointsSuiteOption, roles: RoleSchemaResult, admin_prefix?}`.
608
697
 
609
698
  `rpc_endpoints` is `Array<RpcEndpointSpec> | ((ctx: AppServerContext) => Array<RpcEndpointSpec>)` —
610
699
  the same `RpcEndpointsSuiteOption` union every DB-backed suite accepts
@@ -644,28 +733,70 @@ branch.
644
733
 
645
734
  ### `audit_completeness.ts` — `describe_audit_completeness_tests`
646
735
 
647
- Verifies every auth mutation produces the expected `audit_log` row by
648
- querying the table after each request. Uses the real middleware stack.
736
+ Verifies every auth mutation produces the expected `audit_log` row.
737
+ Mutations fire over the real middleware stack; reads go back through the
738
+ `audit_log_list` RPC (the same path the admin UI consumes) — intentional
739
+ 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`.
742
+
649
743
  Same `rpc_endpoints` hard-fail as the admin suite — the mutation-audit
650
744
  tests drive role_grant flow, session/token revoke-all, and invite
651
745
  create/delete through `role_grant_offer_create_action_spec` /
652
- `role_grant_revoke_action_spec` / `admin_session_revoke_all_action_spec` /
746
+ `role_grant_offer_accept_action_spec` / `role_grant_revoke_action_spec` /
747
+ `admin_session_revoke_all_action_spec` /
653
748
  `admin_token_revoke_all_action_spec` / `app_settings_update_action_spec` /
654
749
  `invite_create_action_spec` / `invite_delete_action_spec`.
655
750
 
751
+ **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
755
+ bootstrapped admin's credentials (logout, session_revoke, password_change).
756
+
656
757
  Bootstrap audit logging is excluded because `create_test_app` doesn't
657
758
  provide the filesystem token state; covered separately in
658
759
  `bootstrap_account.db.test.ts`.
659
760
 
660
761
  ### `standard.ts` — `describe_standard_tests`
661
762
 
662
- Convenience wrapper: always runs `describe_standard_integration_tests`;
663
- runs `describe_standard_admin_integration_tests` only when `roles` is
664
- provided. `rpc_endpoints: RpcEndpointsSuiteOption` is a required field on
665
- `StandardTestOptions` the admin suite's requirement is enforced at the
666
- type level, so a missing `rpc_endpoints` is a compile error rather than a
667
- runtime throw. Round-trips the union through unchanged so consumers can
668
- pass either an eager array or the factory form.
763
+ Bundles every DB-backed suite carrying the standard option shape, each
764
+ gated on its relevant config — silent-skip when the gate isn't met:
765
+
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
790
+ `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.
794
+
795
+ Attack surface suites stay separate — their option shape is
796
+ `{build, snapshot_path, expected_public_routes, ...}` rather than the
797
+ shared `{setup_test, surface_source, capabilities}`. A peer
798
+ `describe_standard_surface_tests` bundler lives for that side if/when
799
+ needed.
669
800
 
670
801
  ## RPC helpers
671
802
 
@@ -718,7 +849,7 @@ Registry lookups:
718
849
  - unauthenticated → `unauthenticated` (code -32001)
719
850
  - wrong role → `forbidden` (-32002)
720
851
  - authenticated without role → `forbidden`
721
- - **keeper rejects non-daemon credentials** — session and api_token credentials are rejected even when the account has the keeper role (only `daemon_token` passes). The credential-type gate fires before the role gate (see `auth/CLAUDE.md` §`request_context.ts` for `require_credential_types`).
852
+ - **keeper rejects non-daemon credentials** — session and api_token credentials are rejected even when the account has the keeper role (only `daemon_token` passes). The credential-type gate fires before the role gate (see `auth/CLAUDE.md` §Keeper auth shape).
722
853
  - correct auth passes (not 401/403)
723
854
  - GET unauthenticated for `side_effects: false` reads
724
855
  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`.
@@ -764,3 +895,46 @@ fuz_app-specific points:
764
895
  (`create_bearer_auth_test_app`, `create_test_middleware_stack_app`)
765
896
  alongside the assertions in `src/test/auth/*.test.ts`. Drift surfaces
766
897
  as a missed assertion, not a test failure.
898
+
899
+ ## Cross-backend integration layer
900
+
901
+ 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.
907
+
908
+ The shape:
909
+
910
+ - `testing/cross_backend/setup.ts` — `SetupTest` / `TestFixture` /
911
+ `TestAccountFixture` / `CreateTestAccountOptions` types,
912
+ `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
936
+ `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.
@@ -1,33 +1,35 @@
1
1
  import './assert_dev_env.js';
2
2
  import type { SessionOptions } from '../auth/session_cookie.js';
3
- import type { AppServerContext } from '../server/app_server.js';
4
- import type { RouteSpec } from '../http/route_spec.js';
5
3
  import { type RoleSchemaResult } from '../auth/role_schema.js';
6
- import { type SuiteAppOptions } from './app_server.js';
7
- import { type DbFactory } from './db.js';
8
4
  import { type RpcEndpointsSuiteOption } from './rpc_helpers.js';
5
+ import type { BackendCapabilities } from './cross_backend/capabilities.js';
6
+ import type { SetupTest } from './cross_backend/setup.js';
7
+ import type { SurfaceSource } from './transports/surface_source.js';
9
8
  /**
10
9
  * Configuration for `describe_standard_admin_integration_tests`.
11
10
  */
12
11
  export interface StandardAdminIntegrationTestOptions {
13
- /** Session config for cookie-based auth. */
12
+ /**
13
+ * Per-test fixture-producing function. The admin suite calls this in
14
+ * every `test()` body — auth_integration_truncate_tables clears
15
+ * `account`, so each test re-bootstraps the keeper.
16
+ */
17
+ setup_test: SetupTest;
18
+ /**
19
+ * Source of the app surface for route iteration and error-coverage
20
+ * scoping. Currently requires `kind: 'inline'` — the cross-process
21
+ * snapshot variant lands alongside the spawned-backend transport plumbing.
22
+ */
23
+ surface_source: SurfaceSource;
24
+ /** Backend capability declarations. */
25
+ capabilities: BackendCapabilities;
26
+ /** Session config — needed for cookie_name + factory-form rpc_endpoints resolution. */
14
27
  session_options: SessionOptions<string>;
15
- /** Route spec factory — same one used in production. */
16
- create_route_specs: (ctx: AppServerContext) => Array<RouteSpec>;
17
28
  /** Role schema result from `create_role_schema()` — used to determine valid/invalid/web-grantable roles. */
18
29
  roles: RoleSchemaResult;
19
30
  /**
20
31
  * RPC endpoint specs — the source `RpcAction` arrays. Required; role_grant
21
32
  * grant/revoke are RPC-only and the suite hard-fails without them.
22
- *
23
- * Accepts either an array (eager) or a factory
24
- * `(ctx: AppServerContext) => Array<RpcEndpointSpec>` — the factory form
25
- * is required when action handlers must close over the per-test
26
- * `ctx.app_settings` / `ctx.deps` (e.g. the canonical
27
- * `create_standard_rpc_actions(ctx.deps, {app_settings: ctx.app_settings})`
28
- * pattern). The factory must return the same endpoint `path` regardless
29
- * of ctx — it is invoked once at setup with a stub ctx for path lookup
30
- * and again per-test by `create_app_server` for live dispatch.
31
33
  */
32
34
  rpc_endpoints: RpcEndpointsSuiteOption;
33
35
  /**
@@ -37,13 +39,6 @@ export interface StandardAdminIntegrationTestOptions {
37
39
  * stub deps. Default `'/api/admin'`.
38
40
  */
39
41
  admin_prefix?: string;
40
- /** Optional overrides for `AppServerOptions`. */
41
- app_options?: SuiteAppOptions;
42
- /**
43
- * Database factories to run tests against. Default: pglite only.
44
- * Pass consumer factories (e.g. `[pglite_factory, pg_factory]`) to also test against PostgreSQL.
45
- */
46
- db_factories?: Array<DbFactory>;
47
42
  }
48
43
  /**
49
44
  * Standard admin integration test suite for fuz_app admin routes.
@@ -1 +1 @@
1
- {"version":3,"file":"admin_integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/admin_integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAgC7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAA0B,KAAK,gBAAgB,EAAC,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAA6C,KAAK,eAAe,EAAC,MAAM,iBAAiB,CAAC;AACjG,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AASjB,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAoB1B;;GAEG;AACH,MAAM,WAAW,mCAAmC;IACnD,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,4GAA4G;IAC5G,KAAK,EAAE,gBAAgB,CAAC;IACxB;;;;;;;;;;;;OAYG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAChC;AAiCD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,mCAAmC,KAC1C,IAg2BF,CAAC"}
1
+ {"version":3,"file":"admin_integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/admin_integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAgC7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAA0B,KAAK,gBAAgB,EAAC,MAAM,wBAAwB,CAAC;AAWtF,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAiB1B,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,iCAAiC,CAAC;AACzE,OAAO,KAAK,EAAC,SAAS,EAAc,MAAM,0BAA0B,CAAC;AACrE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,gCAAgC,CAAC;AAElE;;GAEG;AACH,MAAM,WAAW,mCAAmC;IACnD;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,cAAc,EAAE,aAAa,CAAC;IAC9B,uCAAuC;IACvC,YAAY,EAAE,mBAAmB,CAAC;IAClC,uFAAuF;IACvF,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4GAA4G;IAC5G,KAAK,EAAE,gBAAgB,CAAC;IACxB;;;OAGG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAiBD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,mCAAmC,KAC1C,IAqzBF,CAAC"}