测试:实证静态委派澄清 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:
@@ -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 轮:D1(repair_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 轮:D2(repair_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 以创建 D3,repair_of = D2 ----------
|
||||
// 关键钉子:直接调用 validate_static_delegate_repair_request_at(agent.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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user