fix(peer-cli): fail missing downloads from peer event

The peer core already emits NoPeersHaveGame when a requested game cannot be
served by any known peer. The JSONL harness still waited for the generic file
detail timeout before returning the download command error, which made the
manual scenario slower and less precise.

Correlate the existing no-peers event with the pending CLI download command so
the harness returns a deterministic error immediately. This is harness
bookkeeping only; game availability and peer behavior remain owned by
lanspread-peer.

Test Plan:
- just fmt
- just test
- just clippy
- just peer-cli-build
- just peer-cli-image
- just peer-cli-alpha, just peer-cli-bravo, just peer-cli-charlie
- In charlie, send {"cmd":"download","game_id":"not-a-game"}; observe
  no-peers-have-game followed by error "no peers have game not-a-game"

Refs: PEER_CLI_SCENARIOS.md
This commit is contained in:
2026-05-17 09:34:20 +02:00
parent 10a1f57183
commit 5d58791192
+24 -6
View File
@@ -94,6 +94,7 @@ struct CliState {
remote_games: Vec<Game>,
active_operations: Vec<ActiveOperation>,
game_files: HashMap<String, Vec<GameFileDescription>>,
unavailable_games: HashSet<String>,
}
#[derive(Clone, serde::Serialize)]
@@ -304,23 +305,32 @@ async fn game_files_for_download(
shared: &SharedState,
game_id: &str,
) -> eyre::Result<Vec<GameFileDescription>> {
if let Some(files) = shared.state.read().await.game_files.get(game_id).cloned() {
return Ok(files);
{
let mut state = shared.state.write().await;
if let Some(files) = state.game_files.get(game_id).cloned() {
return Ok(files);
}
state.unavailable_games.remove(game_id);
}
sender.send(PeerCommand::GetGame(game_id.to_string()))?;
let wait = async {
loop {
if let Some(files) = shared.state.read().await.game_files.get(game_id).cloned() {
return files;
let state = shared.state.read().await;
if let Some(files) = state.game_files.get(game_id).cloned() {
return Ok(files);
}
if state.unavailable_games.contains(game_id) {
eyre::bail!("no peers have game {game_id}");
}
drop(state);
shared.notify.notified().await;
}
};
tokio::time::timeout(Duration::from_secs(10), wait)
.await
.wrap_err("timed out waiting for game file details")
.wrap_err("timed out waiting for game file details")?
}
async fn event_loop(
@@ -396,7 +406,15 @@ async fn update_state_from_event(shared: &SharedState, event: PeerEvent) -> (&'s
PeerEvent::UninstallGameBegin { id } => ("uninstall-begin", json!({"game_id": id})),
PeerEvent::UninstallGameFinished { id } => ("uninstall-finished", json!({"game_id": id})),
PeerEvent::UninstallGameFailed { id } => ("uninstall-failed", json!({"game_id": id})),
PeerEvent::NoPeersHaveGame { id } => ("no-peers-have-game", json!({"game_id": id})),
PeerEvent::NoPeersHaveGame { id } => {
shared
.state
.write()
.await
.unavailable_games
.insert(id.clone());
("no-peers-have-game", json!({"game_id": id}))
}
PeerEvent::PeerConnected(addr) => ("peer-connected", peer_addr_json(addr)),
PeerEvent::PeerDisconnected(addr) => ("peer-disconnected", peer_addr_json(addr)),
PeerEvent::PeerDiscovered(addr) => ("peer-discovered", peer_addr_json(addr)),