把 Fast GDD 审批等待的可恢复判据钉成不变量

判据在 task record 的 status 而不是 phase:审批等待只改 phase、保留
status=running,恢复扫描仍会拉起;真正的澄清等待才会把 status 一并写成
waiting-for-user-input。一旦有人把审批等待也写成后者,审批命令的通用 wake
会静默变成 no-op,用户点完批准/修改/退回不会有任何东西继续跑,且无报错。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 03:28:24 +00:00
parent 360cedf4c0
commit c89a13e682
@@ -1770,3 +1770,76 @@ mod orphaned_external_generation_recovery_tests {
);
}
}
#[cfg(test)]
mod plan_gdd_approval_wait_recovery_tests {
use super::*;
/// Fast GDD 审批等待期,根 Supervisor 到底还能不能被恢复扫描拉起。
///
/// 判据不在 `phase` 上:`read_recoverable_game_creator_agent_runtime_task` 按
/// task record 的 **status** 分类,而 record 的 status 由
/// `game_creator_agent_runtime_task_status` 从 `state.status` 推导。审批等待
/// `main_loop` 的 `runtime.plan_gdd` 分支)只改 phase、保留 `status="running"`
/// 真正的澄清等待(`action_projection`)才会把 status 一并写成
/// `waiting-for-user-input`,那才是恢复扫描要让路的外部输入等待。
///
/// 这个区别决定了审批决定之后还有没有生产路径驱动父 run:审批命令只调通用
/// wake,一旦有人把审批等待也写成 `status="waiting-for-user-input"`,通用 wake
/// 会静默变成 no-op,用户点了批准/修改/退回之后不会有任何东西继续跑。这条测试
/// 把这个区别钉成不变量。
#[test]
fn plan_gdd_approval_wait_stays_recoverable_while_real_user_input_wait_does_not() {
let temporary = crate::tests::canonical_test_tempdir("plan-gdd-approval-wait-recovery-");
let root = temporary.path();
init_local_game_project_at(root, "plan-gdd-approval-wait", "Fast GDD 审批等待恢复判定")
.expect("init project");
let mut runtime = start_game_creator_agent_runtime_task_at(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"推进立项策划",
"plan-gdd-approval-wait-run",
"agent-ready-task-scheduler",
"准备推进立项策划",
vec!["推进立项策划".to_string()],
)
.expect("start plan-root supervisor runtime");
// main_loop.rs 的 Fast GDD 审批等待形状。
runtime.status = "running".to_string();
runtime.phase = "waiting-for-user-input".to_string();
runtime.current_action = "等待 Fast GDD 审批决定".to_string();
runtime.waiting_on = "用户在审批卡选择批准、修改或退回".to_string();
runtime.updated_at = unix_timestamp();
append_game_creator_agent_runtime_task(root, &runtime).expect("append approval wait task");
refresh_game_creator_agent_runtime_task_queue(root, &mut runtime)
.expect("refresh approval wait task queue");
write_game_creator_agent_runtime_state(root, &runtime).expect("write approval wait state");
let task =
read_recoverable_runnable_game_creator_agent_runtime_task(root, &runtime.agent_id)
.expect("read approval wait task")
.expect("审批等待态必须仍可被恢复扫描拉起,否则审批决定后没有生产路径驱动父 run");
assert_eq!(task.status, "running");
assert_eq!(task.phase, "waiting-for-user-input");
assert!(has_recoverable_game_creator_agent_background_tasks_at(root)
.expect("public preflight must agree with the recoverable task read"));
// 对照组:真正的用户输入等待把 status 一并写成 waiting-for-user-input。
runtime.status = "waiting-for-user-input".to_string();
runtime.updated_at = unix_timestamp();
append_game_creator_agent_runtime_task(root, &runtime)
.expect("append user input wait task");
refresh_game_creator_agent_runtime_task_queue(root, &mut runtime)
.expect("refresh user input wait task queue");
write_game_creator_agent_runtime_state(root, &runtime)
.expect("write user input wait state");
assert!(
read_recoverable_runnable_game_creator_agent_runtime_task(root, &runtime.agent_id)
.expect("read user input wait task")
.is_none(),
"status 写成 waiting-for-user-input 才是恢复扫描让路的外部输入等待"
);
}
}