审查发现2:验收前置门先识别 Fast GDD 再走绑定父链

ensure_plan_gdd_approval_pending_after_acceptance_locked 先用会遍历并校验整条
祖先链的 read_..._run_profile_binding 读绑定,再检查 binding.source 是否为
project-supervisor-plan。顺序反了:任何祖先绑定的毛病都抢先变成 Err,连「这根本
不是策划根」都来不及说,于是别人的坏链变成了这道门的失败。

这正是 P4(cb5af31e7)在同文件 plan_root_completion_identity_at 修过的形状,当时
漏了这个姊妹函数。而它比完成门更容易被踩到:agent.run_status 每次 status
observation 都会重跑本门(见 runtime_tools/run_status.rs 的
「Re-run the locked gate on every plan-root status observation」),那条路径上没有
任何上游守卫先把带父链的 run 挡掉,适用性完全交给门自己判。

- 识别改用 _once,只看 run 自己那条绑定记录;确认是策划根之后才走完整父链,
  严格度一点没降,只是不再作用到别人身上
- 回归 broken_ancestor_binding_must_not_fail_the_acceptance_gate_for_a_non_plan_supervisor_run:
  复用 P4 的构造(supervisor 的 isolated-join 子 run,删掉父绑定),断言门只能判
  NotApplicable。变异验证:换回链走优先后该用例报出
  Err("Agent Runtime Run Profile 父绑定缺失")

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 05:33:52 +00:00
parent 7e3fd411a1
commit c1a70cb37d
@@ -302,8 +302,14 @@ pub(crate) fn ensure_plan_gdd_approval_pending_after_acceptance_locked(
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID || run_id.trim().is_empty() {
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
}
// 先识别、后校验。`read_..._binding`(不带 `_once`)会遍历并校验整条祖先链,
// 把它排在识别前面,等于让任何祖先绑定的毛病都抢先变成 Err——连「这根本不是
// 策划根」都来不及说,于是别人的坏链变成了这道门的失败。`agent.run_status`
// 每次都会重跑本门,带父链的非策划 supervisor run 因此会被误伤。P4
// `cb5af31e7`)已在同文件的 `plan_root_completion_identity_at` 修过同一形状,
// 当时漏了这个姊妹函数。识别只看 run 自己那条绑定记录。
let Some(binding) =
read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?
read_game_creator_agent_runtime_run_profile_binding_once(root, agent_id, run_id.trim())?
else {
return Ok(PlanGddAcceptanceGateOutcome::NotApplicable);
};
@@ -313,6 +319,8 @@ pub(crate) fn ensure_plan_gdd_approval_pending_after_acceptance_locked(
if binding.profile != AGENT_RUNTIME_RUN_PROFILE_STANDARD {
return Err("Fast GDD acceptance gate 的 plan 根 Run Profile 已漂移".to_string());
}
// 确认是策划根之后才走完整父链:严格度一点没降,只是不再作用到别人身上。
read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?;
let gdds = read_plan_gdd_chain_locked(root).map_err(|error| error.to_string())?;
let Some(gdd) = latest_plan_gdd_for_root(&gdds, run_id) else {
// A contract may be updated before the planning child has submitted a
@@ -2167,4 +2175,68 @@ mod tests {
);
let _ = fs::remove_dir_all(root);
}
/// 同一形状的第二处:验收前置门。它比完成门更容易被踩到——`agent.run_status`
/// 每次 status observation 都会重跑这道门(见 `runtime_tools/run_status.rs` 的
/// 「Re-run the locked gate on every plan-root status observation」),而那条路径
/// 上没有任何上游守卫先把带父链的 run 挡掉,适用性完全交给门自己判。所以门必须
/// 先说得出「这不是策划根」,才轮到校验祖先链。
#[test]
fn broken_ancestor_binding_must_not_fail_the_acceptance_gate_for_a_non_plan_supervisor_run() {
let root = std::env::temp_dir().join(format!(
"genarrative-plan-acceptance-gate-identity-{}",
uuid::Uuid::new_v4().simple()
));
init_local_game_project_at(
&root,
"project-test-acceptance-gate",
"acceptance gate identity",
)
.expect("init project");
let parent_run_id = "acceptance-gate-isolated-join-parent";
let child_run_id = "acceptance-gate-isolated-join-child";
bind_game_creator_agent_runtime_run_profile_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD),
None,
)
.expect("bind ancestor run");
bind_game_creator_agent_runtime_run_profile_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
child_run_id,
AGENT_RUNTIME_ISOLATED_JOIN_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD),
Some(&AgentRuntimeTaskLink {
parent_agent_id: Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string()),
parent_run_id: Some(parent_run_id.to_string()),
delegation_id: None,
}),
)
.expect("bind isolated-join child run");
fs::remove_file(game_creator_agent_runtime_run_profile_binding_path(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
))
.expect("remove ancestor binding");
let outcome = ensure_plan_gdd_approval_pending_after_acceptance_locked(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
child_run_id,
);
assert!(
matches!(outcome, Ok(PlanGddAcceptanceGateOutcome::NotApplicable)),
"isolated-join run 的 source 不是立项策划,验收前置门只能判 NotApplicable\
不能因为别人的祖先链坏掉就让整个 agent.run_status 失败:{:?}",
outcome.map(|outcome| format!("{outcome:?}"))
);
let _ = fs::remove_dir_all(root);
}
}