test(peer-cli): add large exact-transfer coverage

Add deeper peer CLI coverage for file-transfer integrity and multi-peer
chunking. The alpha fixture now carries a real renamed RAR archive larger
than 100 MB for alienswarm, which gives the chunk planner enough work to
split a single game archive across multiple peers.

Expose completed chunk source details as a peer event and have the CLI print
that event as JSONL. This keeps transfer behavior in lanspread-peer while the
CLI remains a harness that reports what the peer runtime did. The Tauri shell
logs the event at debug level so the shared PeerEvent enum stays exhaustive.

Document the new S13/S14 scenarios and record the manual run evidence,
including SHA-256 manifests and the per-peer byte split for the large archive.

Test Plan:
- just fmt
- just test
- just peer-cli-build
- just clippy
- just peer-cli-image
- unrar t -idq crates/lanspread-peer-cli/fixtures/fixture-alpha/alienswarm/alienswarm.eti
- Manual peer CLI: bravo -> deep-small-client bfbc2 download with matching SHA-256 manifests
- Manual peer CLI: alpha -> deep-stage-b alienswarm download with matching SHA-256 manifests
- Manual peer CLI: alpha + deep-stage-b -> deep-stage-c alienswarm download with chunk events from both peers and matching SHA-256 manifests

Refs: PEER_CLI_SCENARIOS.md S13 S14
This commit is contained in:
2026-05-17 10:25:26 +02:00
parent 5d58791192
commit 274b9d2fd4
7 changed files with 157 additions and 19 deletions
@@ -109,21 +109,32 @@ pub async fn download_game_files(
}
for chunk_result in results {
if let Err(e) = chunk_result.result {
log::warn!(
"Failed to download chunk from {}: {e}",
chunk_result.peer_addr
);
if chunk_result.chunk.retry_count < MAX_RETRY_COUNT {
let mut retry_chunk = chunk_result.chunk;
retry_chunk.retry_count += 1;
retry_chunk.last_peer = Some(chunk_result.peer_addr);
failed_chunks.push(retry_chunk);
} else {
last_err = Some(eyre::eyre!(
"Max retries exceeded for chunk: {}",
chunk_result.chunk.relative_path
));
match chunk_result.result {
Ok(()) => {
let _ = tx_notify_ui.send(PeerEvent::DownloadGameFileChunkFinished {
id: game_id.to_string(),
peer_addr: chunk_result.peer_addr,
relative_path: chunk_result.chunk.relative_path,
offset: chunk_result.chunk.offset,
length: chunk_result.chunk.length,
});
}
Err(e) => {
log::warn!(
"Failed to download chunk from {}: {e}",
chunk_result.peer_addr
);
if chunk_result.chunk.retry_count < MAX_RETRY_COUNT {
let mut retry_chunk = chunk_result.chunk;
retry_chunk.retry_count += 1;
retry_chunk.last_peer = Some(chunk_result.peer_addr);
failed_chunks.push(retry_chunk);
} else {
last_err = Some(eyre::eyre!(
"Max retries exceeded for chunk: {}",
chunk_result.chunk.relative_path
));
}
}
}
}
@@ -174,9 +185,20 @@ pub async fn download_game_files(
eyre::bail!("download cancelled for game {game_id}");
}
if let Err(e) = chunk_result.result {
log::error!("Retry failed for chunk: {e}");
last_err = Some(e);
match chunk_result.result {
Ok(()) => {
let _ = tx_notify_ui.send(PeerEvent::DownloadGameFileChunkFinished {
id: game_id.to_string(),
peer_addr: chunk_result.peer_addr,
relative_path: chunk_result.chunk.relative_path,
offset: chunk_result.chunk.offset,
length: chunk_result.chunk.length,
});
}
Err(e) => {
log::error!("Retry failed for chunk: {e}");
last_err = Some(e);
}
}
}
}