测试:实证静态委派澄清 continuation 只能走一跳

新增 clarification_continuation_chain_supports_multiple_rounds,走真实
observe_agent_runtime_agent_delegate 生产路径验证 #165 澄清中转的链长边界:

- D1 -> D2 第一跳成功,D2 落盘且 repair_of_delegation_id == Some(D1)
- D2 -> D3 第二跳被 delegation.rs:1119 的「静态委派返工深度最多为 1」拒绝
- D3 从未落盘

澄清 continuation 与质量返工共用 repair_of_delegation_id 字段和同一道深度门,
因此一次澄清会同时耗尽该链路唯一一次质量返工额度。本用例把当前行为钉死,
作为后续拆分两个维度时的对照基线。

仅新增测试,未改动任何生产代码。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 02:27:06 +00:00
parent 93d8d0c1cf
commit 7754a2f820
@@ -2655,3 +2655,352 @@ async fn project_supervisor_waiting_state_survives_agent_db_audit_failure() {
fs::remove_dir_all(root).ok();
}
#[test]
fn clarification_continuation_chain_supports_multiple_rounds() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-clarification-chain", "链式多轮澄清测试")
.expect("project init");
let run_id = "project-supervisor-clarification-chain-run";
let mut state = start_game_creator_agent_runtime_task_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"协调专业 Agent 完成玩法方案",
run_id,
"agent-chat",
"协调专业 Agent",
vec!["取得用户多轮澄清后继续专业委派".to_string()],
)
.expect("start supervisor run");
let acceptance_criteria = vec!["明确首发平台与美术风格".to_string()];
let expected_artifacts: Vec<String> = vec![];
let target_agent_id = "design-director";
// ---------- 第 1 轮:D1repair_of=None-> 子 Agent 澄清终态 -> 认领 -> 用户回答 -> 真实
// continuation 派发出 D2 ----------
let d1_delegation_id = "clarification-chain-round1-delivery";
let d1_delivery = new_static_delegate_delivery_with_contract(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&state.session_id,
run_id,
"clarification-chain-round1-delegate-action",
d1_delegation_id,
target_agent_id,
"clarification-chain-round1-child-session",
"clarification-chain-round1-child-run",
&acceptance_criteria,
&expected_artifacts,
None,
);
create_or_read_static_delegate_delivery_at(&root, &d1_delivery).expect("create D1 delivery");
let round1_result = build_static_delegate_structured_result_at(
&root,
"completed",
&expected_artifacts,
false,
None,
None,
None,
Some(concat!(
"AGC_NEEDS_USER_INPUT_V1\n",
r#"{"questions":[{"id":"target_platform","header":"首发平台","question":"首版优先发布到哪个平台?","options":[{"label":"Web","description":"优先浏览器交付。"},{"label":"桌面端","description":"优先桌面客户端交付。"}]}]}"#
)),
)
.expect("build D1 needs-user-input result");
mark_static_delegate_delivery_ready_with_result_at(
&root,
&d1_delivery.target_agent_id,
&d1_delivery.target_session_id,
&d1_delivery.target_run_id,
d1_delegation_id,
"completed",
"需要用户确认首发平台",
round1_result,
)
.expect("mark D1 ready");
claim_ready_static_delegate_receipts_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
"clarification-chain-round1-claim-action",
)
.expect("claim D1 receipt");
let round1_deliveries = claimed_static_delegate_deliveries_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
)
.expect("read round1 claimed deliveries");
assert!(
ensure_static_delegate_user_input_wait_at(&root, &mut state, &round1_deliveries)
.expect("create round1 supervisor wait")
);
let round1_pending = read_game_creator_agent_runtime_pending_tool_action(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
)
.expect("read round1 pending action");
assert!(round1_pending.task.contains(d1_delegation_id));
let round1_request =
match prepare_game_creator_agent_user_input_request_at(&root, &round1_pending)
.expect("prepare round1 clarification request")
{
AgentRuntimeUserInputRecovery::Waiting(request) => request,
other => panic!("unexpected round1 clarification recovery: {other:?}"),
};
let (_, round1_observation) = answer_game_creator_agent_user_input_request_for_pending_at(
&root,
&round1_pending,
&round1_request.request_id,
"clarification-chain-round1-response",
BTreeMap::from([("target_platform".to_string(), "Web".to_string())]),
)
.expect("answer round1 clarification request");
let round1_detail = serde_json::from_str::<Value>(
round1_observation
.detail
.as_deref()
.expect("round1 answer detail"),
)
.expect("parse round1 answer detail");
let round1_questions_sha = round1_detail["questionsSha256"]
.as_str()
.expect("round1 questions sha")
.to_string();
let round1_answers_sha = round1_detail["answersSha256"]
.as_str()
.expect("round1 answers sha")
.to_string();
let pending_path = game_creator_agent_runtime_pending_tool_action_path(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
);
fs::remove_file(&pending_path).expect("clear round1 answered pending before next planning");
let round1_continuation_input = serde_json::json!({
"continuationOfDelegationId": d1_delegation_id,
"questionsSha256": round1_questions_sha,
"answersSha256": round1_answers_sha,
});
let round1_identity = validate_static_delegate_clarification_continuation_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
&round1_continuation_input,
Some(d1_delegation_id),
)
.expect("validate round1 continuation binding")
.expect("derived round1 continuation identity");
let target_lock = try_acquire_game_creator_agent_runtime_task_lock(&root, target_agent_id)
.expect("acquire clarification chain target lane")
.expect("clarification chain target lane available");
let round1_delegate_input = serde_json::json!({
"agentId": target_agent_id,
"task": "根据用户确认的 Web 首发平台继续完成原方案",
"acceptanceCriteria": acceptance_criteria,
"expectedArtifacts": expected_artifacts,
"repairOfDelegationId": d1_delegation_id,
"runId": null,
"continuationOfDelegationId": d1_delegation_id,
"questionsSha256": round1_questions_sha,
"answersSha256": round1_answers_sha,
});
let round1_continuation_observation = observe_agent_runtime_agent_delegate(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
Some("clarification-chain-round1-continuation-action"),
&round1_delegate_input,
);
assert_eq!(
round1_continuation_observation.status, "ok",
"{round1_continuation_observation:?}"
);
let d2_delegation_id = agent_runtime_delegation_id(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
target_agent_id,
&round1_identity,
);
let d2_delivery = read_static_delegate_delivery_at(&root, &d2_delegation_id)
.expect("read D2 delivery")
.expect("D2 delivery exists");
assert_eq!(
d2_delivery.repair_of_delegation_id.as_deref(),
Some(d1_delegation_id),
"D2 必须记录自己续接自 D1,这正是第 2 轮返工深度校验会读到的字段"
);
// ---------- 第 2 轮:D2repair_of=D1-> 子 Agent 再次澄清终态 -> 认领 -> 用户回答 ----------
let round2_result = build_static_delegate_structured_result_at(
&root,
"completed",
&expected_artifacts,
false,
None,
None,
None,
Some(concat!(
"AGC_NEEDS_USER_INPUT_V1\n",
r#"{"questions":[{"id":"art_style","header":"美术风格","question":"角色美术走哪种风格?","options":[{"label":"写实","description":"偏写实渲染。"},{"label":"卡通","description":"偏卡通渲染。"}]}]}"#
)),
)
.expect("build D2 needs-user-input result");
mark_static_delegate_delivery_ready_with_result_at(
&root,
&d2_delivery.target_agent_id,
&d2_delivery.target_session_id,
&d2_delivery.target_run_id,
&d2_delegation_id,
"completed",
"需要用户确认美术风格",
round2_result,
)
.expect("mark D2 ready");
claim_ready_static_delegate_receipts_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
"clarification-chain-round2-claim-action",
)
.expect("claim D2 receipt");
let round2_deliveries = claimed_static_delegate_deliveries_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
)
.expect("read round2 claimed deliveries");
assert!(
ensure_static_delegate_user_input_wait_at(&root, &mut state, &round2_deliveries)
.expect("create round2 supervisor wait")
);
let round2_pending = read_game_creator_agent_runtime_pending_tool_action(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
)
.expect("read round2 pending action");
assert!(round2_pending.task.contains(&d2_delegation_id));
let round2_request =
match prepare_game_creator_agent_user_input_request_at(&root, &round2_pending)
.expect("prepare round2 clarification request")
{
AgentRuntimeUserInputRecovery::Waiting(request) => request,
other => panic!("unexpected round2 clarification recovery: {other:?}"),
};
let (_, round2_observation) = answer_game_creator_agent_user_input_request_for_pending_at(
&root,
&round2_pending,
&round2_request.request_id,
"clarification-chain-round2-response",
BTreeMap::from([("art_style".to_string(), "卡通".to_string())]),
)
.expect("answer round2 clarification request");
let round2_detail = serde_json::from_str::<Value>(
round2_observation
.detail
.as_deref()
.expect("round2 answer detail"),
)
.expect("parse round2 answer detail");
let round2_questions_sha = round2_detail["questionsSha256"]
.as_str()
.expect("round2 questions sha")
.to_string();
let round2_answers_sha = round2_detail["answersSha256"]
.as_str()
.expect("round2 answers sha")
.to_string();
fs::remove_file(&pending_path).expect("clear round2 answered pending before next planning");
// 澄清 continuation 自身的绑定校验(不含返工深度检查):应当通过——证明 D2 -> D3 的绑定
// 本身是合法的,问题出在另一道独立的深度门上。
let round2_continuation_input = serde_json::json!({
"continuationOfDelegationId": d2_delegation_id,
"questionsSha256": round2_questions_sha,
"answersSha256": round2_answers_sha,
});
let round2_identity = validate_static_delegate_clarification_continuation_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
&round2_continuation_input,
Some(&d2_delegation_id),
)
.expect("validate round2 continuation binding")
.expect("derived round2 continuation identity");
let d3_delegation_id = agent_runtime_delegation_id(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
target_agent_id,
&round2_identity,
);
// ---------- 第 3 轮尝试:对 D2 发起 continuation 以创建 D3repair_of = D2 ----------
// 关键钉子:直接调用 validate_static_delegate_repair_request_atagent.delegate 在真实路径
// 上无条件对所有携带 repairOfDelegationId 的请求都会跑这一步),用第 2 轮的参数验证返工
// 深度门是否放行。
let depth_gate_error = validate_static_delegate_repair_request_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
&d3_delegation_id,
target_agent_id,
&acceptance_criteria,
&expected_artifacts,
Some(&d2_delegation_id),
)
.expect_err("repair depth gate must reject the third round directly");
assert_eq!(
depth_gate_error, "静态委派返工深度最多为 1",
"实际返回错误:{depth_gate_error}"
);
// 再走一遍真实的 agent.delegate 全路径,确认生产代码路径上同一道门同样会拦下第 3 轮,
// 而不只是被孤立调用的校验函数拦下。
let round3_delegate_input = serde_json::json!({
"agentId": target_agent_id,
"task": "根据用户确认的卡通美术风格继续完成原方案",
"acceptanceCriteria": acceptance_criteria,
"expectedArtifacts": expected_artifacts,
"repairOfDelegationId": d2_delegation_id,
"runId": null,
"continuationOfDelegationId": d2_delegation_id,
"questionsSha256": round2_questions_sha,
"answersSha256": round2_answers_sha,
});
let round3_observation = observe_agent_runtime_agent_delegate(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
Some("clarification-chain-round3-continuation-action"),
&round3_delegate_input,
);
assert_eq!(
round3_observation.status, "failed",
"第 3 轮 continuation 在真实 agent.delegate 路径上必须被拒绝:{round3_observation:?}"
);
assert!(
round3_observation.summary.contains("深度最多为 1"),
"第 3 轮失败摘要应包含返工深度门的原文,实际为:{round3_observation:?}"
);
// D3 从未真正落盘:第 3 轮在整条链路上永远不可达(unreachable as a consequence
// 不是被某条独立规则在“第 3 轮”专门拦下)。
assert!(
read_static_delegate_delivery_at(&root, &d3_delegation_id)
.expect("read D3 lookup")
.is_none(),
"第 3 轮被拒绝后不应留下任何 D3 delivery 记录"
);
drop(target_lock);
fs::remove_dir_all(root).ok();
}