# 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 | Catalog-version skew | Three peers advertise the same catalog game ID. Peers A and B have stale `version.ini` values; peer C has the catalog's expected version. 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=1` and the catalog `eti_game_version`. The `got-game-files` descriptor set and transfer source are peer C only; no chunks come from A or B. The receiver's `version.ini` and SHA-256 manifest match C exactly. | | S16 | Catalog-version fanout with stale peers present | Peer A has a stale version of a game. Peers B and C both advertise the catalog version with matching file manifests; use a large file when proving chunk split. | The aggregated row counts only catalog-version ready peers. Large-file chunks may split between B and C; peer A is not listed as downloadable and contributes no manifest vote or file chunks. | | S17 | Catalog-version conflict rejection | Peer A has a stale version. Peers B and C both advertise the catalog version, but their file sizes conflict. | Validation considers only the catalog-version peers, so A cannot rescue the majority. The download fails with `download-failed`, and no committed target `version.ini` remains. | | S18 | Mid-download source drop with redundancy | Client downloads a large shared game from two ready peers, then one source is killed after the download has begun. | Failed chunks are retried against the surviving source; the download finishes, no `download-failed` is emitted, and the receiver's files match the source by diff or SHA-256. | | S19 | Mid-download sole-source drop | Client downloads a large game from one source, then that source is killed after the download has begun. | The download emits `download-failed`; no committed target `version.ini` remains; any partial payload is not advertised as ready; active operation state clears so a retry is possible. | | S20 | Receiver write failure | Client downloads a large game into a constrained `/games` filesystem. | The download fails deterministically, no committed `version.ini` is advertised, and active operation state clears so the peer can retry later. | | S21 | Add-game propagation | Two connected peers are running; one peer gains a new catalog game root through a completed download or an external drop. | The other peer receives a library update without reconnecting, and `list-games` shows the new remote game under the existing peer. | | S22 | Remove-game propagation | Two connected peers are running; one peer loses a previously advertised game root. | The other peer receives a library update without dropping the peer, and `list-games` no longer shows that remote game. | | S23 | Version bump propagation | Two connected peers are running; one peer's ready game root starts with a stale `version.ini`, then changes to the catalog version. | The other peer receives a library update without reconnecting; the stale row is absent before the change, then the catalog-version game appears as downloadable. | | S24 | Two clients pull from one source | Two empty clients connect to the same source and download the same large game concurrently. | Both downloads finish, both receivers match the source by diff or SHA-256, and the source remains responsive. | | S25 | One client downloads two games concurrently | One client connected to a source issues two different `download` commands without waiting for the first to finish. | Both operations may run in parallel; both eventually finish, each game reaches the requested install state, and each transferred root matches its source. | | S26 | Same-game duplicate download rejection | A client starts downloading a game, then issues a second `download` command for the same game while the first operation is active. | The second request is rejected deterministically as an operation-in-progress condition; the first download is not corrupted and still reaches its documented final state. | | S27 | Self-connect rejection | A peer sends `connect` to its own advertised listener address. | The command fails cleanly, no self-peer entry is created, and the peer remains responsive. | | S28 | Address change without identity change | A known peer is rediscovered with the same peer ID and a different listener address while its library is still known. | The peer record updates in place to the new address, the existing library stays attached to that peer ID, and no duplicate peer entry appears. This is covered with a deterministic unit-level check until the CLI can rebind a live listener without restart. | | S29 | Empty-library peer participates | A peer with no games connects into the mesh. | Other peers list it as a peer with zero games; it can receive a download, advertise the new game without restart, and become a source. | | S30 | 5+ peer mesh aggregation | Five peers advertise partially overlapping catalog games with a mix of unique and shared catalog-version games; a sixth client connects to all five. | The client shows one row per game ID, correct catalog-version ready-source `peer_count`, catalog `eti_game_version`, no duplicates, and no self entries. | | S31 | Bootstrapped peer becomes source in same session | An empty client downloads a game from a source, the original source shuts down, then a fresh third peer downloads the same game from the bootstrapped client. | The third peer's files match the original source by diff or SHA-256, proving downloaded files become servable without restart. | | S32 | Reinstall after uninstall | A downloaded game is installed, uninstalled, then installed again without another download. | `local/` is recreated from preserved root files, no transfer events occur during reinstall, and the game returns to `installed=true`. | | S33 | Install after external root mutation | A downloaded game root is externally mutated before `install` is issued. | The CLI fixture installer installs from the current root bytes. The resulting `local/fixture-payload.txt` must match the mutated archive bytes exactly. | | S34 | Many-small-files game without `.eti` | A catalog game root contains `version.ini` plus many small regular files and no archive. | Download with `install=false` transfers every file, chunk events are coherent for small files, and source/receiver manifests match exactly. | | S35 | Unknown game ID from remote peer | A remote peer advertises a game ID that is not in the receiver's catalog. | The receiver does not list the unknown game as downloadable, download attempts fail deterministically, and no local files are created. | | S36 | Catalog singleton beats stale majority | Five peers advertise one game; one peer has the catalog version and four peers have stale versions. | `list-games` reports `peer_count=1` and the catalog `eti_game_version`; all descriptors and chunks come from the singleton catalog-version peer, while stale peers remain hidden and contribute zero bytes. | | S37 | Single-source download throughput | A source peer advertises a temporary catalog game with one sparse `2 GiB` `.eti`; an empty client downloads it with `install=false`. | The client emits `download-finished` with throughput measurements (`bytes`, `duration_ms`, `mib_per_s`, `mbit_per_s`), and the downloaded archive size matches the source. | | S38 | First-play launch-setting stamping | `fixture-persona/css` ships a real RAR `.eti` whose tree buries a CRLF `SmartSteamEmu.ini` with a stub `PersonaName` line under `engine/bin/win64/steam_settings/`, plus a stub `account_name.txt` and `language.txt` under `profiles/local/`. A peer installs `css` (with `--unrar`), then sends `play css` with a username and language, then `play css` again. | After install the marker `games/css/launch_settings_applied` is absent and the stub files are intact under `local/`. The first `play` returns `already_applied=false` with `account_name_written`, `language_written`, and `persona_name_written` all true; the deep `SmartSteamEmu.ini` `PersonaName` value becomes the username with its `\r\n` ending and sibling lines preserved, `account_name.txt` becomes the username, `language.txt` becomes the passed language, and the marker now exists. A second `play` returns `already_applied=true`, rewrites nothing, and leaves the files untouched even if their values were reset externally. | | S39 | Streamed install without keeping archive payload | Empty client connects to `fixture-bravo`, then sends `stream-install cnctw`. The source has real RAR `.eti` payload entries under `bin/` and `data/`; the receiver uses the container-bundled `unrar` stream provider. | Client emits `download-begin`, streamed `download-chunk-finished`, `download-finished`, `install-begin`, and `install-finished`. Local `cnctw` is `downloaded=false`, `installed=true`, `availability=LocalOnly`; root `version.ini` and `.eti` are absent; `local/bin/cnctw-payload.bin` and `local/data/cnctw-assets.dat` match `unrar p` output by SHA-256. | | S40 | Streamed install receiver is not a peer source | After S39, a third peer connects only to the streamed-install receiver. | The third peer may see the receiver's local-only summary in peer snapshots, but `list-games` remote aggregation does not expose `cnctw` as downloadable, `peer_count` remains zero/absent, and attempting `download cnctw` fails with no local files created. | | S41 | Solid archive streamed install | Empty client connects to a peer serving `fixture-solid/cnctw`, whose `.eti` is a real solid RAR archive. The receiver uses the container-bundled `unrar` stream provider. | The fixture is verified as solid with `unrar lt`; streamed install finishes with `downloaded=false`, `installed=true`, `availability=LocalOnly`; root archive and `version.ini` are absent; streamed byte count equals the extracted solid entries; local payload SHA-256 hashes match `unrar p` output. | | S42 | Streamed install whole-stream retry | Empty client connects to two peers serving the same catalog-version `cnctw`: one broken source whose `--unrar` path is missing, followed by one good source. | The broken source sorts before the good source in retry order, contributes zero chunks, and the good source completes a fresh whole-stream attempt. The final state is local-only installed, no root archive/sentinel, no `.local.installing`, byte count matches the extracted entries, and payload hashes match the good source. | | S43 | Already-installed streamed install rejection | A client first stream-installs `cnctw`, then attempts `stream-install cnctw` again. | The second request emits `download-failed`, does not emit a new success event, leaves the existing local-only install intact, and clears active operations. | | S44 | Corrupt archive streamed install rollback | A source advertises catalog-version `cnctw`, but its root `.eti` is replaced with invalid bytes before the client requests `stream-install cnctw`. | The stream emits `download-failed`, does not emit download/install success, clears active operations, and leaves no `local/`, `.local.installing`, root archive, or root `version.ini` on the receiver. | | S45 | Sender disconnect during streamed install | A source serves large catalog-version `alienswarm`; after the client receives the first streamed chunk, the source container is killed. | The operation reaches a terminal failure/peers-gone event, emits no download/install success, clears active operations, and rolls back local/staging state. | | S46 | Receiver cancel during streamed install | A client starts streaming large catalog-version `alienswarm`, receives the first chunk, then sends `cancel-download alienswarm`. | The receiver cancels without emitting download/install success or a user-visible download failure, clears active operations, and rolls back local/staging state. | | S47 | Multi-archive streamed install order | A source serves `fixture-multi/cnctw` with two root `.eti` archives named to require sorted processing. | Streamed chunk paths arrive in root archive sort order, both payloads install under `local/`, the receiver is local-only installed, and no root archives or sentinel are committed. | ## Version-Skew Contract Use S15-S17 to pin down what happens when several peers have the same game ID but only some match the local catalog version: - The receiver's catalog is authoritative. A remote root whose `version.ini` does not match the catalog's expected version for that game ID is not downloadable. - `list-games` aggregates by game ID. The game appears once; `peer_count` counts only ready peers with that ID and the catalog version. - The aggregated `eti_game_version` must be the catalog version. - The descriptor set emitted to the download path, file-size validation, and transfer planning are catalog-version-only. Stale peers must not supply download descriptors, majority votes, or chunks. - If exactly one peer has the catalog version, that peer is the only transfer source. If several peers match the catalog version, validation and chunk fanout happen among that catalog-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. ## Extended Failure And Mutation Contracts Use S18-S36 to pin down operational behavior that is awkward to prove with the GUI: - A failed download must not commit the root `version.ini` sentinel. Partial payload files may remain, but they must not be advertised as a ready local game and must not leave an active operation stuck. - Source failure during a redundant download should retry failed chunks against another validated source for the same catalog-version file. - Live local library changes are observable by connected peers through library deltas; reconnect is not required for add, remove, or version-bump cases. - Same-game operations are single-flight. A duplicate download request while a game is already active is rejected instead of starting another writer. - Unknown remote game IDs are filtered by the receiver's current catalog and are not downloadable. 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 where some peers match the catalog version and others deliberately use stale `version.ini` contents. The existing alpha/bravo/charlie fixtures cover duplicate-source and shared-game cases; S15-S17 add the focused skew cases. ## First-Play Launch-Setting Contract Use S38 to pin down how launcher settings are stamped into an installed game: - Stamping happens on the first `play`, not during install/update. The install transaction only clears the `games//launch_settings_applied` marker so the next play reapplies settings to a freshly (re)created `local/`. - The first play stamps the username into the first `account_name.txt` and the first `SmartSteamEmu.ini` `PersonaName` line, and the language into the first `language.txt`, searching the whole `local/` tree. The matched `PersonaName` line keeps its existing line ending (`\n` or `\r\n`). - The marker records only that we *tried*: it is written unconditionally after the first play, so a game with none of these files is still marked done. - S38 needs a real archive expanded with `--unrar`; the Docker matrix image now carries the Linux sidecar for streamed-install coverage, while the peer crate's `launch_settings` unit tests cover the rewrite, line-ending, and marker logic deterministically. ## Streamed Install Archive Contract Use S39-S41 to pin down low-disk streamed installs: - The stream provider performs one archive metadata pass and one payload pass per `.eti`, then frames entry boundaries for the receiver. - Non-solid and solid archives both install into `local/` without committing a root archive or root `version.ini`, so the receiver is installed but not a downloadable source. - Streamed install integrity is currently sender archive integrity: size and RAR CRC32 must match the sender's archive metadata. The SHA-256 checks in the scenarios prove the Docker/provider path matches the source fixture; they are not catalog-owned trust anchors. - S41 verifies the fixture is actually solid inside the source container, so solid handling stays covered by the same Docker harness as the existing streamed-install scenarios. - S42 verifies retry/resume semantics: failed streamed attempts roll back their staging directory and retry the whole stream from another validated peer. There is no byte-offset resume contract. - S43-S47 cover the remaining streamed-install failure and archive-shape edges: already-installed rejection, corrupt archive rollback, sender disconnect, receiver cancel, and multi-archive root sorting. ## Run Log ### 2026-06-07 - Catalog-Version Matrix Alignment (S1-S47) - Code under test aligned checked-in fixture `version.ini` sentinels with the catalog, made `run_extended_scenarios.py` stamp generated fixture games with catalog versions by default, updated S15-S17/S23/S30/S36/S37 to assert catalog-authoritative aggregation, and wired S38 into the executable matrix. - Gates before Docker: `python3 -m py_compile crates/lanspread-peer-cli/scripts/run_extended_scenarios.py` passed. - Targeted rebuilt-image runner: `python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py S3 S8 S14 S15 S16 S17 S21 S22 S23 S24 S29 S30 S31 S34 S36 S37 S39 S40 S41 S42 S43 S44 S45 S46 S47 --build-image` passed. - S38 standalone runner: `python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py S38` passed, proving the real-RAR `css` fixture installs with the container `/usr/local/bin/unrar` sidecar and stamps launch settings only once. - Full matrix runner: `python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py` passed for S1-S47 against the rebuilt `lanspread-peer-cli:dev` image. - The final full-run highlights included S3 aggregation, S15-S17 catalog-version skew/fanout/conflict, S23 stale-to-catalog propagation, S30 mesh aggregation, S36 catalog singleton over stale majority, S37 throughput, S38 first-play stamping, and S39-S47 streamed-install coverage. ### 2026-06-07 - Streamed Install Edge Coverage (S43-S47) - Code under test added `cancel-download` to `lanspread-peer-cli`, added the tiny `fixture-multi/cnctw` two-archive fixture, and added S43-S47 in `run_extended_scenarios.py`. - Gates before Docker: `just fmt` and `python3 -m py_compile crates/lanspread-peer-cli/scripts/run_extended_scenarios.py` passed. - Runner: `python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py S43 S44 S45 S46 S47 --build-image` passed against the rebuilt `lanspread-peer-cli:dev` image. - S43 stream-installed `cnctw`, retried `stream-install cnctw`, observed `download-failed`, and verified the existing local-only install stayed intact. - S44 replaced the source `cnctw.eti` with invalid bytes. The receiver emitted `download-failed`, cleared active operations, and left no `local/`, `.local.installing`, root archive, or root `version.ini`. - S45 killed the sole `alienswarm` source after the first streamed chunk. The receiver ended with `download-failed`, emitted no success, cleared active operations, and rolled back local/staging state. - S46 cancelled `alienswarm` on the receiver after the first streamed chunk. The receiver emitted no success and no user-visible `download-failed`, cleared active operations, and rolled back local/staging state. - S47 streamed `fixture-multi/cnctw` and observed chunk paths in sorted root archive order: `cnctw/.local.installing/order/first.txt`, then `cnctw/.local.installing/order/second.txt`. ### 2026-06-07 - Streamed Install Whole-Stream Retry (S42) - Code under test added S42 in `run_extended_scenarios.py`. - Gates before Docker: `python3 -m py_compile crates/lanspread-peer-cli/scripts/run_extended_scenarios.py` passed. - Runner: `python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py S42` passed against the current `lanspread-peer-cli:dev` image. - S42 started a broken source with `--unrar /missing-unrar` and a good source with the same catalog-version `cnctw` metadata. The broken source sorted first (`10.66.0.2:32897`) and the good source second (`10.66.0.3:34092`). - The broken source contributed zero chunks; the good source completed the fresh whole-stream attempt with `3145728` streamed file bytes. - The final client state was `downloaded=false`, `installed=true`, `availability=LocalOnly`, with no root `version.ini`, no root `cnctw.eti`, and no `.local.installing` staging directory. Payload SHA-256 hashes matched the good source's `unrar p` output. ### 2026-06-07 - Solid Streamed Install Coverage (S41) - Code under test added `fixture-solid/cnctw`, a real solid RAR `.eti`, plus S41 in `run_extended_scenarios.py`. - Gates before Docker: `just fmt`, `git diff --check`, and `python3 -m py_compile crates/lanspread-peer-cli/scripts/run_extended_scenarios.py` passed. - Runner: `python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py S41 --build-image` passed against the rebuilt `lanspread-peer-cli:dev` image. - S41 verified the source archive with `unrar lt -cfg-` inside the source container; the archive reported `Details: RAR 5, solid`. - The streamed install finished with `downloaded=false`, `installed=true`, `availability=LocalOnly`, no root `version.ini`, and no root `cnctw.eti`. - The client received `118` streamed file bytes, matching the extracted solid entries. Payload SHA-256 hashes matched `unrar p` output: `88764c9a6c9b5b846b4323cf7725cb7fd70766ddd7fba4168332804a839fa193` (`bin/cnctw-solid-payload.bin`) and `44afc308269b2381b7c707a056dd8d9d393274108ac4d880237fa6772c861d7a` (`data/cnctw-solid-assets.dat`). ### 2026-06-07 - Streamed Install Prototype (S39-S40) - Code under test added `stream-install` to `lanspread-peer-cli`, a peer `StreamInstallGame` command, streamed install frames over QUIC, and an injected `unrar lt`/`unrar p` provider for archive-derived bytes. - Gates before Docker: `just fmt` and `RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= just test` passed for the workspace. - Runner: `python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py S39 S40 --build-image` passed against the rebuilt `lanspread-peer-cli:dev` image. - S39 streamed a catalog-version-adjusted `cnctw` fixture from a real RAR `.eti` into the receiver's `local/` only. The receiver had `downloaded=false`, `installed=true`, `availability=LocalOnly`, no root `version.ini`, no root `.eti`, and payload SHA-256 hashes `82f4da22dc042166def2a5ee2eca19fc9e52785f99838e86c32167cb342e2588` (`bin/cnctw-payload.bin`) and `abf833a06c74ea9f17d505c2684186491898ce906405e0f098f0deac19476b06` (`data/cnctw-assets.dat`) matching `unrar p`. - S40 connected an observer only to that streamed-install receiver. The observer saw the receiver's `cnctw` summary as local-only, remote aggregation hid it as a downloadable source, and `download cnctw` failed with `no peers have game cnctw`. ### 2026-05-28 - First-Play Launch-Setting Stamping (S38) - Code under test moved the `account_name.txt`/`language.txt` overwrite out of the install transaction and into a single first-play step (shared with the new `SmartSteamEmu.ini` `PersonaName` rewrite) gated by the `games//launch_settings_applied` marker. - `just test` passed the whole workspace, including the new `lanspread_peer::launch_settings` unit tests and `install::transaction::install_resets_launch_settings_marker`. - S38 host run: built `crates/lanspread-peer-cli/fixtures/fixture-persona/css` with a stored RAR `.eti` (verified by `unrar t`) burying a CRLF `SmartSteamEmu.ini` plus stub `account_name.txt`/`language.txt`. A host peer installed `css` with `--unrar /usr/bin/unrar`, then `play css` stamped the username into the deep `PersonaName` line (CRLF preserved, sibling lines intact) and `account_name.txt`, the language into `language.txt`, and created the marker. A second `play css` returned `already_applied=true` and rewrote nothing even after the value was reset externally. ### 2026-05-19 - Snapshot Status Fix Docker Matrix Pass - Code under test included `5c4976d` (`fix(peer): settle local state before clearing operations`) and `6651f02` (`fix(ui): derive operation status from snapshots`). - Gates before the matrix: `just fmt`, `just test`, `just frontend-test`, and `just build` passed. The peer harness image was rebuilt with `just peer-cli-image`. - Runner: `python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py` passed S1-S36 against the rebuilt `lanspread-peer-cli:dev` image. - Auto-install coverage remained good: S5 downloaded and installed `cnctw`, saw the fixture payload under `local/`, and the downloaded root diffed cleanly against `fixture-bravo/cnctw` excluding local metadata. - Large/exact transfer coverage remained good: S13 small and large downloads diffed cleanly; S14 split `alienswarm` between two sources with chunk totals `67,108,864` and `58,721,049` bytes and the final root diffed cleanly. - Failure and mutation coverage remained good: S17 latest-version conflict, S19 sole-source drop, S20 write failure, S26 duplicate operation, and S35 unknown catalog filtering all failed safely without advertising bad local state; S21-S23 propagation, S24-S25 concurrency, S29-S31 bootstrapping, S32 reinstall, S33 mutation install, S34 many-small-files, and S36 latest singleton all passed. ### 2026-05-18 - Full Automated Docker Matrix Pass - Runner: `python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py` passed S1-S36 against the current `lanspread-peer-cli:dev` image. - S1-S17 rerun highlights: startup, direct connect, aggregation, download, install/uninstall, duplicate-source, ambiguous metadata, missing game, shutdown cleanup, identity reconnect, serve gates, exact equality, large multi-peer chunking, and latest-version selection/conflict all passed. Exact transfer scenarios used `diff -r`/SHA-256 manifest checks; S14 chunk totals were `58,721,049` and `67,108,864` bytes, balanced within one `32 MiB` chunk. - S18-S36 rerun highlights: source-drop, disk-full, live mutation, concurrency, duplicate-operation rejection, self-connect rejection, empty-peer sourcing, 5-peer aggregation, bootstrapped sourcing, reinstall, external mutation, many-small-files, unknown catalog filtering, and stale-majority/latest singleton cases all passed. File-copy scenarios used diff/manifests or `cmp` for the mutated install payload. ### 2026-05-18 - Extended Scenario Docker Pass - Runner: `python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py` passed for S18-S36 after rebuilding `lanspread-peer-cli:dev` with `just peer-cli-image`. - S18 redundant source drop: one `alienswarm` source was killed after `download-begin`; the client emitted `download-finished`, no `download-failed`, and `diff -r`/SHA-256 manifest comparison matched the surviving source. Recorded large-file chunk bytes from the surviving source: `58,721,049`. - S19 sole-source drop: killing the only source after `download-begin` emitted `download-failed`; the receiver had no committed `alienswarm/version.ini`, no ready local row, and no active operation left. - S20 receiver write failure: a client with `/games` constrained to a `32m` tmpfs emitted `download-failed`; `/games/alienswarm/version.ini` was absent inside the container and active operations were empty. - S21-S23 live mutation propagation: a connected peer observed `cod5` added, `cod5` removed, and `cnc4` bumped from `20250101` to `20260501` without reconnecting or dropping the peer. - S24-S25 concurrency: two clients downloaded `alienswarm` from one source at the same time and both diffed cleanly; one client downloaded `bfbc2` and `cnctw` concurrently and both roots diffed cleanly. - S26 duplicate same-game download: the second `alienswarm` download command returned `operation already in progress for game alienswarm`; the first download still finished and diffed cleanly. - S27 self-connect rejection: connecting a peer to its own listener returned `cannot connect peer to itself ...`; `list-peers` stayed empty and the peer stayed responsive. - S28 address-change invariant: `just test` passed and included `peer_db::tests::address_update_preserves_peer_identity_and_library`. - S29 empty-library peer: an observer first saw the empty peer with zero games; after that peer downloaded `alienswarm`, the downloaded root diffed cleanly and the observer's peer snapshot for that same peer contained `alienswarm`. - S30 5-peer aggregation: a sixth client connected to five peers and aggregated six game IDs with expected `peer_count` and latest versions, with no duplicate game rows and no self-peer entry. - S31 bootstrapped source: after the original source was killed, a third peer downloaded `alienswarm` from the bootstrapped client and diffed cleanly against the original fixture. - S32 reinstall: reinstall after uninstall recreated `local/`, reported `installed=true`, and produced no transfer chunk events during reinstall. - S33 external root mutation: after mutating the downloaded `bfbc2.eti` inside the client container, `install` wrote `local/fixture-payload.txt` that matched the mutated archive exactly by `cmp`. - S34 many-small-files transfer: a `bf1942` fixture with 20 small regular files and no `.eti` downloaded with `install=false`; 21 file chunks were observed including `version.ini`, and the receiver diffed cleanly against the source. - S35 unknown game ID: a source advertised `mystery-game` via `--fixture`; the receiver filtered it out of `list-games`, `download mystery-game` returned `game mystery-game is not in the local catalog`, and no local files were created. - S36 latest singleton: with one peer on `20260501` and four peers on `20250101`, the client reported `peer_count=5` and latest `20260501`; only the singleton latest peer sent chunks and the final root diffed cleanly. ### 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`.