diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs index 6cbc7c149..360c24f46 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs @@ -925,8 +925,8 @@ fn autonomous_continuation_intent_is_exact_and_does_not_swallow_new_requirements } } -#[test] -fn game_chat_pure_continue_inherits_failed_root_semantics_and_manifest_progress() { +#[tokio::test(flavor = "current_thread")] +async fn game_chat_pure_continue_inherits_failed_root_semantics_and_manifest_progress() { let _config_guard = crate::tests::write_test_local_config("{}".to_string()); let original_task = "做一个水晶主题的俄罗斯方块,完成移动、旋转、消行和重开"; let (_temporary, root, original_state, original_contract) = autonomous_fixture_with_source( @@ -1093,6 +1093,18 @@ fn game_chat_pure_continue_inherits_failed_root_semantics_and_manifest_progress( .expect("continued root must pass the scheduler contract gate"); assert_eq!(scheduled.len(), 1); assert_eq!(scheduled[0].state.agent_id, "code-prototype"); + // Scheduling starts the child worker asynchronously. Wait for its + // durable terminal projection and lane release before mutating the same + // project; a lane release alone can precede a late manifest projection. + let _ = crate::tests::wait_for_agent_runtime_manifest_projection_async( + &root, + "code-prototype", + &scheduled[0].state.run_id, + "failed", + "budget-exhausted", + GameCreationAppTaskStatus::Failed, + ) + .await; update_manifest_task_status_at( &root, "code-prototype", diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs index 743fb95d8..d0fe0840b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs @@ -475,7 +475,7 @@ fn wait_for_agent_runtime_terminal_and_lane_release( ); } -async fn wait_for_agent_runtime_terminal_and_lane_release_async( +pub(crate) async fn wait_for_agent_runtime_terminal_and_lane_release_async( root: &Path, agent_id: &str, run_id: &str, @@ -526,6 +526,58 @@ async fn wait_for_agent_runtime_terminal_and_lane_release_async( } } +pub(crate) async fn wait_for_agent_runtime_manifest_projection_async( + root: &Path, + agent_id: &str, + run_id: &str, + runtime_status: &str, + phase: &str, + manifest_status: GameCreationAppTaskStatus, +) -> AgentRuntimeResult { + let deadline = Instant::now() + Duration::from_secs(10); + let mut terminal = wait_for_agent_runtime_terminal_and_lane_release_async( + root, agent_id, run_id, runtime_status, phase, + ) + .await; + let mut stable_samples = 0_u8; + loop { + if let Ok(project_lock) = acquire_game_creator_agent_runtime_project_write_lock_with_wait( + root, + "test.wait_runtime_manifest_projection", + ) { + let manifest = read_manifest_for_project(root) + .expect("read manifest while waiting for terminal projection"); + let projected = manifest + .tasks + .iter() + .find(|task| task.id == agent_id) + .is_some_and(|task| task.status == manifest_status); + let reread = read_game_creator_agent_runtime_at(root, agent_id) + .expect("reread runtime while waiting for terminal projection"); + drop(project_lock); + if projected + && reread.state.run_id == run_id + && reread.state.status == runtime_status + && reread.state.phase == phase + { + stable_samples = stable_samples.saturating_add(1); + terminal = reread; + if stable_samples >= 2 { + return terminal; + } + } else { + stable_samples = 0; + terminal = reread; + } + } + assert!( + Instant::now() < deadline, + "runtime terminal manifest projection did not settle for {agent_id}/{run_id}" + ); + tokio::time::sleep(Duration::from_millis(20)).await; + } +} + async fn wait_for_agent_runtime_lane_release_async( root: &Path, agent_id: &str,