修复自主构建测试项目锁竞态
Project CI / Repository checks (push) Successful in 1m21s
Project CI / Frontend tests (push) Successful in 3m8s
Project CI / Backend tests (push) Successful in 4m24s
Project CI / Native shell tests (push) Successful in 16m33s

将纯继续契约测试改为异步并等待子任务持久化终态。

等待 Agent lane 释放后再执行后续 manifest 写入,避免后台终态投影竞争项目锁。
This commit is contained in:
2026-08-12 11:08:53 +08:00
parent 03a4410272
commit 51e35468a5
2 changed files with 67 additions and 3 deletions
@@ -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",
@@ -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,