合并 master 的自主构建测试锁竞态修复
Project CI / Repository checks (pull_request) Successful in 1m30s
Project CI / Frontend tests (pull_request) Successful in 3m57s
Project CI / Backend tests (pull_request) Successful in 4m41s
Project CI / Native shell tests (pull_request) Successful in 16m59s

冲突在 game_chat_pure_continue_inherits_failed_root_semantics_and_manifest_progress:
两侧是同一竞态的互斥策略。M0A-2 持有 code-prototype 的 lane 锁让 child
起不来,再释放;master 改为让 child 跑到终态并等待其持久 manifest 投影,
为此把用例改成 tokio::test。

取 master 方案并移除本侧 lane 锁:两者并存会自相矛盾——锁着 lane 时 child
到不了终态,等待必然空转。且如 master 注释所述,lane 释放本身可能早于迟到
的 manifest 投影,仅靠 lane 锁并不能保证投影已落地。

验证:game_chat_pure_continue_inherits_failed_root_semantics_and_manifest_progress
与 agent_runtime_reject_revalidates_pending_action_after_lock_wait 均通过,
后者正是本次 master 修复针对的 CI 失败用例。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 03:54:20 +00:00
2 changed files with 67 additions and 8 deletions
@@ -1011,8 +1011,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(
@@ -1170,10 +1170,6 @@ fn game_chat_pure_continue_inherits_failed_root_semantics_and_manifest_progress(
"继续完成继承的现有游戏目标",
)
.expect("persist continued root Supervisor decision");
let code_runtime_lock =
try_acquire_game_creator_agent_runtime_task_lock(&root, "code-prototype")
.expect("acquire continued code-prototype runtime lane")
.expect("continued code-prototype runtime lane is free");
let scheduled = schedule_autonomous_game_build_ready_tasks_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
@@ -1183,7 +1179,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");
drop(code_runtime_lock);
// 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",
@@ -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,