修复 HEAD 既存红:策划恢复门用例改建可达状态

planning_recovery_contains_session_identity_conflict_before_provider 自 M1C-2b
(152cc40c7)写下起一天都没绿过:它伪造一个 source=agent-background-task、且没有
Run Profile 绑定的策划任务,而 agent_runtime_tool_policy_snapshot_for_run_at 早在
M1A-2(09c7d7af8,152cc40c7 的祖先,已核对该门在 152cc40c7 当时就一模一样)就按
agent 身份挡死了这种 run。被告造不出来,庭开不了。当时的定向门禁过滤器
planning_clarification_* 恰好匹配不到它的名字,加上本 crate 不在 CI 里编译,两层
网都没接住。

那道创建门不能放宽:放宽后伪造的策划 run 就能拿到默认宽工具面,正是 M1A-2 关掉的
洞。所以改测试而不是改生产代码。

也不删:该用例第四条断言「身份门必须早于任何 Provider lifecycle」是全 crate 唯一
一处该不变量的否定断言(AGENT_RUNTIME_PROVIDER_REQUEST_LIFECYCLE_RECORD_TYPE 共
22 处引用,否定断言仅此一处),且 plan_session_recovery_gate_tests 模块只有这一条
用例,删掉等于整个门失去覆盖。

先试了更贴现实的「事后删父绑定」,实测走不到本门:entrypoints 的 tool policy 收容
(currentAction=Run Profile 工具策略需要人工核对)先把 run 打成 failed,
read_recoverable_... 随即不再认它,审计里只有 agent.runtime.turn。也就是说绑定层的
破坏到不了 session 门;能到这里的只有「绑定完好、task record 自身不满足 D11 契约」
这一类持久不一致——这正是本门存在的理由。

- 改成:supervisor plan 根与 planning child 绑定都合法(创建门放行),但裸 start
  不带 task link,task record 上没有 parentAgentId/delegationId
- 四条断言原样保留:resume 必须 Ok(收容不外溢)、该 run failed/needs-reconciliation
  且 error 含 PLAN_SOURCE_PROFILE_MISMATCH、审计有 plan.session_recovery.
  needs_reconciliation、且没有任何 Provider lifecycle 记录
- 断言失败信息带上实际审计记录类型与 currentAction,下次判因不用再加探针
- 变异验证:把 exact_plan_child_identity_at 的策划识别翻成直接 Ok(None),用例报出
  "PLAN_NEEDS_RECONCILIATION: project-planning 恢复任务未命中 planning session 协调器"
  ——证明它确实在考这道门,而不是碰巧绿
- recovery 107/0、planning_ 151/0、session 88/0、plan_ 160/0、approval 11/0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 06:02:33 +00:00
parent 73816bd512
commit 6234b420fc
@@ -2176,6 +2176,21 @@ mod plan_gdd_approval_wait_recovery_tests {
mod plan_session_recovery_gate_tests {
use super::*;
/// 这条用例原来伪造一个 `source=agent-background-task`、且**没有 Run Profile
/// 绑定**的策划任务。但 `agent_runtime_tool_policy_snapshot_for_run_at` 早在
/// `M1A-2``09c7d7af8`)就按 agent 身份挡死了这种 run——那道门是刻意的
/// fail-closed 收窄(放宽它,伪造的策划 run 就能拿到默认宽工具面),于是被告
/// 根本造不出来。用例自 `M1C-2b` 写下起一天都没绿过,只是当时的定向门禁过滤器
/// `planning_clarification_*`)恰好匹配不到它的名字。
///
/// 重写时试过「事后删掉父绑定」这种更贴现实的破坏方式,实测走不到本门:读路径
/// 的 tool policy 收容(`entrypoints.rs` 里 `Run Profile 工具策略需要人工核对`)
/// 会先把 run 打成 failed`read_recoverable_...` 随即不再认它。也就是说**绑定
/// 层的破坏根本到不了 session 门**——能到这里的只有「绑定完好、task record 自身
/// 不满足 D11 契约」这一类持久不一致,这正是本门存在的理由。
///
/// 所以改成:绑定全部合法(创建门放行),但 task record 缺 `delegationId`
/// (裸 start 不带 task link 时就是这个形状)。合同一字未改。
#[test]
fn planning_recovery_contains_session_identity_conflict_before_provider() {
let temporary =
@@ -2183,13 +2198,40 @@ mod plan_session_recovery_gate_tests {
let root = temporary.path();
init_local_game_project_at(root, "plan-recovery-gate", "策划恢复门冲突收敛")
.expect("init project");
let parent_run_id = "plan-session-recovery-conflict-root";
let child_run_id = "plan-session-recovery-conflict-run";
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 supervisor plan root");
bind_game_creator_agent_runtime_run_profile_at(
root,
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
child_run_id,
"agent-delegate",
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: Some("plan-session-recovery-conflict-delegation".to_string()),
}),
)
.expect("bind delegated planning child");
// 裸 start 的 task link 取自已排队的 record,这里没有,于是 task record 上
// 的 parentRunId/delegationId 是空的——绑定说这是委派子 runtask record 却
// 讲不出自己属于哪次委派。
start_game_creator_agent_runtime_task_at(
root,
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
"不具备 Supervisor 委派身份的伪造策划任务",
"plan-session-recovery-conflict-run",
"agent-background-task",
"准备执行伪造策划任务",
"缺少委派链的策划任务",
child_run_id,
"agent-delegate",
"准备执行策划任务",
vec!["不得发起 Provider 请求".to_string()],
)
.expect("persist recoverable planning task");
@@ -2202,14 +2244,24 @@ mod plan_session_recovery_gate_tests {
.expect("planning task is returned as contained");
assert_eq!(contained.state.status, "failed");
assert_eq!(contained.state.phase, "needs-reconciliation");
assert!(contained
.state
.error
.as_deref()
.is_some_and(|error| error.contains("PLAN_SOURCE_PROFILE_MISMATCH")));
let contained_error = contained.state.error.as_deref().unwrap_or_default();
assert!(
contained_error.contains("PLAN_SOURCE_PROFILE_MISMATCH"),
"身份冲突必须落在 planning coordinator 的类型化合同里,实际为:{contained_error}"
);
let public_audit = fs::read_to_string(root.join(".agent/agent.db")).unwrap_or_default();
assert!(public_audit.contains("agent.runtime.plan.session_recovery.needs_reconciliation"));
let record_types = public_audit
.split("\"recordType\"")
.skip(1)
.filter_map(|chunk| chunk.split('"').nth(1).map(str::to_string))
.collect::<std::collections::BTreeSet<_>>();
assert!(
public_audit.contains("agent.runtime.plan.session_recovery.needs_reconciliation"),
"必须由 session 恢复门收容,实际审计记录类型:{record_types:?}\
currentAction={}",
contained.state.current_action
);
assert!(
!public_audit.contains(AGENT_RUNTIME_PROVIDER_REQUEST_LIFECYCLE_RECORD_TYPE),
"session identity 冲突必须在任何 Provider lifecycle 之前停止"