@fuzdev/fuz_app 0.90.0 → 0.91.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/auth/CLAUDE.md +7 -0
- package/dist/auth/all_cell_actions.d.ts +35 -0
- package/dist/auth/all_cell_actions.d.ts.map +1 -0
- package/dist/auth/all_cell_actions.js +32 -0
- package/dist/testing/CLAUDE.md +82 -9
- package/dist/testing/cross_backend/action_manifest.d.ts +113 -0
- package/dist/testing/cross_backend/action_manifest.d.ts.map +1 -0
- package/dist/testing/cross_backend/action_manifest.js +89 -0
- package/dist/testing/cross_backend/action_manifest_parity.d.ts +67 -0
- package/dist/testing/cross_backend/action_manifest_parity.d.ts.map +1 -0
- package/dist/testing/cross_backend/action_manifest_parity.js +88 -0
- package/dist/testing/cross_backend/capabilities.d.ts +47 -30
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +19 -4
- package/dist/testing/cross_backend/default_backend_configs.d.ts +31 -15
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +30 -14
- package/dist/testing/cross_backend/full_spine_mount.d.ts +81 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -0
- package/dist/testing/cross_backend/full_spine_mount.js +49 -0
- package/dist/testing/cross_backend/method_coverage.d.ts +71 -0
- package/dist/testing/cross_backend/method_coverage.d.ts.map +1 -0
- package/dist/testing/cross_backend/method_coverage.js +89 -0
- package/dist/testing/cross_backend/setup.d.ts +12 -0
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +19 -0
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +9 -5
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +76 -2
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +63 -2
- package/package.json +2 -2
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -211,6 +211,13 @@ auth, ids)`: batched strict relation-read filter. Every relation read —
|
|
|
211
211
|
`cell_item_actions.ts` / `cell_audit_actions.ts` — the relation + ACL +
|
|
212
212
|
audit handlers, exporting `to_grant_json` / `to_field_json` /
|
|
213
213
|
`to_item_json` reused by the `cell_get` bundle.
|
|
214
|
+
- `auth/all_cell_actions.ts` — `create_all_cell_actions(deps, {roles})`, the
|
|
215
|
+
handler-side twin of `all_cell_action_specs` and sibling of
|
|
216
|
+
`create_standard_rpc_actions`: bundles all five cell factories (CRUD + grant
|
|
217
|
+
- field + item + audit) into one list. Reach for this whenever a backend
|
|
218
|
+
mounts the full cell layer — a single source so an HTTP-RPC mount and a WS
|
|
219
|
+
mount (or two backends) can't diverge on which cell verbs they expose.
|
|
220
|
+
Distinct from `create_cell_actions` (the CRUD-only factory it bundles).
|
|
214
221
|
- `auth/cell_audit_metadata.ts` + `cell_grant_audit_metadata.ts` +
|
|
215
222
|
`cell_field_audit_metadata.ts` + `cell_item_audit_metadata.ts` — audit
|
|
216
223
|
metadata envelopes (IDs only), registered via `extra_events:` on the
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RpcAction } from '../actions/action_rpc.js';
|
|
2
|
+
import { type CellActionDeps } from './cell_actions.js';
|
|
3
|
+
import type { RoleSchemaResult } from './role_schema.js';
|
|
4
|
+
/**
|
|
5
|
+
* Options for `create_all_cell_actions`.
|
|
6
|
+
*
|
|
7
|
+
* `roles` flows to the `cell_grant_*` sub-factory — actor-shaped grants are
|
|
8
|
+
* role-validated against this registry, so callers thread their app's role
|
|
9
|
+
* schema (the same one `create_standard_rpc_actions` takes).
|
|
10
|
+
*/
|
|
11
|
+
export interface AllCellActionsOptions {
|
|
12
|
+
readonly roles: RoleSchemaResult;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build the full cell RPC action set — CRUD (`create_cell_actions`,
|
|
16
|
+
* which also carries `cell_clone`) + grant ACL + field + item relations +
|
|
17
|
+
* per-cell audit — as a single handler-bound bundle.
|
|
18
|
+
*
|
|
19
|
+
* The handler-side twin of the `all_cell_action_specs` spec bundle and the
|
|
20
|
+
* sibling of `create_standard_rpc_actions`. Assembling the five cell factories
|
|
21
|
+
* here means an HTTP-RPC mount and a WS mount (or two different backends)
|
|
22
|
+
* can't silently diverge on which cell verbs they expose — the
|
|
23
|
+
* `spine_method_coverage` reconciliation gate enforces that the spine's live
|
|
24
|
+
* mount matches its coverage manifest, and this aggregator is the single list
|
|
25
|
+
* every mount draws from.
|
|
26
|
+
*
|
|
27
|
+
* Distinct from `create_cell_actions` (the CRUD-only factory this bundles) —
|
|
28
|
+
* reach for this whenever a backend mounts the complete cell layer.
|
|
29
|
+
*
|
|
30
|
+
* @param deps - `CellActionDeps` (`log`, `audit`, optional `validate_data`)
|
|
31
|
+
* @param options - the role schema for grant validation
|
|
32
|
+
* @returns every cell `RpcAction`, in mount order
|
|
33
|
+
*/
|
|
34
|
+
export declare const create_all_cell_actions: (deps: CellActionDeps, options: AllCellActionsOptions) => Array<RpcAction>;
|
|
35
|
+
//# sourceMappingURL=all_cell_actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"all_cell_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/all_cell_actions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAsB,KAAK,cAAc,EAAC,MAAM,mBAAmB,CAAC;AAK3E,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,cAAc,EACpB,SAAS,qBAAqB,KAC5B,KAAK,CAAC,SAAS,CAMjB,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { create_cell_actions } from './cell_actions.js';
|
|
2
|
+
import { create_cell_grant_actions } from './cell_grant_actions.js';
|
|
3
|
+
import { create_cell_field_actions } from './cell_field_actions.js';
|
|
4
|
+
import { create_cell_item_actions } from './cell_item_actions.js';
|
|
5
|
+
import { create_cell_audit_actions } from './cell_audit_actions.js';
|
|
6
|
+
/**
|
|
7
|
+
* Build the full cell RPC action set — CRUD (`create_cell_actions`,
|
|
8
|
+
* which also carries `cell_clone`) + grant ACL + field + item relations +
|
|
9
|
+
* per-cell audit — as a single handler-bound bundle.
|
|
10
|
+
*
|
|
11
|
+
* The handler-side twin of the `all_cell_action_specs` spec bundle and the
|
|
12
|
+
* sibling of `create_standard_rpc_actions`. Assembling the five cell factories
|
|
13
|
+
* here means an HTTP-RPC mount and a WS mount (or two different backends)
|
|
14
|
+
* can't silently diverge on which cell verbs they expose — the
|
|
15
|
+
* `spine_method_coverage` reconciliation gate enforces that the spine's live
|
|
16
|
+
* mount matches its coverage manifest, and this aggregator is the single list
|
|
17
|
+
* every mount draws from.
|
|
18
|
+
*
|
|
19
|
+
* Distinct from `create_cell_actions` (the CRUD-only factory this bundles) —
|
|
20
|
+
* reach for this whenever a backend mounts the complete cell layer.
|
|
21
|
+
*
|
|
22
|
+
* @param deps - `CellActionDeps` (`log`, `audit`, optional `validate_data`)
|
|
23
|
+
* @param options - the role schema for grant validation
|
|
24
|
+
* @returns every cell `RpcAction`, in mount order
|
|
25
|
+
*/
|
|
26
|
+
export const create_all_cell_actions = (deps, options) => [
|
|
27
|
+
...create_cell_actions(deps),
|
|
28
|
+
...create_cell_grant_actions({ ...deps, roles: options.roles }),
|
|
29
|
+
...create_cell_field_actions(deps),
|
|
30
|
+
...create_cell_item_actions(deps),
|
|
31
|
+
...create_cell_audit_actions(),
|
|
32
|
+
];
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -222,7 +222,37 @@ test-only by construction.
|
|
|
222
222
|
- `assert_schema_snapshots_equal(a, b, labels?)` — throws on drift with a fully-formatted diff message.
|
|
223
223
|
- `SchemaDiff` — tagged-union per drift kind: `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`, `enum_only_in`, `enum_labels_differ` (enum labels compared positionally — declared order is significant).
|
|
224
224
|
|
|
225
|
-
fuz_app's own spine gates this cross-process via the `
|
|
225
|
+
fuz_app's own spine gates this cross-process via the `cross_backend_parity` project (`schema_parity.cross.test.ts` + the dual-spawn `global_setup_schema_parity.ts`), diffing the TS spine ↔ `testing_spine_stub` full schema — `npm run test:cross:parity`. The forge has its own deno↔rust parity gate.
|
|
226
|
+
|
|
227
|
+
## Cross-impl RPC-manifest parity
|
|
228
|
+
|
|
229
|
+
The action-surface sibling of schema parity. Where schema parity diffs the live
|
|
230
|
+
_database_ shape, this diffs the live _RPC registry_ shape — proving the TS
|
|
231
|
+
spine and the Rust `testing_spine_stub` mount the same method set with the same
|
|
232
|
+
per-method auth shape.
|
|
233
|
+
|
|
234
|
+
- `cross_backend/action_manifest.ts` — `ActionManifest` / `ActionManifestEntry`
|
|
235
|
+
Zod schema (the `_testing_action_manifest` wire validator) + `build_action_manifest(specs)`,
|
|
236
|
+
which normalizes each spec to `{method, side_effects, account, actor, roles,
|
|
237
|
+
credential_types}` (auth axes pass through; `roles` / `credential_types` flatten
|
|
238
|
+
to sorted arrays). Twin of `schema_introspect.ts`. Excludes the protocol actions
|
|
239
|
+
(`heartbeat` / `cancel`) by construction — the two impls organize them
|
|
240
|
+
differently (TS WS-only; Rust on one shared registry), so the caller passes a
|
|
241
|
+
protocol-free list.
|
|
242
|
+
- `cross_backend/action_manifest_parity.ts` — `diff_action_manifests` /
|
|
243
|
+
`format_action_manifest_diffs` / `assert_action_manifests_equal` (twin of
|
|
244
|
+
`schema_parity.ts`) — asserts exact method-set + per-method auth-shape parity.
|
|
245
|
+
- `cross_backend/setup.ts` `capture_action_manifest(handle)` — dumps a backend's
|
|
246
|
+
manifest over the daemon-token `_testing_action_manifest` channel (twin of
|
|
247
|
+
`capture_schema_snapshot`).
|
|
248
|
+
|
|
249
|
+
It runs under the same dual-spawn `cross_backend_parity` project (one TS-spine +
|
|
250
|
+
Rust-stub spawn serves both gates). It complements the in-repo
|
|
251
|
+
`spine_method_coverage` reconciliation gate (which proves _mounted ⟹ covered_);
|
|
252
|
+
this proves _TS-mount-set ≡ Rust-mount-set_. The gate asserts **exact** parity —
|
|
253
|
+
every mounted method and its full auth shape (side-effects + the four auth axes)
|
|
254
|
+
must match across the two impls, with no allowlisted divergences (see
|
|
255
|
+
`action_manifest_parity.cross.test.ts`).
|
|
226
256
|
|
|
227
257
|
**Cross-impl gate pattern** — a dual-impl consumer running two backends
|
|
228
258
|
(a TS Hono server and a Rust spine server) against a shared schema, plus
|
|
@@ -861,18 +891,26 @@ source of truth for wire-shape conformance.
|
|
|
861
891
|
deliver it.
|
|
862
892
|
|
|
863
893
|
- `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
|
|
864
|
-
(
|
|
865
|
-
`
|
|
866
|
-
`
|
|
894
|
+
(the **gating** flags `ws` / `sse` / `cell_crud` / `cell_relations` /
|
|
895
|
+
`account_lifecycle` / `fact_serving` / `ready` / `account_status` /
|
|
896
|
+
`oversized_reject_closes_connection` — each has a `test_if` reader),
|
|
867
897
|
`test_if(cond, name, fn)`
|
|
868
|
-
for capability-gated cases, and `in_process_capabilities` preset.
|
|
898
|
+
for capability-gated cases, and `in_process_capabilities` preset. The
|
|
899
|
+
non-gating wiring facts (`bearer_auth` / `trusted_proxy` / `login_rate_limit`,
|
|
900
|
+
no `test_if` readers) live in the parallel `BackendShapeNotes` record
|
|
901
|
+
(`in_process_shape_notes`; `ts_default_shape_notes` / `rust_default_shape_notes`
|
|
902
|
+
in `default_backend_configs.ts`) — split out so the capability type stops
|
|
903
|
+
claiming gating it doesn't do. `cell_crud`
|
|
869
904
|
gates the CRUD parity suite, `cell_relations` the relation / ACL / audit
|
|
870
905
|
parity suite — both `true` on every backend that live-mounts the full cell
|
|
871
906
|
surface (TS spine binary, in-process app, Rust stub). A backend mounting only
|
|
872
907
|
plain CRUD would declare `cell_crud: true, cell_relations: false`.
|
|
873
908
|
`account_lifecycle` gates `describe_account_lifecycle_cross_tests` (the
|
|
874
|
-
`account_delete` / `account_undelete` / `account_purge` parity suite)
|
|
875
|
-
|
|
909
|
+
`account_delete` / `account_undelete` / `account_purge` parity suite); unlike
|
|
910
|
+
cells those verbs **are** on the declared surface (in `create_admin_actions`,
|
|
911
|
+
auto-enumerated for shape/auth) — the flag gates the behavioral parity the
|
|
912
|
+
generic round-trip can't drive (it can't delete the subject). `true` on every
|
|
913
|
+
spine. `fact_serving`
|
|
876
914
|
gates `describe_fact_serving_cross_tests` (the cell-scoped per-reference +
|
|
877
915
|
admin-only bare-hash fact-serving parity suite); like cells it stays off the
|
|
878
916
|
declared surface and is `true` on every spine that mounts the serve routes +
|
|
@@ -1191,6 +1229,16 @@ in-process legs (plain `gro test`) are `src/test/auth/cell_crud_parity.db.test.t
|
|
|
1191
1229
|
payload-expiry tests never reached. Daemon-token-gated like its siblings; the
|
|
1192
1230
|
Rust mirror is `fuz_testing::create_testing_mint_session_action_spec`.
|
|
1193
1231
|
|
|
1232
|
+
Also in the module — but **not** bundled by `create_testing_actions` —
|
|
1233
|
+
`_testing_action_manifest` (+ `create_testing_action_manifest_action(mounted)`):
|
|
1234
|
+
dumps the live RPC registry as a normalized `ActionManifest` for the
|
|
1235
|
+
cross-impl manifest-parity gate (see §Cross-impl RPC-manifest parity). It must
|
|
1236
|
+
enumerate _every_ mounted method, which `create_testing_actions` (one
|
|
1237
|
+
sub-factory) can't see, so it's appended at the full-mount layer
|
|
1238
|
+
(`full_spine_mount.ts` `build_full_spine_rpc_actions`), closing over the
|
|
1239
|
+
complete action list plus its own spec. Daemon-token-gated like its siblings;
|
|
1240
|
+
the Rust mirror is `fuz_testing::create_testing_action_manifest_action_spec`.
|
|
1241
|
+
|
|
1194
1242
|
### Origin verification parity — `cross_backend/origin.ts`
|
|
1195
1243
|
|
|
1196
1244
|
`describe_origin_cross_tests({setup_test, capabilities, rpc_path?})` — the
|
|
@@ -1309,10 +1357,35 @@ re-roll the serve / daemon-info / WS-attach / drain boilerplate:
|
|
|
1309
1357
|
- `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.
|
|
1310
1358
|
- `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).
|
|
1311
1359
|
- `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.
|
|
1312
|
-
- `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.
|
|
1360
|
+
- `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`. This is the **declared** surface — the `create_standard_rpc_actions` bundle the spec-derived suites auto-enumerate. `$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.
|
|
1361
|
+
- `testing/cross_backend/full_spine_mount.ts` — `build_full_spine_rpc_actions(deps, options)` / `full_spine_rpc_endpoints(ctx, options)` — the **full** live RPC mount: the declared bundle **plus** the off-declared-surface families the binary live-mounts (`_testing_*` backdoors, the cell verb set, the opt-in `actor_lookup` / `actor_search` resolvers). Single-sources what was an inline assembly in `testing_spine_server.ts`, so the binary and the `spine_method_coverage` reconciliation test build the same list. Also `$lib`-free.
|
|
1313
1362
|
- `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 `rust_spine_stub_backend_config()`.
|
|
1314
1363
|
|
|
1315
|
-
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` + `
|
|
1364
|
+
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` + `full_spine_rpc_endpoints` + 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.
|
|
1365
|
+
|
|
1366
|
+
### Live-method coverage reconciliation — `method_coverage.ts`
|
|
1367
|
+
|
|
1368
|
+
`assert_rpc_method_coverage({live_methods, declared_methods, manifest})` keeps
|
|
1369
|
+
the **off-declared-surface** methods as drift-proof as the declared ones. The
|
|
1370
|
+
spec-derived suites auto-enumerate the declared surface, but a backend's live
|
|
1371
|
+
RPC endpoint also mounts methods kept off it (stateful cell verbs, opt-in
|
|
1372
|
+
resolvers, `_testing_*` backdoors) that rely on hand-wired imperative suites —
|
|
1373
|
+
nothing structurally guaranteed every live method was actually claimed by one.
|
|
1374
|
+
|
|
1375
|
+
The assertion diffs the live method set against a `MethodCoverageEntry`
|
|
1376
|
+
manifest both directions (a mounted-but-unclaimed method **and** a stale
|
|
1377
|
+
manifest row both fail loud) and checks each entry's `tier`
|
|
1378
|
+
(`declared` / `off_surface` / `backdoor`) is consistent with the declared
|
|
1379
|
+
surface + the `_testing_` prefix. `off_surface` rows must name the covering
|
|
1380
|
+
`suite`; `capability` is typed `keyof BackendCapabilities` so a stale flag is a
|
|
1381
|
+
compile error. The manifest is the forcing function — a method can't reach the
|
|
1382
|
+
live mount without a row naming its coverage.
|
|
1383
|
+
|
|
1384
|
+
Pairs with `surface_invariants.ts` `assert_no_testing_methods` (the _reverse_
|
|
1385
|
+
guard — a backdoor must never leak _onto_ the declared surface). fuz_app's own
|
|
1386
|
+
wiring is the manifest `src/test/cross_backend/spine_method_coverage.ts` + the
|
|
1387
|
+
no-DB reconciliation test `spine_method_coverage.test.ts`, which enumerates
|
|
1388
|
+
`build_full_spine_rpc_actions` with stub deps against `create_spine_surface_spec`.
|
|
1316
1389
|
|
|
1317
1390
|
The in-process `ws_round_trip` harness stays (it drives the dispatcher
|
|
1318
1391
|
against a fake upgrade, no wire), but the real-upgrade coverage now lives in
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-impl RPC **action-manifest** introspection — a normalized,
|
|
4
|
+
* JSON-serializable dump of a backend's live RPC method set, one entry per
|
|
5
|
+
* method carrying its auth shape + side-effect flag.
|
|
6
|
+
*
|
|
7
|
+
* The sibling of `schema_introspect.ts`'s `SchemaSnapshot`: where that
|
|
8
|
+
* captures the live *database* shape for the schema-parity gate, this
|
|
9
|
+
* captures the live *RPC registry* shape for the action-manifest parity gate.
|
|
10
|
+
* Both are dumped over a daemon-token `_testing_*` introspection action
|
|
11
|
+
* (`_testing_action_manifest` here, `_testing_schema_snapshot` there) and
|
|
12
|
+
* diffed across the TS spine and the Rust `testing_spine_stub` so a
|
|
13
|
+
* method-set or per-method auth-shape divergence fails loud. This complements
|
|
14
|
+
* the in-repo `spine_method_coverage` gate: that proves *mounted ⟹ covered*;
|
|
15
|
+
* this proves *TS-mount-set ≡ Rust-mount-set* (method set + auth shape).
|
|
16
|
+
*
|
|
17
|
+
* **Scope — domain + testing surface, not wire protocol.** The manifest is
|
|
18
|
+
* built from exactly the spec list its caller passes
|
|
19
|
+
* (`build_full_spine_rpc_actions` on the TS spine; the spec list minus
|
|
20
|
+
* `PROTOCOL_ACTION_SPECS` on the Rust stub). The protocol actions
|
|
21
|
+
* (`heartbeat` / `cancel`) are excluded by the caller on purpose: the two
|
|
22
|
+
* impls organize them differently (the TS spine mounts them on the WS
|
|
23
|
+
* endpoint only, never the HTTP-RPC endpoint; the Rust stub compiles one
|
|
24
|
+
* shared registry serving both transports), so including them would be a
|
|
25
|
+
* spurious cross-impl diff. This matches the scope of the in-repo
|
|
26
|
+
* `spine_method_coverage` gate (also over `build_full_spine_rpc_actions`).
|
|
27
|
+
*
|
|
28
|
+
* Paired with `action_manifest_parity.ts` for the diff + assertion helpers.
|
|
29
|
+
*
|
|
30
|
+
* `$lib`-free by contract — reached by the spawned TS binary (via
|
|
31
|
+
* `testing_reset_actions.ts` → `full_spine_mount.ts`), so every import is
|
|
32
|
+
* relative.
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
import { z } from 'zod';
|
|
37
|
+
import { type RouteAuth } from '../../http/auth_shape.js';
|
|
38
|
+
import type { RequestResponseActionSpec } from '../../actions/action_spec.js';
|
|
39
|
+
/**
|
|
40
|
+
* One method's normalized RPC metadata — the cross-impl-comparable unit.
|
|
41
|
+
* `roles` / `credential_types` are always present + sorted (an absent gate
|
|
42
|
+
* and an empty list both serialize to `[]`) so the diff never trips on a
|
|
43
|
+
* `undefined`-vs-`[]` or declaration-order difference between impls; the
|
|
44
|
+
* auth axes reuse the canonical `AuthAxisState` enum.
|
|
45
|
+
*/
|
|
46
|
+
export declare const ActionManifestEntry: z.ZodObject<{
|
|
47
|
+
/** RPC method name (`spec.method`). */
|
|
48
|
+
method: z.ZodString;
|
|
49
|
+
/** `true` when the action declares `side_effects` (mutation; POST-only on the RPC transport). */
|
|
50
|
+
side_effects: z.ZodBoolean;
|
|
51
|
+
/** The `account` auth axis. */
|
|
52
|
+
account: z.ZodEnum<{
|
|
53
|
+
optional: "optional";
|
|
54
|
+
none: "none";
|
|
55
|
+
required: "required";
|
|
56
|
+
}>;
|
|
57
|
+
/** The `actor` auth axis. */
|
|
58
|
+
actor: z.ZodEnum<{
|
|
59
|
+
optional: "optional";
|
|
60
|
+
none: "none";
|
|
61
|
+
required: "required";
|
|
62
|
+
}>;
|
|
63
|
+
/** Required roles (any-of), sorted; `[]` when the action declares no role gate. */
|
|
64
|
+
roles: z.ZodArray<z.ZodString>;
|
|
65
|
+
/** Permitted credential channels, sorted; `[]` when ungated (any credential). */
|
|
66
|
+
credential_types: z.ZodArray<z.ZodString>;
|
|
67
|
+
}, z.core.$strict>;
|
|
68
|
+
export type ActionManifestEntry = z.infer<typeof ActionManifestEntry>;
|
|
69
|
+
/** The full action manifest — every entry, sorted by `method`. */
|
|
70
|
+
export declare const ActionManifest: z.ZodObject<{
|
|
71
|
+
methods: z.ZodArray<z.ZodObject<{
|
|
72
|
+
/** RPC method name (`spec.method`). */
|
|
73
|
+
method: z.ZodString;
|
|
74
|
+
/** `true` when the action declares `side_effects` (mutation; POST-only on the RPC transport). */
|
|
75
|
+
side_effects: z.ZodBoolean;
|
|
76
|
+
/** The `account` auth axis. */
|
|
77
|
+
account: z.ZodEnum<{
|
|
78
|
+
optional: "optional";
|
|
79
|
+
none: "none";
|
|
80
|
+
required: "required";
|
|
81
|
+
}>;
|
|
82
|
+
/** The `actor` auth axis. */
|
|
83
|
+
actor: z.ZodEnum<{
|
|
84
|
+
optional: "optional";
|
|
85
|
+
none: "none";
|
|
86
|
+
required: "required";
|
|
87
|
+
}>;
|
|
88
|
+
/** Required roles (any-of), sorted; `[]` when the action declares no role gate. */
|
|
89
|
+
roles: z.ZodArray<z.ZodString>;
|
|
90
|
+
/** Permitted credential channels, sorted; `[]` when ungated (any credential). */
|
|
91
|
+
credential_types: z.ZodArray<z.ZodString>;
|
|
92
|
+
}, z.core.$strict>>;
|
|
93
|
+
}, z.core.$strict>;
|
|
94
|
+
export type ActionManifest = z.infer<typeof ActionManifest>;
|
|
95
|
+
/**
|
|
96
|
+
* Normalize one spec's auth + side-effects into a manifest entry. Pulls the
|
|
97
|
+
* four auth axes off `RouteAuth` (the same shape the Rust `AuthSpec` mirrors)
|
|
98
|
+
* and flattens optional `roles` / `credential_types` to sorted arrays.
|
|
99
|
+
*/
|
|
100
|
+
export declare const action_manifest_entry: (spec: {
|
|
101
|
+
readonly method: string;
|
|
102
|
+
readonly auth: RouteAuth;
|
|
103
|
+
readonly side_effects: boolean;
|
|
104
|
+
}) => ActionManifestEntry;
|
|
105
|
+
/**
|
|
106
|
+
* Build the normalized `ActionManifest` from a list of request-response
|
|
107
|
+
* specs. Entries are sorted by `method` so two impls producing the same set
|
|
108
|
+
* serialize identically regardless of mount order. The caller owns the scope
|
|
109
|
+
* it passes (the TS spine passes its full mount; the Rust stub filters
|
|
110
|
+
* `PROTOCOL_ACTION_SPECS` first) — see the module doc.
|
|
111
|
+
*/
|
|
112
|
+
export declare const build_action_manifest: (specs: ReadonlyArray<Pick<RequestResponseActionSpec, "method" | "auth" | "side_effects">>) => ActionManifest;
|
|
113
|
+
//# sourceMappingURL=action_manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action_manifest.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/action_manifest.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,EAAgB,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AACvE,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,8BAA8B,CAAC;AAE5E;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB;IAC/B,uCAAuC;;IAEvC,iGAAiG;;IAEjG,+BAA+B;;;;;;IAE/B,6BAA6B;;;;;;IAE7B,mFAAmF;;IAEnF,iFAAiF;;kBAEhF,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,kEAAkE;AAClE,eAAO,MAAM,cAAc;;QAhB1B,uCAAuC;;QAEvC,iGAAiG;;QAEjG,+BAA+B;;;;;;QAE/B,6BAA6B;;;;;;QAE7B,mFAAmF;;QAEnF,iFAAiF;;;kBAQhF,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAM5D;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAAI,MAAM;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAC/B,KAAG,mBAOF,CAAC;AAMH;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GACjC,OAAO,aAAa,CAAC,IAAI,CAAC,yBAAyB,EAAE,QAAQ,GAAG,MAAM,GAAG,cAAc,CAAC,CAAC,KACvF,cAED,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-impl RPC **action-manifest** introspection — a normalized,
|
|
4
|
+
* JSON-serializable dump of a backend's live RPC method set, one entry per
|
|
5
|
+
* method carrying its auth shape + side-effect flag.
|
|
6
|
+
*
|
|
7
|
+
* The sibling of `schema_introspect.ts`'s `SchemaSnapshot`: where that
|
|
8
|
+
* captures the live *database* shape for the schema-parity gate, this
|
|
9
|
+
* captures the live *RPC registry* shape for the action-manifest parity gate.
|
|
10
|
+
* Both are dumped over a daemon-token `_testing_*` introspection action
|
|
11
|
+
* (`_testing_action_manifest` here, `_testing_schema_snapshot` there) and
|
|
12
|
+
* diffed across the TS spine and the Rust `testing_spine_stub` so a
|
|
13
|
+
* method-set or per-method auth-shape divergence fails loud. This complements
|
|
14
|
+
* the in-repo `spine_method_coverage` gate: that proves *mounted ⟹ covered*;
|
|
15
|
+
* this proves *TS-mount-set ≡ Rust-mount-set* (method set + auth shape).
|
|
16
|
+
*
|
|
17
|
+
* **Scope — domain + testing surface, not wire protocol.** The manifest is
|
|
18
|
+
* built from exactly the spec list its caller passes
|
|
19
|
+
* (`build_full_spine_rpc_actions` on the TS spine; the spec list minus
|
|
20
|
+
* `PROTOCOL_ACTION_SPECS` on the Rust stub). The protocol actions
|
|
21
|
+
* (`heartbeat` / `cancel`) are excluded by the caller on purpose: the two
|
|
22
|
+
* impls organize them differently (the TS spine mounts them on the WS
|
|
23
|
+
* endpoint only, never the HTTP-RPC endpoint; the Rust stub compiles one
|
|
24
|
+
* shared registry serving both transports), so including them would be a
|
|
25
|
+
* spurious cross-impl diff. This matches the scope of the in-repo
|
|
26
|
+
* `spine_method_coverage` gate (also over `build_full_spine_rpc_actions`).
|
|
27
|
+
*
|
|
28
|
+
* Paired with `action_manifest_parity.ts` for the diff + assertion helpers.
|
|
29
|
+
*
|
|
30
|
+
* `$lib`-free by contract — reached by the spawned TS binary (via
|
|
31
|
+
* `testing_reset_actions.ts` → `full_spine_mount.ts`), so every import is
|
|
32
|
+
* relative.
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
import { z } from 'zod';
|
|
37
|
+
import { AuthAxisState } from '../../http/auth_shape.js';
|
|
38
|
+
/**
|
|
39
|
+
* One method's normalized RPC metadata — the cross-impl-comparable unit.
|
|
40
|
+
* `roles` / `credential_types` are always present + sorted (an absent gate
|
|
41
|
+
* and an empty list both serialize to `[]`) so the diff never trips on a
|
|
42
|
+
* `undefined`-vs-`[]` or declaration-order difference between impls; the
|
|
43
|
+
* auth axes reuse the canonical `AuthAxisState` enum.
|
|
44
|
+
*/
|
|
45
|
+
export const ActionManifestEntry = z.strictObject({
|
|
46
|
+
/** RPC method name (`spec.method`). */
|
|
47
|
+
method: z.string(),
|
|
48
|
+
/** `true` when the action declares `side_effects` (mutation; POST-only on the RPC transport). */
|
|
49
|
+
side_effects: z.boolean(),
|
|
50
|
+
/** The `account` auth axis. */
|
|
51
|
+
account: AuthAxisState,
|
|
52
|
+
/** The `actor` auth axis. */
|
|
53
|
+
actor: AuthAxisState,
|
|
54
|
+
/** Required roles (any-of), sorted; `[]` when the action declares no role gate. */
|
|
55
|
+
roles: z.array(z.string()),
|
|
56
|
+
/** Permitted credential channels, sorted; `[]` when ungated (any credential). */
|
|
57
|
+
credential_types: z.array(z.string()),
|
|
58
|
+
});
|
|
59
|
+
/** The full action manifest — every entry, sorted by `method`. */
|
|
60
|
+
export const ActionManifest = z.strictObject({
|
|
61
|
+
methods: z.array(ActionManifestEntry),
|
|
62
|
+
});
|
|
63
|
+
/** Sorted copy of a readonly string list (stable cross-impl ordering). */
|
|
64
|
+
const sorted_strings = (xs) => [...(xs ?? [])].sort();
|
|
65
|
+
/**
|
|
66
|
+
* Normalize one spec's auth + side-effects into a manifest entry. Pulls the
|
|
67
|
+
* four auth axes off `RouteAuth` (the same shape the Rust `AuthSpec` mirrors)
|
|
68
|
+
* and flattens optional `roles` / `credential_types` to sorted arrays.
|
|
69
|
+
*/
|
|
70
|
+
export const action_manifest_entry = (spec) => ({
|
|
71
|
+
method: spec.method,
|
|
72
|
+
side_effects: spec.side_effects,
|
|
73
|
+
account: spec.auth.account,
|
|
74
|
+
actor: spec.auth.actor,
|
|
75
|
+
roles: sorted_strings(spec.auth.roles),
|
|
76
|
+
credential_types: sorted_strings(spec.auth.credential_types),
|
|
77
|
+
});
|
|
78
|
+
/** Deterministic byte-lexicographic method order — matches the Rust `str::cmp` the stub sorts by. */
|
|
79
|
+
const compare_method = (a, b) => a.method < b.method ? -1 : a.method > b.method ? 1 : 0;
|
|
80
|
+
/**
|
|
81
|
+
* Build the normalized `ActionManifest` from a list of request-response
|
|
82
|
+
* specs. Entries are sorted by `method` so two impls producing the same set
|
|
83
|
+
* serialize identically regardless of mount order. The caller owns the scope
|
|
84
|
+
* it passes (the TS spine passes its full mount; the Rust stub filters
|
|
85
|
+
* `PROTOCOL_ACTION_SPECS` first) — see the module doc.
|
|
86
|
+
*/
|
|
87
|
+
export const build_action_manifest = (specs) => ({
|
|
88
|
+
methods: specs.map(action_manifest_entry).sort(compare_method),
|
|
89
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-impl action-manifest parity — structural diff + assertion over two
|
|
4
|
+
* `ActionManifest`s captured via the `_testing_action_manifest` RPC action.
|
|
5
|
+
*
|
|
6
|
+
* The action-surface twin of `schema_parity.ts`: two live impls (the TS
|
|
7
|
+
* fuz_app spine and the Rust `testing_spine_stub`) are each other's parity
|
|
8
|
+
* reference. After both bootstrap, dump each one's live RPC method set, diff,
|
|
9
|
+
* fail loudly on drift. The diff entries name the specific divergence (a
|
|
10
|
+
* method only one impl mounts, a per-method auth-axis or side-effect
|
|
11
|
+
* mismatch) so the error message points at the source.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const manifest_a = await capture_action_manifest(ts_handle);
|
|
15
|
+
* const manifest_b = await capture_action_manifest(rust_handle);
|
|
16
|
+
* assert_action_manifests_equal(manifest_a, manifest_b, {a: 'ts', b: 'rust'});
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Non-coverage: the manifest captures `{method, side_effects, account, actor,
|
|
20
|
+
* roles, credential_types}` — the wire-relevant auth shape. It does **not**
|
|
21
|
+
* capture input/output schemas (the declared-surface wire shape is gated by
|
|
22
|
+
* `rpc_round_trip`) nor the protocol actions `heartbeat` / `cancel` (excluded
|
|
23
|
+
* by construction — see `action_manifest.ts`).
|
|
24
|
+
*
|
|
25
|
+
* @module
|
|
26
|
+
*/
|
|
27
|
+
import type { ActionManifest } from './action_manifest.js';
|
|
28
|
+
/** Structured drift entry. `where` is the named source impl ('a' or 'b'). */
|
|
29
|
+
export type ActionManifestDiff = {
|
|
30
|
+
readonly kind: 'method_only_in';
|
|
31
|
+
readonly where: 'a' | 'b';
|
|
32
|
+
readonly method: string;
|
|
33
|
+
} | {
|
|
34
|
+
readonly kind: 'side_effects_differ';
|
|
35
|
+
readonly method: string;
|
|
36
|
+
readonly a: boolean;
|
|
37
|
+
readonly b: boolean;
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: 'auth_field_differs';
|
|
40
|
+
readonly method: string;
|
|
41
|
+
readonly field: 'account' | 'actor' | 'roles' | 'credential_types';
|
|
42
|
+
readonly a: unknown;
|
|
43
|
+
readonly b: unknown;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Structural diff between two manifests — empty array means parity holds.
|
|
47
|
+
*
|
|
48
|
+
* Order is deterministic: methods in sorted order, with each method's
|
|
49
|
+
* side-effect + auth-field sub-diffs grouped together.
|
|
50
|
+
*/
|
|
51
|
+
export declare const diff_action_manifests: (a: ActionManifest, b: ActionManifest) => Array<ActionManifestDiff>;
|
|
52
|
+
/** Labels used in formatted output — defaults to `'a'` and `'b'`. */
|
|
53
|
+
export interface ActionManifestDiffLabels {
|
|
54
|
+
readonly a?: string;
|
|
55
|
+
readonly b?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Render a diff list as a human-readable multi-line string. Empty diffs
|
|
59
|
+
* produce an empty string.
|
|
60
|
+
*/
|
|
61
|
+
export declare const format_action_manifest_diffs: (diffs: ReadonlyArray<ActionManifestDiff>, labels?: ActionManifestDiffLabels) => string;
|
|
62
|
+
/**
|
|
63
|
+
* Throw if the two manifests disagree. The error message names the impls
|
|
64
|
+
* (via `labels`) and lists every diff, so the failure is self-diagnosing.
|
|
65
|
+
*/
|
|
66
|
+
export declare const assert_action_manifests_equal: (a: ActionManifest, b: ActionManifest, labels?: ActionManifestDiffLabels) => void;
|
|
67
|
+
//# sourceMappingURL=action_manifest_parity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action_manifest_parity.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/action_manifest_parity.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAC,cAAc,EAAsB,MAAM,sBAAsB,CAAC;AAW9E,6EAA6E;AAC7E,MAAM,MAAM,kBAAkB,GAC3B;IAAC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GACrF;IACA,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;CACnB,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,kBAAkB,CAAC;IACnE,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAML;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GACjC,GAAG,cAAc,EACjB,GAAG,cAAc,KACf,KAAK,CAAC,kBAAkB,CA+B1B,CAAC;AAEF,qEAAqE;AACrE,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,OAAO,aAAa,CAAC,kBAAkB,CAAC,EACxC,SAAQ,wBAA6B,KACnC,MA4BF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,6BAA6B,GACzC,GAAG,cAAc,EACjB,GAAG,cAAc,EACjB,SAAQ,wBAA6B,KACnC,IAQF,CAAC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/** The auth axes compared as scalars (`'none' | 'optional' | 'required'`). */
|
|
3
|
+
const AUTH_SCALAR_FIELDS = ['account', 'actor'];
|
|
4
|
+
/** The auth axes compared as sorted string lists. */
|
|
5
|
+
const AUTH_LIST_FIELDS = ['roles', 'credential_types'];
|
|
6
|
+
/** Positional string-list equality (entries are pre-sorted by `build_action_manifest`). */
|
|
7
|
+
const string_lists_equal = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
|
|
8
|
+
/**
|
|
9
|
+
* Structural diff between two manifests — empty array means parity holds.
|
|
10
|
+
*
|
|
11
|
+
* Order is deterministic: methods in sorted order, with each method's
|
|
12
|
+
* side-effect + auth-field sub-diffs grouped together.
|
|
13
|
+
*/
|
|
14
|
+
export const diff_action_manifests = (a, b) => {
|
|
15
|
+
const diffs = [];
|
|
16
|
+
const a_by = new Map(a.methods.map((m) => [m.method, m]));
|
|
17
|
+
const b_by = new Map(b.methods.map((m) => [m.method, m]));
|
|
18
|
+
const all_methods = new Set([...a_by.keys(), ...b_by.keys()]);
|
|
19
|
+
for (const method of [...all_methods].sort()) {
|
|
20
|
+
const ea = a_by.get(method);
|
|
21
|
+
const eb = b_by.get(method);
|
|
22
|
+
if (!ea) {
|
|
23
|
+
diffs.push({ kind: 'method_only_in', where: 'b', method });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!eb) {
|
|
27
|
+
diffs.push({ kind: 'method_only_in', where: 'a', method });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (ea.side_effects !== eb.side_effects) {
|
|
31
|
+
diffs.push({ kind: 'side_effects_differ', method, a: ea.side_effects, b: eb.side_effects });
|
|
32
|
+
}
|
|
33
|
+
for (const field of AUTH_SCALAR_FIELDS) {
|
|
34
|
+
if (ea[field] !== eb[field]) {
|
|
35
|
+
diffs.push({ kind: 'auth_field_differs', method, field, a: ea[field], b: eb[field] });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const field of AUTH_LIST_FIELDS) {
|
|
39
|
+
if (!string_lists_equal(ea[field], eb[field])) {
|
|
40
|
+
diffs.push({ kind: 'auth_field_differs', method, field, a: ea[field], b: eb[field] });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return diffs;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Render a diff list as a human-readable multi-line string. Empty diffs
|
|
48
|
+
* produce an empty string.
|
|
49
|
+
*/
|
|
50
|
+
export const format_action_manifest_diffs = (diffs, labels = {}) => {
|
|
51
|
+
if (diffs.length === 0)
|
|
52
|
+
return '';
|
|
53
|
+
const label_a = labels.a ?? 'a';
|
|
54
|
+
const label_b = labels.b ?? 'b';
|
|
55
|
+
const where_label = (where) => (where === 'a' ? label_a : label_b);
|
|
56
|
+
const lines = [];
|
|
57
|
+
for (const d of diffs) {
|
|
58
|
+
switch (d.kind) {
|
|
59
|
+
case 'method_only_in':
|
|
60
|
+
lines.push(` method ${d.method} only in ${where_label(d.where)}`);
|
|
61
|
+
break;
|
|
62
|
+
case 'side_effects_differ':
|
|
63
|
+
lines.push(` ${d.method} side_effects differs: ${label_a}=${d.a}, ${label_b}=${d.b}`);
|
|
64
|
+
break;
|
|
65
|
+
case 'auth_field_differs':
|
|
66
|
+
lines.push(` ${d.method} auth.${d.field} differs: ${label_a}=${JSON.stringify(d.a)}, ${label_b}=${JSON.stringify(d.b)}`);
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
// Compile-time exhaustiveness — a new variant without a case here
|
|
70
|
+
// makes `d` non-never and fails type-check.
|
|
71
|
+
d;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return lines.join('\n');
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Throw if the two manifests disagree. The error message names the impls
|
|
79
|
+
* (via `labels`) and lists every diff, so the failure is self-diagnosing.
|
|
80
|
+
*/
|
|
81
|
+
export const assert_action_manifests_equal = (a, b, labels = {}) => {
|
|
82
|
+
const diffs = diff_action_manifests(a, b);
|
|
83
|
+
if (diffs.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
const label_a = labels.a ?? 'a';
|
|
86
|
+
const label_b = labels.b ?? 'b';
|
|
87
|
+
throw new Error(`Action-manifest parity failed: ${diffs.length} diff(s) between ${label_a} and ${label_b}\n${format_action_manifest_diffs(diffs, labels)}`);
|
|
88
|
+
};
|