diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs index b996c9e8a..f83bf5b0a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs @@ -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 = 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::( + 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::( + 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(); +}