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.
146 lines
7.1 KiB
Markdown
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. |