# Peer CLI P2P Scenarios This matrix tracks the headless peer-to-peer contract exercised through `lanspread-peer-cli`. It intentionally avoids the GUI and uses direct connect for deterministic local runs; mDNS/macvlan remains an environment smoke path. ## Scenario Matrix | ID | Scenario | Setup | Expected result | | --- | --- | --- | --- | | S1 | Startup scan | Start one peer with `fixture-alpha`. | Peer emits `local-peer-ready` and `local-library-changed`; catalog fixture games are `downloaded=true`, `installed=false`, `availability=Ready`. | | S2 | Direct connect handshake | Start alpha and bravo, send alpha `connect` to bravo's ready address. | Both peers record one remote peer, no self-peer entry appears, and each peer receives the other's library. | | S3 | Remote aggregation | Empty client connects to alpha and bravo. | `list-games` shows remote-only games once; shared `ggoo` has `peer_count=2`, unique games have `peer_count=1`. | | S4 | Single-source download, no install | Empty client connected to bravo downloads `bfbc2` with `install=false`. | Client emits `got-game-files`, `download-begin`, `download-finished`, then local `bfbc2` is `downloaded=true`, `installed=false`; root files exist and `local/` does not. | | S5 | Auto-install download | Empty client connected to bravo downloads `cnctw` with default install. | Download finishes, install begins and finishes, and local `cnctw` is `downloaded=true`, `installed=true` with `local/fixture-payload.txt`. | | S6 | Manual install and uninstall | After S4, client sends `install bfbc2`, then `uninstall bfbc2`. | Install marks `bfbc2` installed and creates `local/`; uninstall removes `local/` while preserving downloaded root files. | | S7 | Duplicate-source majority download | Empty client connects to alpha and bravo, then downloads shared `ggoo`. | Metadata from both peers validates by majority/plurality, download completes once, and installed state matches the install flag. | | S8 | Ambiguous metadata rejection | Two peers advertise the same game/version with conflicting file sizes. | Download fails with a `download-failed` event; no committed `version.ini` is left for the target game. | | S9 | Missing game | Client asks for a game none of its peers can serve. | CLI reports a deterministic command failure and emits `no-peers-have-game`; no local files are created. | | S10 | Shutdown and goodbye cleanup | Alpha and bravo are connected, then bravo shuts down. | Alpha receives peer loss/removal and remote games from bravo disappear. | | S11 | Same identity reconnect | Bravo restarts with the same state dir but a new port, then alpha connects to the new address. | Alpha has one bravo peer entry with the updated address, not duplicate identities. | | S12 | Transfer serving gates | A peer has a non-catalog, missing-sentinel, active-operation, or `local/` path request. | The serving peer declines metadata/data; covered by unit tests where timing is too small for a stable CLI race test. | | S13 | Exact transferred-file equality | Repeat small and large downloads, then compare every transferred regular file against its source with SHA-256 manifests. | Source and receiver manifests match exactly for each transferred file; no extra or missing files appear in the downloaded game root. | | S14 | Large multi-peer chunked download | `fixture-alpha/alienswarm` contains a renamed RAR `.eti` larger than 100 MB. A second peer downloads it, then a third peer downloads `alienswarm` from both peers. | The third peer's downloaded files match the source by SHA-256; `download-chunk-finished` events show the large `.eti` chunks coming from both peers with byte counts balanced within one chunk. | | S15 | Three-way version skew | Three peers advertise the same catalog game ID. Peer A has `version.ini=20250101`, peer B has `version.ini=20250201`, and peer C has `version.ini=20250301`; each version has distinguishable file contents. An empty client connects to all three and downloads the game with `install=false`. | `list-games` shows one row for the game with `peer_count=3` and `eti_game_version=20250301`. The `got-game-files` descriptor set and transfer source are peer C's newest version only; no chunks come from A or B. The receiver's `version.ini` and SHA-256 manifest match C exactly. | | S16 | Latest-version fanout with stale peers present | Peer A has an older version of a game. Peers B and C both advertise the same newest version with matching file manifests; use a large file when proving chunk split. | The aggregated row still counts all ready peers, but eligible transfer peers are only B and C. Large-file chunks may split between B and C; peer A contributes no manifest majority vote and no file chunks. | | S17 | Latest-version conflict rejection | Peer A has an older version. Peers B and C both advertise the newest version, but their latest-version file sizes conflict. | Validation considers only the latest-version peers, so A cannot rescue the majority. The download fails with `download-failed`, and no committed target `version.ini` remains. | ## Version-Skew Contract Use S15-S17 to pin down what "newer" means when several peers have the same game ID: - Version comparison uses the eight-digit `version.ini` string, so use sortable `YYYYMMDD` values in manual fixtures. - `list-games` aggregates by game ID. The game appears once; `peer_count` counts all ready peers with that ID, including peers that only have older versions. - The aggregated `eti_game_version` must be the newest ready version. - The descriptor set emitted to the download path, file-size validation, and transfer planning are latest-only. Older-version peers may be queried by a generic detail request, but their descriptors must not supply download descriptors, majority votes, or chunks once a newer version exists. - If exactly one peer has the latest version, that peer is the only transfer source. If several peers tie on the latest version, validation and chunk fanout happen among that latest-version set only. - Capture proof with the `list-games` row, `got-game-files` descriptors, `download-chunk-finished` source addresses, and source/receiver SHA-256 manifests. For a manual run, prefer a catalog game ID already served by the fixture lab, such as `cnc4`, then create temporary `just peer-cli-run` game roots with different `version.ini` contents. The existing alpha/bravo/charlie fixtures cover duplicate-source and shared-game cases, but not the three-version skew until a dedicated fixture or temporary games root is prepared. ## Run Log ### 2026-05-18 - Full Matrix Manual Docker Pass - Build/setup: `just peer-cli-image` passed. Local `just peer-cli-build` needed `RUSTC_WRAPPER=` because the host `kache` wrapper failed with a read-only filesystem error; `RUSTC_WRAPPER= just peer-cli-build` passed. - Temporary skew/conflict fixtures were created under the ignored `.lanspread-peer-cli/full-fixtures/` tree using `rar a -idq -m0` against `/dev/urandom` payloads and then renaming the archives to `.eti`. `find .lanspread-peer-cli/full-fixtures -name '*.eti' -exec unrar t -idq {} \;` passed. - S1 startup scan: `just peer-cli-alpha` emitted `cli-started`, `local-library-changed`, and `local-peer-ready`; `alienswarm`, `bf1942`, and `ggoo` were `downloaded=true`, `installed=false`, `availability=Ready`. - S2 clean direct connect: with only alpha and bravo running, alpha connected to bravo at `10.66.0.3:42776`; `wait-peers` returned `peer_count=1`, and `list-peers` showed exactly one bravo peer with four games. - S3 clean remote aggregation: an empty `clean-s3-client` saw exactly alpha and bravo. `list-games` showed `ggoo peer_count=2`; `alienswarm`, `bf1942`, `bfbc2`, `cnc4`, and `cnctw` each had `peer_count=1`. - S4 single-source no-install: `full-empty-client` downloaded `bfbc2` from bravo with `install=false`. Events included `got-game-files`, `download-begin`, `download-finished`, and local `installed=false`. Host verification: `diff -r crates/lanspread-peer-cli/fixtures/fixture-bravo/bfbc2 .lanspread-peer-cli/full-empty-client/games/bfbc2` passed and `local/` was absent. - S5 auto-install: `full-empty-client` downloaded `cnctw` with default install. Events included download finish, `install-begin`, and `install-finished`; `local/fixture-payload.txt` existed. Host verification diffed the downloaded files against `fixture-bravo/cnctw` excluding `local/` and `.lanspread.json`. - S6 manual install/uninstall: after S4, `install bfbc2` created `local/` and marked `installed=true`; `uninstall bfbc2` removed `local/` and preserved the downloaded root files. Host verification diffed the preserved files against `fixture-bravo/bfbc2` excluding `.lanspread.json`. - S7 duplicate-source download: `full-empty-client` downloaded shared `ggoo` from alpha/bravo with `install=false`. Chunk events used alpha for `version.ini` and bravo for `ggoo.eti`; host `diff -r` matched both `fixture-alpha/ggoo` and `fixture-bravo/ggoo`. - S8 ambiguous metadata rejection: `full-s8-a` and `full-s8-b` both advertised `ggoo` version `20260101` but with different `.eti` sizes (`1,048,746` and `2,097,323` bytes). The client saw `peer_count=2`, then `download ggoo` emitted `download-failed`; no target `ggoo/version.ini` was committed. - S9 missing game: `download does-not-exist` emitted `no-peers-have-game` and returned a command error; `.lanspread-peer-cli/full-empty-client/games` had no `does-not-exist` directory. - S10 shutdown cleanup: alpha saw bravo before shutdown with one remote peer and bravo-only remote games. After bravo `shutdown`, alpha emitted `peer-lost`; `list-peers` returned `[]` and `list-games` returned an empty remote list. - S11 same identity reconnect: restarting bravo reused peer ID `019e347d901e70c19adf5b9fd313fce4` at new address `10.66.0.3:41764`. Alpha `list-peers` showed exactly one bravo entry at the new address. - S12 transfer serving gates: this remains covered by unit tests because the CLI cannot stably race raw transfer requests against non-catalog, missing sentinel, active-operation, and `local/` path states. `RUSTC_WRAPPER= just test` passed, including `local_download_available_gates_on_catalog_operation_and_sentinel`, `get_game_response_respects_serve_gates`, `file_transfer_dispatch_respects_serve_gates`, and `local_relative_paths_are_never_transferable`. - S13 exact transferred-file equality: the S4 small transfer and S14 large transfer both passed host `diff -r` against the original source game directories, proving exact file equality beyond event flow. - S14 large multi-peer chunked download: `full-empty-client` first downloaded `alienswarm` from alpha and diffed cleanly against `fixture-alpha/alienswarm`. A fresh `full-s14-client` then saw `alienswarm peer_count=2` and downloaded from both alpha and `full-empty-client`. Large `.eti` chunk totals were `67,108,864` bytes from alpha and `58,721,049` bytes from the staged peer, balanced within one `32 MiB` chunk. Final host `diff -r` against `fixture-alpha/alienswarm` passed. - S15 three-way version skew: peers A/B/C advertised `cnc4` versions `20250101`, `20250201`, and `20250301`. The client saw one row with `peer_count=3` and `eti_game_version=20250301`; all chunks came only from C at `10.66.0.4:60290`. Host `diff -r` against C passed. - S16 latest-version fanout with stale peer present: A advertised stale `20250101`; B/C both advertised latest `20250301` with a `134,217,906` byte `.eti`. The client saw `peer_count=3`; chunks came only from B/C (`67,108,873` and `67,109,042` bytes respectively), with stale A contributing zero. Host `diff -r` matched both B and C. - S17 latest-version conflict rejection: A advertised stale `20250101`; B/C both advertised latest `20250301` but with conflicting `.eti` sizes (`1,048,748` and `2,097,325` bytes). The client saw `peer_count=3` and latest `20250301`, then `download cnc4` emitted `download-failed`; no target `cnc4/version.ini` was committed. - Gates after manual runs: `just fmt`, `RUSTC_WRAPPER= just test`, and `RUSTC_WRAPPER= just clippy` passed. ### 2026-05-17 - Exact Transfer And Large Multi-Peer Chunking - Fixture update: `fixture-alpha/alienswarm/alienswarm.eti` was rebuilt with `rar a -idq -m0` from three random 40 MiB payload files, then renamed to `.eti`. Final archive size: `125,829,913` bytes. `unrar t -idq` passed. - Gates before manual runs: `just fmt`, `just test`, `just peer-cli-build`, `just clippy`, and `just peer-cli-image` passed. - S13 small exact transfer: `deep-small-client` downloaded `bfbc2` from `fixture-bravo` with `install=false`. SHA-256 manifests matched exactly: `bfbc2/bfbc2.eti` `f7accef0833f29481acdeaac58261bc4fc23ebb58b7197049024d354f60daabc`; `bfbc2/version.ini` `f3d94f70edcebbbc7d8ce38fdf076412fb95114ce1ecf071b26c9c2f93586372`. - S13 large exact transfer: `deep-stage-b` downloaded `alienswarm` from `fixture-alpha` with `install=false`. SHA-256 manifests matched exactly: `alienswarm/alienswarm.eti` `8a4fb1fd458e731affb175134b7b99efc8d8a5eda80e978ba81f721d01aecc43`; `alienswarm/notes.txt` `3832bcb7057a4453981e975d2d2d528bfd9a26671423352f4a8527362d5b9810`; `alienswarm/version.ini` `8dfdc51d4dbfb06015b41a85a5f5d47f44144139e4a12db2b17eb040773082a3`. - S14 multi-peer setup: `deep-stage-c` connected to alpha (`10.66.0.3:53514`) and `deep-stage-b` (`10.66.0.2:58491`). `list-games` showed `alienswarm` with `peer_count=2` before the download. - S14 chunk-source evidence for `alienswarm/alienswarm.eti`: `deep-stage-c` received chunks from `deep-stage-b` at offsets `0` and `67,108,864` (`67,108,864` bytes total) and from alpha at offsets `33,554,432` and `100,663,296` (`58,721,049` bytes total). The source-byte difference was `8,387,815` bytes, below one `32 MiB` chunk. - S14 final exactness: `deep-stage-c`'s `alienswarm` SHA-256 manifest matched `fixture-alpha` exactly for `alienswarm.eti`, `notes.txt`, and `version.ini`.