diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs index 2e1fe0208..17375996e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs @@ -1422,6 +1422,44 @@ mod tests { assert_eq!(enqueued_ids(&bootstrap.events), vec!["turn-2"]); } + /// 中途加入者拿到的是**合并快照**:正在跑那一轮的锚点 + 还没走的队列,顺序就是事件顺序。 + /// + /// 队列搬到宿主之后这件事只能由 live-set bootstrap 顶上:另一个窗口、离开工作台再回来, + /// 看到的状态必须和一直在场的那位一致——在跑的那一轮还在跑(锚点带开口条目身份,条目挂得回去), + /// 排着的那几条还在队,而不是只剩其中一个。 + #[test] + fn late_subscriber_sees_the_running_turn_and_the_pending_queue_together() { + let mut manager = DirectThreadManager::with_limits(100, 100_000); + manager + .enqueue_pending_turn("thread-1", pending_turn("turn-1")) + .expect("enqueue head"); + manager + .claim_pending_turn("thread-1", FIXED_AT_MS) + .expect("claim head"); + manager + .enqueue_pending_turn("thread-1", pending_turn("turn-2")) + .expect("enqueue tail"); + + let bootstrap = manager.subscribe("thread-1"); + assert!( + matches!( + bootstrap.events.as_slice(), + [ + DirectThreadEvent::TurnStarted { .. }, + DirectThreadEvent::QueueEnqueued { client_turn_id, .. } + ] if client_turn_id == "turn-2" + ), + "中途加入者要先看到在跑的那一轮、再看到还排着的那几条:{:?}", + bootstrap.events + ); + assert_eq!( + bootstrap.events[0].user_item_id(), + Some("direct-codex:turn-1:user"), + "锚点必须带着正在跑那一轮的开口条目身份" + ); + assert_eq!(enqueued_ids(&bootstrap.events), vec!["turn-2"]); + } + /// 入队幂等:判重范围是「在队 ∪ 正在跑的那一轮」,重复入队不排第二条、不发第二条事件。 #[test] fn enqueue_is_idempotent_per_client_turn_id() {