@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.
- package/dist/actions/CLAUDE.md +65 -86
- package/dist/actions/action_codegen.d.ts +1 -1
- package/dist/actions/action_codegen.js +1 -1
- package/dist/actions/action_event_data.d.ts +1 -1
- package/dist/auth/CLAUDE.md +83 -104
- package/dist/auth/audit_log_schema.js +2 -2
- package/dist/auth/daemon_token_middleware.d.ts +15 -5
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +24 -15
- package/dist/auth/invite_queries.d.ts +17 -7
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +19 -8
- package/dist/auth/signup_routes.d.ts +47 -1
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +103 -52
- package/dist/env/resolve.d.ts +44 -7
- package/dist/env/resolve.d.ts.map +1 -1
- package/dist/env/resolve.js +94 -27
- package/dist/http/CLAUDE.md +47 -52
- package/dist/http/jsonrpc.d.ts +23 -7
- package/dist/http/jsonrpc.d.ts.map +1 -1
- package/dist/http/jsonrpc.js +19 -3
- package/dist/http/surface.d.ts +9 -2
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/runtime/mock.d.ts +1 -1
- package/dist/runtime/mock.js +1 -1
- package/dist/testing/CLAUDE.md +659 -511
- package/dist/testing/admin_integration.d.ts +5 -5
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +95 -39
- package/dist/testing/app_server.d.ts +16 -1
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +18 -3
- package/dist/testing/audit_completeness.d.ts +7 -5
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +5 -9
- package/dist/testing/bootstrap_success.js +2 -2
- package/dist/testing/cross_backend/backend_config.d.ts +113 -0
- package/dist/testing/cross_backend/backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/backend_config.js +1 -0
- package/dist/testing/cross_backend/bench/bench_report.d.ts +46 -0
- package/dist/testing/cross_backend/bench/bench_report.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/bench_report.js +83 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +44 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +38 -0
- package/dist/testing/cross_backend/bench/scenario.d.ts +57 -0
- package/dist/testing/cross_backend/bench/scenario.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/scenario.js +28 -0
- package/dist/testing/cross_backend/bootstrap_backend.d.ts +41 -0
- package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -0
- package/dist/testing/cross_backend/bootstrap_backend.js +34 -0
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts +24 -0
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -0
- package/dist/testing/cross_backend/build_test_backend_paths.js +33 -0
- package/dist/testing/cross_backend/capabilities.d.ts +3 -2
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.d.ts +122 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_backend_configs.js +111 -0
- package/dist/testing/cross_backend/default_secrets.d.ts +40 -0
- package/dist/testing/cross_backend/default_secrets.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_secrets.js +39 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts +64 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_spine_surface.js +121 -0
- package/dist/testing/cross_backend/setup.d.ts +270 -34
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +495 -15
- package/dist/testing/cross_backend/spawn_backend.d.ts +58 -0
- package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -0
- package/dist/testing/cross_backend/spawn_backend.js +229 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.d.ts +66 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.js +49 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts +37 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -0
- package/dist/testing/cross_backend/sse_round_trip.js +137 -0
- package/dist/testing/cross_backend/standard.d.ts +96 -0
- package/dist/testing/cross_backend/standard.d.ts.map +1 -0
- package/dist/testing/cross_backend/standard.js +49 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +171 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_reset_actions.js +213 -0
- package/dist/testing/cross_backend/testing_server_bun.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_bun.js +59 -0
- package/dist/testing/cross_backend/testing_server_core.d.ts +140 -0
- package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_core.js +68 -0
- package/dist/testing/cross_backend/testing_server_deno.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_deno.js +37 -0
- package/dist/testing/cross_backend/testing_server_node.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_node.js +50 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +72 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.js +112 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +35 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_round_trip.js +113 -0
- package/dist/testing/data_exposure.d.ts +4 -6
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +1 -5
- package/dist/testing/db_entities.d.ts +18 -7
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +18 -7
- package/dist/testing/integration.d.ts +27 -6
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +93 -58
- package/dist/testing/round_trip.d.ts +4 -5
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +1 -5
- package/dist/testing/rpc_helpers.d.ts +10 -4
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +1 -1
- package/dist/testing/rpc_round_trip.d.ts +5 -5
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +1 -5
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +1 -68
- package/dist/testing/standard.d.ts +4 -5
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/stubs.d.ts +10 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +9 -2
- package/dist/testing/testing_rate_limiter.d.ts +59 -0
- package/dist/testing/testing_rate_limiter.d.ts.map +1 -0
- package/dist/testing/testing_rate_limiter.js +74 -0
- package/dist/testing/transports/bootstrap.d.ts +52 -0
- package/dist/testing/transports/bootstrap.d.ts.map +1 -0
- package/dist/testing/transports/bootstrap.js +70 -0
- package/dist/testing/transports/fetch_transport.d.ts +81 -0
- package/dist/testing/transports/fetch_transport.d.ts.map +1 -0
- package/dist/testing/transports/fetch_transport.js +74 -0
- package/dist/testing/transports/sse_frame_reader.d.ts +41 -0
- package/dist/testing/transports/sse_frame_reader.d.ts.map +1 -0
- package/dist/testing/transports/sse_frame_reader.js +84 -0
- package/dist/testing/transports/sse_transport.d.ts +54 -0
- package/dist/testing/transports/sse_transport.d.ts.map +1 -0
- package/dist/testing/transports/sse_transport.js +51 -0
- package/dist/testing/transports/ws_client.d.ts +108 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -0
- package/dist/testing/transports/ws_client.js +56 -0
- package/dist/testing/transports/ws_transport.d.ts +43 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -0
- package/dist/testing/transports/ws_transport.js +169 -0
- package/dist/testing/ws_round_trip.d.ts +21 -103
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +42 -40
- package/dist/ui/CLAUDE.md +5 -3
- package/dist/ui/MenuLink.svelte +16 -16
- package/dist/ui/MenuLink.svelte.d.ts +13 -4
- package/dist/ui/MenuLink.svelte.d.ts.map +1 -1
- package/package.json +7 -1
- package/dist/testing/transports/surface_source.d.ts +0 -51
- package/dist/testing/transports/surface_source.d.ts.map +0 -1
- package/dist/testing/transports/surface_source.js +0 -19
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -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 (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`
|
|
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`.
|
|
72
|
-
|
|
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
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
`
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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`
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
- `
|
|
176
|
-
|
|
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
|
-
(
|
|
193
|
-
|
|
194
|
-
For exercising the bootstrap success path end-to-end against an empty
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
`
|
|
212
|
-
|
|
213
|
-
**`await_pending_effects: true`** (fire-and-forget effects complete
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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,
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
337
|
-
- `
|
|
338
|
-
|
|
339
|
-
- `
|
|
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
|
-
|
|
369
|
-
|
|
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
|
-
|
|
375
|
-
- `
|
|
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
|
-
|
|
386
|
-
|
|
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
|
-
|
|
400
|
-
- `
|
|
401
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
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)` (
|
|
451
|
-
|
|
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
|
-
|
|
459
|
-
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
537
|
-
|
|
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
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
The admin RPC auth test picks a session-based identity (`authed` /
|
|
555
|
-
|
|
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
|
-
|
|
569
|
-
|
|
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
|
|
572
|
-
`\n\n` framing, a 2s per-read timeout
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
`request` throws with
|
|
602
|
-
asserting `result.foo` on a
|
|
603
|
-
not a `Cannot read property 'foo'
|
|
604
|
-
timeout_ms?)` checks already-received
|
|
605
|
-
new arrivals (default 1000ms); drops 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
|
|
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
|
-
|
|
642
|
-
|
|
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?}`.
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
692
|
-
header/account mismatch. Direct-grant fixtures (
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
|
|
709
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
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
|
-
|
|
754
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
793
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=¶ms=<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=¶ms=<JSON>`.
|
|
817
722
|
|
|
818
723
|
Response assertions:
|
|
819
724
|
|
|
820
|
-
- `assert_jsonrpc_error_response(body, expected_code?)` — validates
|
|
821
|
-
|
|
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
|
-
|
|
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
|
-
|
|
865
|
-
`actor_lookup_actions`, and `actor_search_actions`) get zero adversarial
|
|
866
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
- **`await_pending_effects: true`** is set by `create_test_app`.
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
`
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
- `
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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.
|