bb7497c0ff
NEXT_STEPS item 4 needed the streamed-install integrity model to be a conscious decision. Keep the current runtime behavior, but name it as sender archive integrity: the receiver verifies streamed file size and RAR CRC32 from the sender's archive metadata before committing the install transaction. This protects against truncation, transport corruption, and stream provider bugs. It deliberately does not claim malicious-peer protection, because the sender controls both the streamed bytes and the RAR metadata. The docs now say that trusted content requires a future catalog schema with catalog-owned archive or extracted-file SHA-256 hashes. Test Plan: - just fmt - just test - just clippy - python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py S41 --build-image - git diff --check - git diff --cached --check Refs: NEXT_STEPS.md item 4
408 lines
34 KiB
Markdown
408 lines
34 KiB
Markdown
# 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. |
|
|
| 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 `got-game-files`, `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. |
|
|
|
|
## 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.
|
|
|
|
## 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 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.
|
|
|
|
## 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/<id>/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.
|
|
|
|
## Run Log
|
|
|
|
### 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/<id>/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`.
|