修复自主试玩失败状态持久化

持久化当前 revision 的试玩失败标记并驱动总控进入源码修复

在试玩通过后安全清除失败凭证并补充上下文压缩回归测试
This commit is contained in:
AIGameCreator App
2026-07-21 13:30:16 +08:00
parent 9bb048c11b
commit ecff991330
2 changed files with 276 additions and 1 deletions
@@ -10726,6 +10726,8 @@ pub(crate) struct AgentRuntimeVerificationGate {
pub(crate) last_mutation_tool: Option<String>,
pub(crate) last_verification_tool: Option<String>,
pub(crate) last_verification_status: Option<String>,
#[serde(default)]
pub(crate) failed_playtest_revision: Option<u64>,
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
@@ -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);</script></body></html>"
}
})
.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::<Vec<_>>();
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::<BTreeSet<_>>();
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();
}