Cross-process role-grant-offer lifecycle WS notification suite — the
machinery proof for the consentful-role-grants notification fan-out across
any spine backend. Covers all seven server-initiated notifications:
role_grant_offer_received → recipient (offer created)role_grant_offer_accepted → grantor (recipient accepts)role_grant_offer_declined → grantor (recipient declines)role_grant_offer_retracted → recipient (grantor retracts)role_grant_revoke (flat, omits revoked_by) → revokee (active grant revoked)role_grant_offer_supersede → each superseded sibling's grantor, fired on
BOTH the accept-cascade (reason: 'sibling_accepted') and the
revoke-cascade (reason: 'role_grant_revoked')
These exercise only spine primitives (accounts, role-grants, offers, WS
notifications) — zero consumer domain — so the suite lives here and runs
against any backend that wires the standard RPC actions' notification_sender
and mounts a registered WS socket: fuz_app's own spine self-tests
(testing_spine_server + the Rust testing_spine_stub) and downstream
twin-impl consumers (the fuz_forge Deno/Hono + Rust fuz_forge_server
backends) alike.
Each case is a *targeted* server-initiated notification (vs the broadcast in
a repo_updated-style suite), so it opens the affected counterparty's socket,
drives the lifecycle RPC over HTTP, then asserts the frame lands on that
socket and strict-parses against the canonical wire schema — the guard
against serialization drift (field / null / datetime / the flat revoke shape /
the supersede reason + cause_id).
Sends are queued on the post-commit drain (handler-emit, not audit-derived),
so a frame may land a beat after the RPC resolves — WsClient.wait_for polls
already-received messages then waits, absorbing the fan-out latency without a
sleep, and its method+predicate filter ignores unrelated frames (e.g. the
received push the recipient also gets). ROLE_ADMIN is the only
admin-grantable role; accounts that already hold it can still be offered it
again (a fresh pending row — the prior accept is terminal, no already-granted
guard), and accept stays idempotent on the role_grant while still superseding
pending siblings. Gated on capabilities.ws.
Cross-process only: create_ws_transport needs a real bound socket, so wire
it from a *.cross.test.ts file, never an in-process setup. Authed cookies
come from the per-account session minted by fixture.create_account /
fixture.create_session_headers.