Files
alkblobs/tasks/ops-call-harness.md
T
glm-5.3-flash bc6da37e35 feat(tests): the in-process alkcall harness — the serving assembly and the client side over one duplex pair (wave 7)
- 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
2026-10-13 00:04:09 +00:00

9.2 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
ops-call-harness the in-process alkcall harness — the serving assembly and the client side over one duplex pair completed
ops-foundation
narrow low component implementation
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

  • 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)
  • Server + client halves over one duplex pair; the dispatch run loop serves; the client calls and receives envelopes
  • 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)
  • The channel path proven: a stub channel_open op establishes (the Establishment phase) and bytes flow both directions
  • 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.