From ecff9913305936fd23e02f724d19344b6c6a4961 Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Tue, 21 Jul 2026 13:30:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=87=AA=E4=B8=BB=E8=AF=95?= =?UTF-8?q?=E7=8E=A9=E5=A4=B1=E8=B4=A5=E7=8A=B6=E6=80=81=E6=8C=81=E4=B9=85?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 持久化当前 revision 的试玩失败标记并驱动总控进入源码修复 在试玩通过后安全清除失败凭证并补充上下文压缩回归测试 --- .../src-tauri/src/agent.rs | 76 ++++++- .../src-tauri/src/tests.rs | 201 ++++++++++++++++++ 2 files changed, 276 insertions(+), 1 deletion(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index c49c6eb80..00d1635fd 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -10726,6 +10726,8 @@ pub(crate) struct AgentRuntimeVerificationGate { pub(crate) last_mutation_tool: Option, pub(crate) last_verification_tool: Option, pub(crate) last_verification_status: Option, + #[serde(default)] + pub(crate) failed_playtest_revision: Option, pub(crate) updated_at: u64, } @@ -17396,6 +17398,7 @@ fn default_agent_runtime_verification_gate( last_mutation_tool: None, last_verification_tool: None, last_verification_status: None, + failed_playtest_revision: None, updated_at: 0, }) } @@ -17460,6 +17463,18 @@ fn validate_agent_runtime_verification_gate( { return Err("Agent Runtime verification gate 的 verifiedRevision 早于修改".to_string()); } + if gate.failed_playtest_revision.is_some_and(|revision| revision == 0) { + return Err("Agent Runtime verification gate 的 failedPlaytestRevision 必须大于 0".to_string()); + } + if gate + .failed_playtest_revision + .zip(gate.mutation_revision) + .is_some_and(|(failed_playtest, mutation)| failed_playtest > mutation) + { + return Err( + "Agent Runtime verification gate 的 failedPlaytestRevision 晚于当前修改".to_string(), + ); + } if gate.last_mutation_tool.as_deref().is_some_and(|tool| { !matches!( tool, @@ -21363,6 +21378,7 @@ pub(crate) fn prepare_agent_runtime_project_mutation_locked( gate.last_mutation_tool = Some(tool.to_string()); gate.last_verification_tool = None; gate.last_verification_status = None; + gate.failed_playtest_revision = None; gate.updated_at = now; if let Err(error) = write_game_creator_agent_runtime_verification_gate(root, &gate) { revision.revision = next_revision; @@ -21409,9 +21425,13 @@ pub(crate) fn finish_agent_runtime_project_verification_locked( let current_revision = read_game_creator_agent_runtime_project_revision(root)?; let revision_unchanged = current_revision.revision == expected_revision.revision; let passed = passed && revision_unchanged; + let verification_tool = gate.last_verification_tool.clone(); gate.verified_revision = passed .then_some(current_revision.revision) .filter(|value| *value > 0); + if passed && verification_tool.as_deref() == Some("preview.validate") { + gate.failed_playtest_revision = None; + } gate.last_verification_status = Some( if passed { AGENT_RUNTIME_VERIFICATION_STATUS_PASSED @@ -21431,6 +21451,30 @@ pub(crate) fn finish_agent_runtime_project_verification_locked( Ok(()) } +fn clear_agent_runtime_failed_playtest_at( + root: &Path, + agent_id: &str, + run_id: &str, + expected_revision: u64, +) -> Result<(), String> { + let _lock = + acquire_game_creator_agent_runtime_project_write_lock_with_wait(root, "preview.validate")?; + let current_revision = read_game_creator_agent_runtime_project_revision(root)?; + if current_revision.revision != expected_revision { + return Err(format!( + "浏览器试玩通过后项目 revision 已从 {expected_revision} 变化为 {}", + current_revision.revision + )); + } + let mut gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; + if gate.mutation_revision != Some(expected_revision) { + return Err("浏览器试玩通过时没有当前 revision 的项目修改凭证".to_string()); + } + gate.failed_playtest_revision = None; + gate.updated_at = unix_timestamp(); + write_game_creator_agent_runtime_verification_gate(root, &gate) +} + pub(crate) fn invalidate_agent_runtime_project_verification_after_preview_failure_at( root: &Path, agent_id: &str, @@ -21454,6 +21498,7 @@ pub(crate) fn invalidate_agent_runtime_project_verification_after_preview_failur gate.verified_revision = None; gate.last_verification_tool = Some("preview.validate".to_string()); gate.last_verification_status = Some(AGENT_RUNTIME_VERIFICATION_STATUS_FAILED.to_string()); + gate.failed_playtest_revision = Some(expected_revision); gate.updated_at = unix_timestamp(); write_game_creator_agent_runtime_verification_gate(root, &gate) } @@ -28217,6 +28262,21 @@ fn validate_agent_runtime_autonomous_plan_liveness( .iter() .any(|action| is_supervisor_orchestrator_project_mutation_tool(&action.tool)); if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { + if verification_gate + .failed_playtest_revision + .zip(verification_gate.mutation_revision) + .is_some_and(|(failed_playtest_revision, mutation_revision)| { + failed_playtest_revision == mutation_revision + }) + && !has_mutation + { + return Err(format!( + "{AGENT_RUNTIME_AUTONOMOUS_FAILED_PLAYTEST_LIVENESS_ERROR_PREFIX};持久验证门仍标记 revision {} 的试玩失败,即使上下文压缩后也必须根据最近一次试玩诊断直接修改 game/index.html;不得继续只更新计划、读取、搜索、重复验证、查询状态、委派或返回最终回复", + verification_gate + .failed_playtest_revision + .unwrap_or_default() + )); + } let Some(failed_playtest_index) = observations.iter().rposition(|observation| { observation.tool == "preview.validate" && observation.status == "failed" @@ -35421,7 +35481,7 @@ async fn observe_agent_runtime_preview_validate( detail: None, }; }; - match write_autonomous_playtest_receipt_at( + let receipt = match write_autonomous_playtest_receipt_at( root, contract, action_id, @@ -35438,7 +35498,21 @@ async fn observe_agent_runtime_preview_validate( detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), }; } + }; + if let Err(error) = clear_agent_runtime_failed_playtest_at( + root, + agent_id, + run_id, + revision_after.revision, + ) { + return AgentRuntimeToolObservation { + tool: "preview.validate".to_string(), + status: "failed".to_string(), + summary: "浏览器验证已通过,但失败试玩凭证无法安全清除".to_string(), + detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), + }; } + receipt } } else { None diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/tests.rs index 2a7a69e98..0bc8277bb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -7587,6 +7587,188 @@ async fn autonomous_game_build_repairs_supervisor_failed_playtest_stall_into_mut fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn autonomous_game_build_repairs_persisted_failed_playtest_after_context_compaction() { + let root = unique_project_path(); + init_local_game_project_at( + &root, + "project-autonomous-supervisor-persisted-playtest-liveness", + "自主构建总控持久试玩失败活性测试", + ) + .expect("project init"); + write_supervisor_collaboration_policy_at(&root, SupervisorCollaborationPolicy::default()) + .expect("disable unrelated collaboration preflight"); + let run_id = "autonomous-supervisor-persisted-playtest-liveness-run"; + bind_game_creator_agent_runtime_run_profile_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + None, + ) + .expect("bind autonomous supervisor profile"); + let runtime = start_game_creator_agent_runtime_task_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "修复试玩失败并完成可玩塔防", + run_id, + AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, + "根据试玩诊断直接修复", + vec!["修复试玩失败".to_string(), "重新验证并交付".to_string()], + ) + .expect("start autonomous supervisor runtime"); + + let revision = prepare_agent_runtime_project_mutation_locked( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + "file.write", + ) + .expect("record project mutation"); + let (expected_revision, gate) = begin_agent_runtime_project_verification_locked( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + "game.static_smoke", + ) + .expect("begin initial static smoke"); + finish_agent_runtime_project_verification_locked(&root, &expected_revision, gate, true) + .expect("finish initial static smoke"); + invalidate_agent_runtime_project_verification_after_preview_failure_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + revision, + ) + .expect("persist failed browser playtest"); + + // Simulate a later context window that only remembers the generic static check. + let (expected_revision, gate) = begin_agent_runtime_project_verification_locked( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + "game.static_smoke", + ) + .expect("begin static smoke after compaction"); + finish_agent_runtime_project_verification_locked(&root, &expected_revision, gate, true) + .expect("finish static smoke after compaction"); + let gate = read_game_creator_agent_runtime_verification_gate( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + ) + .expect("read persisted failed playtest gate"); + assert_eq!(gate.failed_playtest_revision, Some(revision)); + + let (sender, receiver) = mpsc::channel(); + let read_arguments = serde_json::json!({"reason": "继续读取而不修复", "input": {}}).to_string(); + let patch_arguments = serde_json::json!({ + "reason": "根据持久试玩诊断直接修复", + "input": { + "path": "game/index.html", + "oldText": "ctx.", + "newText": "ctx.fillRect(0,0,1,1);" + } + }) + .to_string(); + let base_url = spawn_mock_llm_raw_responses_with_capture( + vec![ + native_agent_tool_plan_chat_response( + "call-autonomous-supervisor-persisted-playtest-read", + &native_runtime_function_name("project.index").expect("index function"), + read_arguments, + ), + native_agent_tool_plan_chat_response( + "call-autonomous-supervisor-persisted-playtest-patch", + &native_runtime_function_name("file.patch").expect("patch function"), + patch_arguments, + ), + ], + Some(sender), + ); + let _config_guard = write_test_local_config(format!( + r#"{{ + "agentLlm": {{ + "project-supervisor": {{ + "apiKey": "autonomous-supervisor-persisted-playtest-repair-key", + "baseUrl": {base_url:?}, + "model": "autonomous-supervisor-persisted-playtest-repair-model", + "apiKind": "openai_chat", + "maxRetries": 0 + }} + }} +}}"# + )); + let observations = (0..AGENT_RUNTIME_AUTONOMOUS_LIVENESS_OBSERVATION_LIMIT) + .map(|_| AgentRuntimeToolObservation { + tool: "runtime.plan_update".to_string(), + status: "blocked".to_string(), + summary: "结构化计划尚未完成".to_string(), + detail: None, + }) + .collect::>(); + let plan = request_game_creator_agent_background_tool_plan_for_test( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &runtime.session_id, + run_id, + &runtime.current_task, + &observations, + 30, + 0, + ) + .await + .expect("repair persisted failed playtest stall") + .expect("repaired persisted supervisor mutation plan"); + assert_eq!(plan.actions.len(), 1); + assert_eq!(plan.actions[0].tool, "file.patch"); + + receiver + .recv_timeout(Duration::from_secs(2)) + .expect("initial persisted stalled supervisor request"); + let repair_request = receiver + .recv_timeout(Duration::from_secs(2)) + .expect("persisted failed playtest mutation repair request"); + assert!(repair_request.contains("持久验证门仍标记")); + assert!(repair_request.contains("不得继续只更新计划、读取、搜索")); + let repair_request_json = mock_http_request_json(&repair_request); + let repair_function_names = repair_request_json["tools"] + .as_array() + .expect("restricted persisted failed playtest repair tools") + .iter() + .filter_map(|tool| { + tool.get("name") + .and_then(serde_json::Value::as_str) + .or_else(|| { + tool.get("function") + .and_then(|function| function.get("name")) + .and_then(serde_json::Value::as_str) + }) + }) + .collect::>(); + assert_eq!( + repair_function_names, + BTreeSet::from([ + native_runtime_function_name("file.write") + .expect("write function") + .as_str(), + native_runtime_function_name("file.patch") + .expect("patch function") + .as_str(), + native_runtime_function_name("file.delete") + .expect("delete function") + .as_str(), + native_runtime_function_name("project.patchset") + .expect("patchset function") + .as_str(), + ]) + ); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); + + fs::remove_dir_all(root).ok(); +} + #[test] fn failed_autonomous_preview_invalidates_static_smoke_verification_gate() { let root = unique_project_path(); @@ -7633,6 +7815,25 @@ fn failed_autonomous_preview_invalidates_static_smoke_verification_gate() { Some("preview.validate") ); assert_eq!(gate.last_verification_status.as_deref(), Some("failed")); + assert_eq!(gate.failed_playtest_revision, Some(revision)); + + // A later static smoke pass must not erase the durable interactive failure. + let (expected_revision, gate) = begin_agent_runtime_project_verification_locked( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + "game.static_smoke", + ) + .expect("begin static smoke after failed playtest"); + finish_agent_runtime_project_verification_locked(&root, &expected_revision, gate, true) + .expect("finish static smoke after failed playtest"); + let gate = read_game_creator_agent_runtime_verification_gate( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + ) + .expect("read gate after static smoke"); + assert_eq!(gate.failed_playtest_revision, Some(revision)); fs::remove_dir_all(root).ok(); }