修复 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:
@@ -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 是空的——绑定说这是委派子 run,task 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 之前停止"
|
||||
|
||||
Reference in New Issue
Block a user