Merge the S18-S36 scenario ideas into the official peer-cli scenario matrix and add a Docker-backed runner that now exercises S1-S36 with concrete file proofs. The runner creates temporary fixtures under .lanspread-peer-cli, drives JSONL peer containers, checks transferred roots with diff and SHA-256 manifests, and covers startup, discovery, transfer, failure, mutation, concurrency, mesh, lifecycle, and catalog edge cases. The scenarios exposed a few harness/runtime boundary gaps that would otherwise make the contract ambiguous. The peer CLI now rejects self-connects, rejects commands for game IDs outside the receiver catalog, filters unknown remote games from its command/event surface, and reports duplicate active same-game commands as operation-in-progress errors. The peer core also refuses non-catalog download commands before transfer, and PeerGameDB has a unit check that address changes preserve identity and library state. S12 and S28 remain unit-level invariants because the CLI cannot stably race raw serve-gate requests or rebind a live listener without restart. The runner treats those scenarios as covered by just test and checks the expected unit test names appear in the output. Test Plan: - just fmt - python3 -m py_compile crates/lanspread-peer-cli/scripts/run_extended_scenarios.py - RUSTC_WRAPPER= just test - RUSTC_WRAPPER= just clippy - RUSTC_WRAPPER= just peer-cli-build - just peer-cli-image - python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py - git diff --check Refs: PEER_CLI_SCENARIOS.md S1-S36
24 KiB
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. |
| 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 gets a newer version.ini. |
The other peer receives a library update without reconnecting, and the aggregated row reflects the newer eti_game_version. |
| 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 games, shared games, and differing versions; a sixth client connects to all five. | The client shows one row per game ID, correct ready-source peer_count, latest 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 | Latest singleton beats stale majority | Five peers advertise one game; one peer has 20260501, four peers have 20250101. |
list-games reports eti_game_version=20260501; all descriptors and chunks come from the singleton latest peer; stale peers contribute zero bytes. |
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.inistring, so use sortableYYYYMMDDvalues in manual fixtures. list-gamesaggregates by game ID. The game appears once;peer_countcounts all ready peers with that ID, including peers that only have older versions.- The aggregated
eti_game_versionmust 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-gamesrow,got-game-filesdescriptors,download-chunk-finishedsource 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.inisentinel. 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 latest-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 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 Automated Docker Matrix Pass
- Runner:
python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.pypassed S1-S36 against the currentlanspread-peer-cli:devimage. - 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 were58,721,049and67,108,864bytes, balanced within one32 MiBchunk. - 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
cmpfor the mutated install payload.
2026-05-18 - Extended Scenario Docker Pass
- Runner:
python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.pypassed for S18-S36 after rebuildinglanspread-peer-cli:devwithjust peer-cli-image. - S18 redundant source drop: one
alienswarmsource was killed afterdownload-begin; the client emitteddownload-finished, nodownload-failed, anddiff -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-beginemitteddownload-failed; the receiver had no committedalienswarm/version.ini, no ready local row, and no active operation left. - S20 receiver write failure: a client with
/gamesconstrained to a32mtmpfs emitteddownload-failed;/games/alienswarm/version.iniwas absent inside the container and active operations were empty. - S21-S23 live mutation propagation: a connected peer observed
cod5added,cod5removed, andcnc4bumped from20250101to20260501without reconnecting or dropping the peer. - S24-S25 concurrency: two clients downloaded
alienswarmfrom one source at the same time and both diffed cleanly; one client downloadedbfbc2andcnctwconcurrently and both roots diffed cleanly. - S26 duplicate same-game download: the second
alienswarmdownload command returnedoperation 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-peersstayed empty and the peer stayed responsive. - S28 address-change invariant:
just testpassed and includedpeer_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 containedalienswarm. - S30 5-peer aggregation: a sixth client connected to five peers and aggregated
six game IDs with expected
peer_countand 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
alienswarmfrom the bootstrapped client and diffed cleanly against the original fixture. - S32 reinstall: reinstall after uninstall recreated
local/, reportedinstalled=true, and produced no transfer chunk events during reinstall. - S33 external root mutation: after mutating the downloaded
bfbc2.etiinside the client container,installwrotelocal/fixture-payload.txtthat matched the mutated archive exactly bycmp. - S34 many-small-files transfer: a
bf1942fixture with 20 small regular files and no.etidownloaded withinstall=false; 21 file chunks were observed includingversion.ini, and the receiver diffed cleanly against the source. - S35 unknown game ID: a source advertised
mystery-gamevia--fixture; the receiver filtered it out oflist-games,download mystery-gamereturnedgame mystery-game is not in the local catalog, and no local files were created. - S36 latest singleton: with one peer on
20260501and four peers on20250101, the client reportedpeer_count=5and latest20260501; 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-imagepassed. Localjust peer-cli-buildneededRUSTC_WRAPPER=because the hostkachewrapper failed with a read-only filesystem error;RUSTC_WRAPPER= just peer-cli-buildpassed. - Temporary skew/conflict fixtures were created under the ignored
.lanspread-peer-cli/full-fixtures/tree usingrar a -idq -m0against/dev/urandompayloads 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-alphaemittedcli-started,local-library-changed, andlocal-peer-ready;alienswarm,bf1942, andggooweredownloaded=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-peersreturnedpeer_count=1, andlist-peersshowed exactly one bravo peer with four games. - S3 clean remote aggregation: an empty
clean-s3-clientsaw exactly alpha and bravo.list-gamesshowedggoo peer_count=2;alienswarm,bf1942,bfbc2,cnc4, andcnctweach hadpeer_count=1. - S4 single-source no-install:
full-empty-clientdownloadedbfbc2from bravo withinstall=false. Events includedgot-game-files,download-begin,download-finished, and localinstalled=false. Host verification:diff -r crates/lanspread-peer-cli/fixtures/fixture-bravo/bfbc2 .lanspread-peer-cli/full-empty-client/games/bfbc2passed andlocal/was absent. - S5 auto-install:
full-empty-clientdownloadedcnctwwith default install. Events included download finish,install-begin, andinstall-finished;local/fixture-payload.txtexisted. Host verification diffed the downloaded files againstfixture-bravo/cnctwexcludinglocal/and.lanspread.json. - S6 manual install/uninstall: after S4,
install bfbc2createdlocal/and markedinstalled=true;uninstall bfbc2removedlocal/and preserved the downloaded root files. Host verification diffed the preserved files againstfixture-bravo/bfbc2excluding.lanspread.json. - S7 duplicate-source download:
full-empty-clientdownloaded sharedggoofrom alpha/bravo withinstall=false. Chunk events used alpha forversion.iniand bravo forggoo.eti; hostdiff -rmatched bothfixture-alpha/ggooandfixture-bravo/ggoo. - S8 ambiguous metadata rejection:
full-s8-aandfull-s8-bboth advertisedggooversion20260101but with different.etisizes (1,048,746and2,097,323bytes). The client sawpeer_count=2, thendownload ggooemitteddownload-failed; no targetggoo/version.iniwas committed. - S9 missing game:
download does-not-existemittedno-peers-have-gameand returned a command error;.lanspread-peer-cli/full-empty-client/gameshad nodoes-not-existdirectory. - S10 shutdown cleanup: alpha saw bravo before shutdown with one remote peer and
bravo-only remote games. After bravo
shutdown, alpha emittedpeer-lost;list-peersreturned[]andlist-gamesreturned an empty remote list. - S11 same identity reconnect: restarting bravo reused peer ID
019e347d901e70c19adf5b9fd313fce4at new address10.66.0.3:41764. Alphalist-peersshowed 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 testpassed, includinglocal_download_available_gates_on_catalog_operation_and_sentinel,get_game_response_respects_serve_gates,file_transfer_dispatch_respects_serve_gates, andlocal_relative_paths_are_never_transferable. - S13 exact transferred-file equality: the S4 small transfer and S14 large
transfer both passed host
diff -ragainst the original source game directories, proving exact file equality beyond event flow. - S14 large multi-peer chunked download:
full-empty-clientfirst downloadedalienswarmfrom alpha and diffed cleanly againstfixture-alpha/alienswarm. A freshfull-s14-clientthen sawalienswarm peer_count=2and downloaded from both alpha andfull-empty-client. Large.etichunk totals were67,108,864bytes from alpha and58,721,049bytes from the staged peer, balanced within one32 MiBchunk. Final hostdiff -ragainstfixture-alpha/alienswarmpassed. - S15 three-way version skew: peers A/B/C advertised
cnc4versions20250101,20250201, and20250301. The client saw one row withpeer_count=3andeti_game_version=20250301; all chunks came only from C at10.66.0.4:60290. Hostdiff -ragainst C passed. - S16 latest-version fanout with stale peer present: A advertised stale
20250101; B/C both advertised latest20250301with a134,217,906byte.eti. The client sawpeer_count=3; chunks came only from B/C (67,108,873and67,109,042bytes respectively), with stale A contributing zero. Hostdiff -rmatched both B and C. - S17 latest-version conflict rejection: A advertised stale
20250101; B/C both advertised latest20250301but with conflicting.etisizes (1,048,748and2,097,325bytes). The client sawpeer_count=3and latest20250301, thendownload cnc4emitteddownload-failed; no targetcnc4/version.iniwas committed. - Gates after manual runs:
just fmt,RUSTC_WRAPPER= just test, andRUSTC_WRAPPER= just clippypassed.
2026-05-17 - Exact Transfer And Large Multi-Peer Chunking
- Fixture update:
fixture-alpha/alienswarm/alienswarm.etiwas rebuilt withrar a -idq -m0from three random 40 MiB payload files, then renamed to.eti. Final archive size:125,829,913bytes.unrar t -idqpassed. - Gates before manual runs:
just fmt,just test,just peer-cli-build,just clippy, andjust peer-cli-imagepassed. - S13 small exact transfer:
deep-small-clientdownloadedbfbc2fromfixture-bravowithinstall=false. SHA-256 manifests matched exactly:bfbc2/bfbc2.etif7accef0833f29481acdeaac58261bc4fc23ebb58b7197049024d354f60daabc;bfbc2/version.inif3d94f70edcebbbc7d8ce38fdf076412fb95114ce1ecf071b26c9c2f93586372. - S13 large exact transfer:
deep-stage-bdownloadedalienswarmfromfixture-alphawithinstall=false. SHA-256 manifests matched exactly:alienswarm/alienswarm.eti8a4fb1fd458e731affb175134b7b99efc8d8a5eda80e978ba81f721d01aecc43;alienswarm/notes.txt3832bcb7057a4453981e975d2d2d528bfd9a26671423352f4a8527362d5b9810;alienswarm/version.ini8dfdc51d4dbfb06015b41a85a5f5d47f44144139e4a12db2b17eb040773082a3. - S14 multi-peer setup:
deep-stage-cconnected to alpha (10.66.0.3:53514) anddeep-stage-b(10.66.0.2:58491).list-gamesshowedalienswarmwithpeer_count=2before the download. - S14 chunk-source evidence for
alienswarm/alienswarm.eti:deep-stage-creceived chunks fromdeep-stage-bat offsets0and67,108,864(67,108,864bytes total) and from alpha at offsets33,554,432and100,663,296(58,721,049bytes total). The source-byte difference was8,387,815bytes, below one32 MiBchunk. - S14 final exactness:
deep-stage-c'salienswarmSHA-256 manifest matchedfixture-alphaexactly foralienswarm.eti,notes.txt, andversion.ini.