From 4f92840fe76c82f0e5a8cd8d8cd0f65daa87af68 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 01:46:43 +0200 Subject: [PATCH] Bridge test: the first process is a runtime dropped after the turn opens A second PlayTable over the same store cannot open while the first is alive (the store takes an exclusive lock), so the first leg now runs on its own runtime and dies with it, as a crashed process would. Does not run on the stopgap (ignored test); verified on Starfire. Co-Authored-By: Claude Opus 5.5 --- crates/ghostlight-dungeon/src/runtime.rs | 47 +++++++++++++++++------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/crates/ghostlight-dungeon/src/runtime.rs b/crates/ghostlight-dungeon/src/runtime.rs index 3908278..3b6075c 100644 --- a/crates/ghostlight-dungeon/src/runtime.rs +++ b/crates/ghostlight-dungeon/src/runtime.rs @@ -3235,22 +3235,43 @@ mod tests { ); assert_eq!(play_intents.len(), 1); assert_eq!(play_intents[0]["payload"]["bindings"]["text"], "I look around the hall."); - let played = post(&fixture.state, &fixture.cookie, play_intents.into_iter().next().unwrap()).await; + // The first process is a runtime of its own, dropped once the turn is + // open: the spawned round dies with it, which is what a crash does. + let (played, opened) = { + let state = fixture.state.clone(); + let cookie = fixture.cookie.clone(); + let held = held.clone(); + let intent = play_intents.into_iter().next().unwrap(); + std::thread::spawn(move || { + let process = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); + let seen = process.block_on(async { + let played = post(&state, &cookie, intent).await; + let opened = tokio::time::timeout(Duration::from_secs(5), async { + loop { + if let Some(view) = held.current_turn_view().await { + if view.run_in_progress { + break view; + } + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("the real client's typed world.play must open a turn a round is working on"); + (played, opened) + }); + process.shutdown_timeout(Duration::from_secs(5)); + seen + }) + .join() + .unwrap() + }; assert_eq!(played["receipt"]["state"], "accepted", "typed world.play via the real client: {played}"); - let opened = tokio::time::timeout(Duration::from_secs(5), async { - loop { - if let Some(view) = held.current_turn_view().await { - break view; - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - }) - .await - .expect("the real client's typed world.play must reach PlayTable and open a turn row"); assert_eq!(opened.state, PlayTurnState::Running); + fixture.state.play = None; + drop(held); - // The process that held that turn is gone; the next one opens the same - // store and finds the turn `Running` with nothing working on it. The + // The next process opens the same store and finds the turn `Running` with nothing working on it. The // client now clicks Play with the box untouched, which is exactly what // the card's hint tells a player to do. let restarted = table_over(