- the harness lands in src/ops.rs's test tree (#\[cfg(test)] under feature ops): serving assembly (OperationRegistryBuilder + CallAdapter run loop) and client side (CallClient + spawn_dispatch; ChannelClient) - the loopback connection source: Connection::from_source with a pre-wired multi-stream BidiStreamSource — the QUIC-shaped in-memory stand-in stream-per-request needs (a single from_bidi half cannot serve spawn_dispatch's call path) - the channel shape: ChannelsAdapter install hook forks the assembled registry, registers the stub openable through register_openable_with_establisher (the awaited Establishment phase), dispatches via run_loop_single_stream - the identity/ACL wiring: TestIdentityProvider (token → identity) + TestOwnership (namespace → identities → granted actions, closed-by-default per OQ-09); call shape rides auth_token resolution, channel shape rides the transport-identity seam - proven with the stub ops: JSON round-trip call → envelope; the ACL stub denies anonymous/ungranted and admits granted; the channel stub establishes and bytes flow both directions; the refusal arms prove ACL-before-establishment and the channel:open_failed/unknown_resource reply shape - dev-dep: async-trait 0.1 (the BidiStreamSource impl); lib tree unchanged verified: cargo test --features ops (88/19/3 green), cargo test (default, harness compiled out — 73/19/3 green), cargo clippy --all-targets -- -D warnings on both shapes, cargo fmt --check, cargo doc --no-deps clean
172 lines
9.2 KiB
Markdown
172 lines
9.2 KiB
Markdown
---
|
|
id: ops-call-harness
|
|
name: the in-process alkcall harness — the serving assembly and the client side over one duplex pair
|
|
status: completed
|
|
depends_on: [ops-foundation]
|
|
scope: narrow
|
|
risk: low
|
|
impact: component
|
|
level: implementation
|
|
tags: [wave-7, ops, testing, harness, alkcall]
|
|
---
|
|
|
|
## Description
|
|
|
|
The test harness every ops handler task exercises its handlers
|
|
through: an in-process alkcall **serving side + client side over one
|
|
`tokio::io::duplex` pair** — the embedder's "dials and hands an
|
|
established `Connection` in" shape (ops-surface.md §Why alkcall;
|
|
alkcall is a pure protocol crate), realized in-process so the ops
|
|
surface is testable server-less.
|
|
|
|
- **The serving assembly** (the embedder-assembly pattern realized in
|
|
test): `OperationRegistryBuilder` receiving the ops family's
|
|
spec/handler pairs → the dispatch serving stack (`CallAdapter` /
|
|
`GatewayDispatch`, plus `ChannelsAdapter` for the binary ops'
|
|
`channel_open` path) → the dispatch run loop over a
|
|
`Connection::from_bidi` half (alkcall ADR-009's public stream
|
|
constructor). The assembly's shape is the module's documented
|
|
embedder entry — the "module exports spec/handler pairs; the
|
|
embedder assembles" placement (ops-surface.md's placement note,
|
|
the alkgit `git/repo/*` pattern).
|
|
- **The client side**: `CallClient` + `spawn_dispatch` over the
|
|
duplex pair's other half; `ChannelClient` (`from_connection`,
|
|
`open_channel`/`call_open_op`) for the channel ops.
|
|
- **The identity/ACL wiring**: a test `IdentityProvider` +
|
|
`OwnershipProvider` — the embedder-registry stand-in (namespace →
|
|
identities → granted actions) — so ACL behavior is exercisable from
|
|
the first handler task onward, including the closed-by-default
|
|
denial path (OQ-09).
|
|
- **Proven with a stub op**: a trivial stub op registered through the
|
|
same path round-trips (call → response envelope), and the channel
|
|
path opens with bytes flowing — the pipe proven before any real op
|
|
rides it. The stub lives in the test tree; it registers nothing
|
|
into the shipped module surface.
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [x] The harness lives in the ops module's test tree
|
|
(`#[cfg(test)]` under feature `ops`), parameterized over the
|
|
assembled registry (ops register; the harness serves)
|
|
- [x] Server + client halves over one duplex pair; the dispatch run
|
|
loop serves; the client calls and receives envelopes
|
|
- [x] The identity/ownership wiring exercisable: a stub op carrying
|
|
an `AccessControl` denies an ungranted identity and admits a
|
|
granted one (the ACL path proven end-to-end pre-handler)
|
|
- [x] The channel path proven: a stub `channel_open` op establishes
|
|
(the `Establishment` phase) and bytes flow both directions
|
|
- [x] `cargo test --features ops` green; the default build unaffected
|
|
(the harness is feature-gated test code);
|
|
`cargo clippy --all-targets -- -D warnings` and
|
|
`cargo fmt --check` green on both shapes
|
|
|
|
## References
|
|
|
|
- docs/architecture/ops-surface.md (§Why alkcall — the Connection
|
|
hand-in; the placement note)
|
|
- docs/architecture/decisions/001-substrate-posture-and-ops-placement.md
|
|
§2 (the machinery inventory this harness assembles)
|
|
- alkcall (upstream checkout `/workspace/@alkdev/alkcall`):
|
|
`src/registry/registration.rs` (`OperationRegistryBuilder`, the
|
|
handler kinds), `src/protocol/adapter.rs` (`CallAdapter`),
|
|
`src/protocol/connection.rs` (`Connection::from_bidi`),
|
|
`src/channels/adapter.rs` (`ChannelsAdapter`),
|
|
`src/channels/client.rs` (`ChannelClient`), ADR-047/049 (the
|
|
channel_open marker + establishment)
|
|
- tasks/ops-foundation.md (the context the harness's assembled
|
|
handlers capture)
|
|
|
|
## Notes
|
|
|
|
> Landed as a `#[cfg(test)] mod harness` appended to
|
|
> `src/ops.rs` (the ops module's test tree — feature-gated by
|
|
> construction, the default build compiles it out; the shipped module
|
|
> surface is untouched). All wiring is alkcall's public API:
|
|
>
|
|
> - **The loopback connection source** (`LoopbackSource` + `Connection::from_source`,
|
|
> the ADR-008 extension point): the QUIC-shaped in-memory stand-in the
|
|
> top-level `alk/call` shape needs — `open_bi` hands out pre-wired
|
|
> stream pairs (one `tokio::io::duplex` pair per stream, joined via
|
|
> the public `BiStream::from_joined`), `open_bi` at exhaustion answers
|
|
> `StreamClosed`, `accept_bi` at idle parks (an open connection with
|
|
> no inbound streams), `close` drains. This was the one place the
|
|
> task text needed a ruling: **`spawn_dispatch` + stream-per-request
|
|
> cannot complete a call over a single `from_bidi` duplex end**
|
|
> (the accept loop consumes the one stream and `call()`'s `open_bi`
|
|
> then fails; alkcall's own e2e shape is the same pre-wired
|
|
> `BidiStreamSource` stand-in, `pub(crate)` there — reimplemented
|
|
> here as the documented downstream seam, N streams wide so the
|
|
> client can call repeatedly). The channel shape uses
|
|
> `Connection::from_bidi` halves directly (the channels protocol
|
|
> multiplexes internally).
|
|
> - **The serving assembly**: `OperationRegistryBuilder` receiving the
|
|
> stub ops family's spec/handler pairs (`with_local`) →
|
|
> `CallAdapter::new(...).with_ownership_provider(...)` →
|
|
> `ProtocolHandler::handle` (the dispatch run loop) over its
|
|
> `from_source` half; client side
|
|
> `CallClient::new(...).spawn_dispatch(...)` over the other.
|
|
> `GatewayDispatch` (alkcall's `gateway`-gated transport-neutral
|
|
> spine) was not wired — no gateway front-end rides the duplex; the
|
|
> wire-serving stack is the `CallAdapter` half.
|
|
> - **The channel assembly**: `ChannelsAdapter::new(install_hook,\ NoCap)`
|
|
> — the install hook (the ADR-036 amendment shape): sets the
|
|
> transport-authenticated identity on channel 0's `Connection`
|
|
> (`Connection::set_identity`, the CF-005 (b) path), forks the
|
|
> assembled registry (ADR-047 §4's per-connection registration), and
|
|
> registers the stub openables through
|
|
> `ChannelCore::register_openable_with_establisher` — the awaited
|
|
> **Establishment phase** (establisher records its invocation,
|
|
> admits `stub-ns` only, returns `Establishment::default()`; refusal
|
|
> → `channel:open_failed`/`unknown_resource`) — then
|
|
> `Dispatcher::run_loop_single_stream` over
|
|
> `CallConnection::new_single_stream`. Client side:
|
|
> `ChannelClient::from_connection`, `open_channel` for the binary
|
|
> data plane, `call_open_op` for the refusal arms.
|
|
> - **The identity/ACL wiring** (`TestIdentityProvider` — token →
|
|
> identity — and `TestOwnership` — namespace → identities → granted
|
|
> actions, closed by default): the call shape exercises the
|
|
> `auth_token` → `IdentityProvider` route (payload `auth_token` via
|
|
> `call_with_payload`, alkcall ADR-017 §7's precedence); the channel
|
|
> shape exercises the transport-identity route (identity set on
|
|
> channel 0's connection in the install hook). Both resolve through
|
|
> the dispatch's `AccessControl::check` with the ownership provider
|
|
> in context — the ops surface's namespace-as-resource mapping
|
|
> (`blob-namespace`, action `read`, `resource_id_path: "$.namespace"`),
|
|
> never a re-implemented check (convention 6).
|
|
> - **The stubs**: `blobs/stub-echo` (no ACL, echo), `blobs/stub-ns-probe`
|
|
> (the ACL op above), `channels/stub/sub` (the channel_open op, over
|
|
> the `alk/stub` data-plane ALPN, whose pump reads one chunk, records
|
|
> it, and echoes `b"pong"` — the R-02 duty in one act). All live in
|
|
> the test tree; nothing registers into the shipped surface.
|
|
>
|
|
> One dev-dependency entered: `async-trait = "0.1"` (the
|
|
> `BidiStreamSource` impl's annotation; cargo dedupes to alkcall's
|
|
> version). The lib dependency tree is unchanged.
|
|
|
|
## Summary
|
|
|
|
> Completed 2026-10-13 (wave 7). The in-process alkcall harness landed in
|
|
> `src/ops.rs`'s test tree: two wire shapes over in-memory connections —
|
|
> the call shape (stub JSON ops registered through
|
|
> `OperationRegistryBuilder`; serving side `CallAdapter` with the
|
|
> identity + ownership providers wired; client side `CallClient` +
|
|
> `spawn_dispatch` over the loopback pair's stream-per-request source)
|
|
> and the channel shape (stub openable registered per-connection through
|
|
> `ChannelCore::register_openable_with_establisher` — the awaited
|
|
> Establishment phase; serving side `ChannelsAdapter`'s install hook
|
|
> forking the assembled registry and running `run_loop_single_stream`;
|
|
> client side `ChannelClient::from_connection` + `open_channel`).
|
|
> Proven by the stub ops: the JSON stub round-trips call → response
|
|
> envelope; the ACL stub (namespace = `blob-namespace` resource) denies
|
|
> anonymous and ungranted identities (closed-by-default, OQ-09) and
|
|
> admits the granted one; the channel stub establishes and bytes flow
|
|
> both directions (`ping`/`pong`); the refusals arms prove the ACL gate
|
|
> runs before the establisher and that an establisher refusal answers
|
|
> `channel:open_failed`/`unknown_resource` through the typed
|
|
> `ChannelOpenError`. Four harness tests green (lib 88 with `--features
|
|
> ops`, 73 default; all conformance/interop columns unchanged). Gates
|
|
> green: `cargo test --features ops`, `cargo test` (default, harness
|
|
> compiled out), `cargo clippy --all-targets -- -D warnings` on both
|
|
> shapes, `cargo fmt --check`, `cargo doc --no-deps` clean. The
|
|
> handler tasks (ops-json-ops, ops-fetch, ops-put) now register their
|
|
> spec/handler pairs through `OpsHarness` instead of inventing wiring. |