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

146 lines
7.1 KiB
Markdown

---
id: w2-loopback-harness
name: "Wave 2: the loopback harness (in-process client/server test pair)"
status: completed
depends_on: [w2-auth-bridge, w1-test-doubles]
scope: moderate
risk: medium
impact: phase
level: 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
- [x] 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)
- [x] Harness helpers cover: auth (success + failure observation),
channel open (success + open-failure reason), session requests
(success/failure), exit-status/close observation
- [x] The transport choice and module placement are recorded
- [x] The harness is documented as the contract-test vehicle; the
wave-1 doubles are exercised live through it
- [x] `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.