Files
glm-5.3-flash f4b5f896b4 docs: sweep em dashes from all markdown prose
Renames the written-usage rule into practice across the contract surface:
docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files,
taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the
.opencode/agents/ prompts. Every replacement is a sense-preserving
rewrite (colons, semicolons, commas, parentheses, sentence splits, varied),
never a mechanical swap; no spaced hyphens were substituted. Enum fields,
frontmatter, tables, wire data, and cited identifiers survive verbatim;
en dashes inside numeric ranges keep their meaning.

Verification: taskgraph validate (43 tasks), repo grep clean (single
exempt hit: the verification command's own search pattern in AGENTS.md),
cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
2026-11-04 20:28:00 +00:00

7.1 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
w2-loopback-harness Wave 2: the loopback harness (in-process client/server test pair) completed
w2-auth-bridge
w1-test-doubles
moderate medium phase implementation

Description

Land the in-process client/server test pair every later contract test runs through: the decline matrix, every mapping's contract tests (wave 3), and the client-direction smokes all drive the crate through this harness. In-memory doubles from wave 1; real-tool smokes ride the mapping waves (wave 3+) and the e2e wave (wave 5).

  • Server side: the SshAdapter from w2-server-mount over an in-process transport. The transport choice (tokio duplex pair vs 127.0.0.1:0 TCP loopback) is pinned here; both work with run_stream/russh's client connect; decide on determinism and portability and record the choice.
  • Client side: a russh client driven directly (the wave-4 dial surface does not exist yet; the harness builds the russh client config from the doubles): in-memory key from the ed25519 fixtures (FIXTURE_KEYS), the MemHostKeyPolicy double with the permissive test posture (with_default(true); the accept-all posture exists only as a test choice, never a crate built-in), the keepalive posture from Limits.
  • Harness helpers (the vocabulary the contract tests speak): authenticate with a fixture key (and observe auth failure shapes), open a channel type (and observe the open-failure reason), send a session request (and observe success/failure), await exit-status / close. Helpers stay thin: the russh client API is the vocabulary; the harness removes boilerplate, not semantics.
  • Convention: lives with the wave-1 doubles under tests/common/ (or a sibling harness module, same convention, decide and record); documented as the contract-test vehicle for the crate.

The harness's first consumer is its own acceptance test: a full in-process connect → authenticate (fixture key through MemIdentityProvider) → authenticated-session observation, proving the wave-1 doubles work live through the wave-2 stack.

Acceptance Criteria

  • In-process server/client pair connects and authenticates end to end (fixture key → MemIdentityProvider → authenticated session) with no TCP port dependency (or the TCP choice recorded, if taken)
  • Harness helpers cover: auth (success + failure observation), channel open (success + open-failure reason), session requests (success/failure), exit-status/close observation
  • The transport choice and module placement are recorded
  • The harness is documented as the contract-test vehicle; the wave-1 doubles are exercised live through it
  • cargo test, cargo clippy --all-targets -- -D warnings, cargo fmt --check pass

References

  • docs/plans/implementation.md §Wave 2 (the loopback-harness bullet)
  • docs/architecture/mapping.md §Decline matrix (the rows the harness will drive)
  • docs/research/poc-1-auth-bridge.md (the decline-observability facts the helpers must be able to observe)
  • tests/common/mod.rs (the wave-1 doubles this harness drives)

Notes

  • Transport decision (recorded, in tests/common/harness.rs module docs): tokio duplex pair (64 KiB/direction) + russh's connect_stream. russh's real codec/kex/transport run over the pair verbatim; the one gap versus real sockets (no OS-socket semantics) is covered more cheaply by the real-tool smokes of wave 3+, since TCP loopback's listener lifecycle and timing flakiness buy nothing the harness needs. Deterministic, no port, no collision.
  • Placement decision (recorded, same docs): tests/common/harness.rs, a sibling module of the wave-1 doubles reached with the same mod common; convention; test surface, never a public API export. Unlike the doubles it is a vehicle convention: later waves call common::harness::drive rather than re-deriving a mount/connect pair.
  • russh 0.65 behavior surfaced by the harness (a dispatch-core input): each un-implemented server channel_open_* hook drops its ChannelOpenHandle → the client observes ChannelOpenFailure::AdministrativelyProhibited, the inherited fail-closed decline on the current skeleton stack. The harness's authenticated-session row matches the reason generically (Error::ChannelOpenFailure(_)), so w2-dispatch-decline-core's reason rows cannot break the vehicle.
  • Scope posture: the harness is a test-vehicle task, so no src/ change. A russh-server behavior note rides the row docs only.

Summary

Landed the loopback harness (tests/common/harness.rs, wired through tests/common/mod.rs's pub mod harness), with both pinned decisions recorded in its module docs: tokio duplex pair as the in-process transport (russh::client::connect_stream against the mounted SshAdapter::handle; recorded over TCP loopback for determinism and no-port portability) and sibling-of-the-doubles placement under tests/common/ as the crate's contract-test vehicle convention.

Contents:

  • HarnessClientHandler: the russh client handler bridging the MemHostKeyPolicy double into check_server_key, computing the decision input ([PresentedHostKey]) exactly as the wave-4 dial wiring will: endpoints labeled LOOPBACK_HOST / LOOPBACK_PORT (the no-port marker 0), the fingerprint on the crate's single fingerprint path.
  • client_config: the keepalive/wall-clock posture from Limits (interval/max verbatim, inactivity always set).
  • Server-side assemblers: serve_config / adapter from the wave-1 doubles.
  • drive → LoopbackSession: the mount-spawn + connect pair, with clean-disconnect unmount.
  • Auth helpers: authenticate_publickey (fixture plumbing removed, AuthResult verbatim) and remaining_methods (the advertisement half of the failure shape).
  • Channel helpers: open_session_channel (open-failure reason observed in the Err), request_exec / request_shell / request_subsystem, and the message observations: next_message, await_exit_status, await_close.

Acceptance proof (tests/loopback_harness.rs, 6 rows, every row a Contract stamp:): full in-process connect → fixture-key auth through MemIdentityProvider → authenticated-session observation (host-key decision log carrying bravo's fingerprint under the loopback labels; a channel open answered with an observed decline reason); the auth failure shape with {publickey} remaining; the fail-closed host-key verdict as a clean dial decline; the pinned-rule trust-anchor case; and the Limits → client-config posture row.

Live-exercised doubles: MemIdentityProvider, MemHostKeySource, MemHostKeyPolicy (both verdicts). TestVault stays on the custody rows (fixture keys are unencrypted by contract). The session-request and exit-status/close helpers are exercised by construction this wave, because channel opens don't confirm on the decline-only skeleton; their live rows arrive with w2-dispatch-decline-core.

Verification: cargo test (81 passed across 6 binaries), cargo clippy --all-targets -- -D warnings, cargo fmt --check; all green.