--- id: w1-test-doubles name: "Wave 1: test doubles (the in-memory seams)" status: completed depends_on: [w1-error-model, w1-key-sources, w1-host-key-policy] scope: narrow risk: low impact: component level: implementation --- ## Description Establish the test-double convention (adapted from the sibling crates) and land the in-memory seams every later wave's contract tests run against: - **In-memory `IdentityProvider`**: registered fingerprints → `Identity` resolutions over alkcall's trait; the double the auth bridge and dispatch tests resolve against. - **In-memory vault seam**: the encrypt/decrypt round-trip plus the failure postures (locked / absent / tampered) behind the same seam the key sources consume, so the custody tests run without a live vault service. If alkvault 0.1 ships an in-memory construction, wrap it; otherwise a local test double behind the source trait (the task pins which, flagged for the wave-1 gate). - **In-memory `HostKeyPolicy`**: a configurable record/verdict double (accept/reject per host, decision log inspectable) for the dial and acceptor tests. - **ed25519 test-key fixtures**: throwaway test keys (generated at test time or committed static PEMs, with the task pinning which; committed keys are throwaway fixtures, not secrets) with recorded `ssh-keygen -lf` fingerprints, shared with the fingerprint parity tests and every later auth/mapping test. The convention: doubles live in-crate (test module or `#[cfg(test)]` utility module per the sibling posture), every protocol wave's gate runs its machines against them, and no double leaks into `src/` non-test code. ## Acceptance Criteria - [ ] In-memory `IdentityProvider`, vault seam, and `HostKeyPolicy` doubles in place behind their production seams - [ ] ed25519 test-key fixtures with recorded fingerprints available to the test suite - [ ] The doubles exercise the wave-1 seams end to end (a round-trip test: register fingerprint → resolve; encrypt → source → decode; policy verdict honored) - [ ] No double code outside test-gated compilation - [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check` pass ## References - docs/architecture/decisions/004-auth-dispositions.md (the `IdentityProvider` surface) - docs/architecture/decisions/005-at-rest-key-custody.md (the custody seam the vault double backs) - docs/research/poc-2-vault-keys.md (the failure postures the vault double reproduces) - The alkgit test-double convention (tasks/implementation/core/ w1-test-doubles.md; reading reference, never copied code) ## Notes - **Vault-double shape (recorded, flagged for the wave-1 gate):** alkvault 0.1 ships an in-memory construction: `VaultServiceHandle::new()` is an in-process vault that `unlock_new` arms in-memory. The double is a **wrapper** (`TestVault` in `tests/common/mod.rs`) around that construction, not a local re-impl behind the source trait: the real decrypt taxonomy, GCM integrity behavior, and rotation semantics are exercised verbatim through the production `VaultKeySource` / `VaultHostKeySource` seams, and the POC-2 §3 postures (locked / absent / tampered / wrong seed) ride the real seams. - **Fixture decision (recorded):** committed static OpenSSH-PEM test keys, minted by `ssh-keygen` at vector-generation time, not generated at test time (determinism, no tooling dependency). Committed private halves are throwaway fixtures by the task's own posture, documented as such in `tests/vectors/fingerprints.txt` and in the common module docs. - **Location decision (recorded):** the doubles live only under `tests/common/mod.rs`, reached with `mod common;` (the alkgit convention, adapted). Nothing doubles-related is reachable from `lib.rs`; no feature gates them. `src/`-unit tests that need the fixture material read the committed *files* via `include_str!` (`src/key_source.rs` tests), never a copy; the single fixture set is shared by the src-side and tests/-side suites. dead_code is allowed module-wide in `tests/common/` (each test binary compiles the whole shared module and never exercises all of it). - **The fixture set was unified and re-minted (flagged for the gate):** the previous state had two overlapping throwaway sets (public-only vectors under `tests/vectors/`, used for fingerprint parity, and inline private-PEM constants in `src/key_source.rs`), and the inline PEM constants, while decodable by russh, were **rejected by OpenSSH itself** (`ssh-keygen` → "error in libcrypto"): unusable as cross-issuer fixtures. All four keys (`alpha`/`bravo`/`charlie`/ `delta`) were freshly minted with OpenSSH_9.6p1's ssh-keygen, their fingerprints recorded byte-verbatim in `tests/vectors/fingerprints.txt`, and the two sets unified into one fixture table, `FIXTURE_KEYS` (committed `.key` + `.pub` files, served to the fingerprint parity tests, the vault round-trip, and every later auth/mapping test. The old vector-only set was retired (its keys had no private halves). - The row/stamp discipline is established in `tests/common/mod.rs`'s module docs (adapted from the sibling crates): rows state properties and carry greppable `Contract stamp:` items citing the pinning ADR §, and `grep -rn "Contract stamp:" tests/` is the inventory (11 stamps across the 11 landed rows). - No `src/` non-test changes: `src/key_source.rs` changed only inside its `#[cfg(test)]` module (fixture `include_str!`s + one parity row); `tests/common/mod.rs` and `tests/doubles.rs` are the new surfaces. ## Summary The wave-1 test-double convention landed: `tests/common/mod.rs` carries `MemIdentityProvider` (registered fingerprints → alkcall `Identity` resolutions; the token path resolves none (ADR-004's fingerprint-only disposition)), `TestVault` (a wrapper over alkvault 0.1's shipped in-memory construction, the pinned resolution of the flagged vault-double question), `MemHostKeyPolicy` (exact `(host, fingerprint)` rules plus an explicit default verdict; fail-closed by default and permissive only by explicit choice, with a fully inspectable decision log), and the unified ed25519 fixture table (`FIXTURE_KEYS`: four committed `ssh-keygen`-minted OpenSSH-PEM key pairs with recorded `ssh-keygen -lf` fingerprints in `tests/vectors/fingerprints.txt`). The end-to-end rows in `tests/doubles.rs` run the seams through the production surfaces: register → resolve, seal → source → decode (round-trip and the locked/absent/tampered postures), verdict → honored-and-logged. `src/key_source.rs` and `tests/fingerprint_parity.rs` were moved onto the shared fixture files (one fixture set, no copies; the previous inline PEMs were discovered to be OpenSSH-rejected and were replaced by freshly minted keys). All doubles live in test-only compilation (`tests/`), with nothing doubles-related reachable from the public API. Verified: `cargo test` (33 + 9 + 5 green; also `--all-features`), `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check` clean.