From 7f129ddd6e6990fc586d267825a255ea65779bb0 Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 11 Aug 2026 11:14:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Native=20CI=20=E7=9A=84=20?= =?UTF-8?q?Agent=20=E6=A8=A1=E5=BC=8F=E6=B5=8B=E8=AF=95=E9=9A=94=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 Provider handoff 与 mock 请求测试显式固定 Provider 模式 同步 Goal steer 测试到条件中断语义 补充模拟 Provider 测试不得依赖本机 Codex 的排障约束 --- .../runtime_actions/response_stream_tests.rs | 61 ++++++++++--------- .../src-tauri/src/tests/goal.rs | 2 +- .../src-tauri/src/tests/mod.rs | 1 + .../src-tauri/src/tests/project.rs | 1 + .../src/tests/runtime_actions/policy.rs | 1 + docs/project-memory/shared-memory/pitfalls.md | 7 +++ 6 files changed, 42 insertions(+), 31 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/response_stream_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/response_stream_tests.rs index aecddbe12..b6953e4a3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/response_stream_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/response_stream_tests.rs @@ -1,5 +1,18 @@ use super::*; +fn provider_retry_identity_for_response_stream_test( + snapshot: &AgentRuntimeProviderRequestSnapshot, + llm: &GameCreatorLlmConfig, + request: &LlmRunRequest, +) -> Result { + game_creator_agent_runtime_provider_retry_identity_for_mode( + snapshot, + llm, + request, + GAME_CREATOR_AGENT_MODE_PROVIDER, + ) +} + fn response_stream_fixture( run_id: &str, ) -> ( @@ -75,10 +88,10 @@ fn persist_tool_plan_handoff_repair_chain( LlmMessage::user("修复格式"), ]); let base_identity = - game_creator_agent_runtime_provider_retry_identity(&base_snapshot, llm, &base_request) + provider_retry_identity_for_response_stream_test(&base_snapshot, llm, &base_request) .expect("build cleanup base identity"); let repair_identity = - game_creator_agent_runtime_provider_retry_identity(&repair_snapshot, llm, &repair_request) + provider_retry_identity_for_response_stream_test(&repair_snapshot, llm, &repair_request) .expect("build cleanup repair identity"); let base_request_id = game_creator_agent_runtime_provider_request_id(&base_snapshot); let repair_request_id = game_creator_agent_runtime_provider_request_id(&repair_snapshot); @@ -972,7 +985,7 @@ async fn provider_handoff_identity_drift_closes_lifecycle_without_leaking_respon let mut new_llm = old_llm.clone(); new_llm.model = "new-provider-model".to_string(); let old_identity = - game_creator_agent_runtime_provider_retry_identity(&snapshot, &old_llm, &request) + provider_retry_identity_for_response_stream_test(&snapshot, &old_llm, &request) .expect("build old handoff identity"); let provider_request_id = game_creator_agent_runtime_provider_request_id(&snapshot); append_game_creator_agent_runtime_provider_request_lifecycle( @@ -1008,9 +1021,7 @@ async fn provider_handoff_identity_drift_closes_lifecycle_without_leaking_respon "agentLlm.design-director", "测试 Provider handoff 身份漂移", &request, - load_game_creator_app_config() - .expect("load test agent mode") - .agent_mode, + GAME_CREATOR_AGENT_MODE_PROVIDER.to_string(), |_request| async { Err(platform_llm::LlmError::Transport( "identity drift must not call Provider".to_string(), @@ -1077,9 +1088,9 @@ async fn tool_plan_handoff_identity_drift_closes_entire_repair_chain_before_remo let mut new_llm = old_llm.clone(); new_llm.model = "new-tool-plan-model".to_string(); let base_identity = - game_creator_agent_runtime_provider_retry_identity(&base_snapshot, &old_llm, &base_request) + provider_retry_identity_for_response_stream_test(&base_snapshot, &old_llm, &base_request) .expect("build old base identity"); - let repair_identity = game_creator_agent_runtime_provider_retry_identity( + let repair_identity = provider_retry_identity_for_response_stream_test( &repair_snapshot, &old_llm, &repair_request, @@ -1138,9 +1149,7 @@ async fn tool_plan_handoff_identity_drift_closes_entire_repair_chain_before_remo "agentLlm.project-supervisor", "测试 tool-plan handoff 身份漂移", &base_request, - load_game_creator_app_config() - .expect("load test agent mode") - .agent_mode, + GAME_CREATOR_AGENT_MODE_PROVIDER.to_string(), |_request| async { Err(platform_llm::LlmError::Transport( "identity drift must not call Provider".to_string(), @@ -1197,7 +1206,7 @@ async fn generic_retry_identity_drift_closes_tool_plan_repair_chain_before_remov persist_tool_plan_handoff_repair_chain(root, &snapshot, &old_llm); let request = LlmRunRequest::new(vec![LlmMessage::user("验证通用 retry 身份漂移")]); let old_identity = - game_creator_agent_runtime_provider_retry_identity(&snapshot, &old_llm, &request) + provider_retry_identity_for_response_stream_test(&snapshot, &old_llm, &request) .expect("build old generic retry identity"); provider_retry::write_next_at( root, @@ -1220,9 +1229,7 @@ async fn generic_retry_identity_drift_closes_tool_plan_repair_chain_before_remov "agentLlm.project-supervisor", "测试通用 retry 身份漂移", &request, - load_game_creator_app_config() - .expect("load test agent mode") - .agent_mode, + GAME_CREATOR_AGENT_MODE_PROVIDER.to_string(), |_request| async { Err(platform_llm::LlmError::Transport( "generic retry drift must not call Provider".to_string(), @@ -1289,7 +1296,7 @@ async fn tool_plan_capacity_gate_runs_before_provider_lifecycle_and_network() { "capacity request {loop_iteration}" ))]); let identity = - game_creator_agent_runtime_provider_retry_identity(&entry_snapshot, &llm, &request) + provider_retry_identity_for_response_stream_test(&entry_snapshot, &llm, &request) .expect("build capacity entry identity"); let request_id = game_creator_agent_runtime_provider_request_id(&entry_snapshot); let response = platform_llm::LlmRunResponse { @@ -1335,9 +1342,7 @@ async fn tool_plan_capacity_gate_runs_before_provider_lifecycle_and_network() { "agentLlm.project-supervisor", "测试 tool-plan 请求前容量门禁", &request, - load_game_creator_app_config() - .expect("load test agent mode") - .agent_mode, + GAME_CREATOR_AGENT_MODE_PROVIDER.to_string(), move |_request| async move { provider_called_for_request.store(true, Ordering::Release); Err(platform_llm::LlmError::Transport( @@ -1404,10 +1409,10 @@ async fn tool_plan_handoff_durable_control_closes_entire_repair_chain_before_rem retry_backoff_ms: 1, }; let base_identity = - game_creator_agent_runtime_provider_retry_identity(&base_snapshot, &llm, &base_request) + provider_retry_identity_for_response_stream_test(&base_snapshot, &llm, &base_request) .expect("build durable control base identity"); let repair_identity = - game_creator_agent_runtime_provider_retry_identity(&repair_snapshot, &llm, &repair_request) + provider_retry_identity_for_response_stream_test(&repair_snapshot, &llm, &repair_request) .expect("build durable control repair identity"); let base_request_id = game_creator_agent_runtime_provider_request_id(&base_snapshot); let repair_request_id = game_creator_agent_runtime_provider_request_id(&repair_snapshot); @@ -1470,9 +1475,7 @@ async fn tool_plan_handoff_durable_control_closes_entire_repair_chain_before_rem "agentLlm.project-supervisor", "测试 durable control 清理 tool-plan handoff", &base_request, - load_game_creator_app_config() - .expect("load test agent mode") - .agent_mode, + GAME_CREATOR_AGENT_MODE_PROVIDER.to_string(), |_request| async { Err(platform_llm::LlmError::Transport( "durable control must not call Provider".to_string(), @@ -1528,7 +1531,7 @@ fn provider_recovery_cleanup_closes_tool_plan_lifecycle_before_removing_handoff( max_retries: 0, retry_backoff_ms: 1, }; - let identity = game_creator_agent_runtime_provider_retry_identity(&snapshot, &llm, &request) + let identity = provider_retry_identity_for_response_stream_test(&snapshot, &llm, &request) .expect("build cleanup handoff identity"); let request_id = game_creator_agent_runtime_provider_request_id(&snapshot); append_game_creator_agent_runtime_provider_request_lifecycle( @@ -1598,7 +1601,7 @@ fn runtime_resume_scans_and_cleans_terminal_tool_plan_handoff() { max_retries: 0, retry_backoff_ms: 1, }; - let identity = game_creator_agent_runtime_provider_retry_identity(&snapshot, &llm, &request) + let identity = provider_retry_identity_for_response_stream_test(&snapshot, &llm, &request) .expect("build terminal handoff identity"); let request_id = game_creator_agent_runtime_provider_request_id(&snapshot); append_game_creator_agent_runtime_provider_request_lifecycle( @@ -1692,7 +1695,7 @@ async fn provider_handoff_retry_conflict_preserves_both_sidecars_for_reconciliat max_retries: 1, retry_backoff_ms: 1, }; - let identity = game_creator_agent_runtime_provider_retry_identity(&snapshot, &llm, &request) + let identity = provider_retry_identity_for_response_stream_test(&snapshot, &llm, &request) .expect("build Provider recovery identity"); let provider_request_id = game_creator_agent_runtime_provider_request_id(&snapshot); append_game_creator_agent_runtime_provider_request_lifecycle( @@ -1738,9 +1741,7 @@ async fn provider_handoff_retry_conflict_preserves_both_sidecars_for_reconciliat "agentLlm.design-director", "测试 Provider recovery 冲突", &request, - load_game_creator_app_config() - .expect("load test agent mode") - .agent_mode, + GAME_CREATOR_AGENT_MODE_PROVIDER.to_string(), |_request| async { Err(platform_llm::LlmError::Transport( "reconciliation must not call Provider".to_string(), diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/goal.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/goal.rs index af67174da..da15d08a6 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/goal.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/goal.rs @@ -80,7 +80,7 @@ async fn agent_goal_edit_pause_resume_keeps_one_session_and_run_until_completion .expect("edit active Goal"); assert_eq!(edited.goal.revision, 2); assert_eq!(edited.goal.run_id, run_id); - assert!(edited.provider_interrupted); + assert!(!edited.provider_interrupted); let stale_edit = edit_game_creator_agent_goal_at( &root, "code-prototype", diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs index 5e2dad361..5cbcb764e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs @@ -1127,6 +1127,7 @@ fn write_mcp_runtime_test_config(config_dir: &Path, llm_base_url: &str, marker_p fs::create_dir_all(config_dir).expect("create MCP runtime test config dir"); let marker_arg = format!("--marker={}", marker_path.display()); let config = serde_json::json!({ + "agentMode": "provider", "agentLlm": { "code-prototype": { "apiKey": "mcp-runtime-test-key", diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs index 71e092fb7..b3413364a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs @@ -677,6 +677,7 @@ async fn background_agent_runtime_can_generate_platform_art_asset() { fs::write( config_dir.join(GAME_CREATOR_CONFIG_FILE_NAME), serde_json::json!({ + "agentMode": "provider", "editorApi": { "baseUrl": canvas_base_url, "apiKey": "editor-runtime-key" diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/policy.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/policy.rs index f852192e0..f65168688 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/policy.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/policy.rs @@ -243,6 +243,7 @@ async fn background_agent_runtime_asset_generation_respects_project_policy() { fs::write( config_dir.join(GAME_CREATOR_CONFIG_FILE_NAME), serde_json::json!({ + "agentMode": "provider", "agentLlm": { "art-asset-plan": { "apiKey": "art-key", diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index ff77dc6fc..fb90ebd79 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -4499,6 +4499,13 @@ - 处理:Codex CLI 必须只作为新的节点推理适配器,继续经过原 Runtime 的 lifecycle、retry、handoff、receipt、revision、verification 和 reconciliation。只接受完整 JSONL `turn.completed` 与可验证的最终消息;`started` 无 `completed`、超时、异常退出或输出破损都继续走原失败/核对边界。`agentMode`、CLI 版本和影响输出的固定参数必须纳入配置指纹;CLI 不可用时显式报错,不得静默切回 Provider。 - 验证:确定性回归覆盖 stdin prompt、空临时 cwd、read-only/no-shell/ephemeral 参数、structured tool call 转换、缺失终态、超时进程组回收和 stderr 不泄漏;真实 smoke 必须显式 opt-in,并分开报告 CLI 协议成功与本机认证/网络结果。 +## 模拟 Provider 的测试不能继承生产默认 Agent 模式(2026-08-11) + +- 现象:生产默认切到 `codex_app_server` 后,HTTP mock Provider 测试不再收到请求并超时;只验证 retry/handoff identity 的纯单测还会在未安装 Codex CLI 的 CI Runner 上直接失败。本机安装了 Codex 时,相关遗漏可能被掩盖。 +- 原因:测试只构造 `agentLlm` 或直接调用读取全局配置的 identity helper,没有显式固定 `agentMode`;缺省配置按正式产品合同选择 `codex_app_server`,测试因此意外依赖本机 CLI 和认证环境。 +- 处理:测试若验证 HTTP Provider 协议或 mock 请求,必须在测试配置中显式写入 `agentMode: provider`;只验证 Provider retry/handoff 数据结构的纯单测应调用显式接收模式的 identity helper。不得把生产默认模式改回 Provider,也不得仅为单测向通用 CI 镜像安装 Codex CLI。 +- 验证:在 PATH 不含 Codex CLI 的环境运行 response-stream identity、MCP Runtime 和平台素材 mock 回归;同时保留独立的 Codex CLI/app-server 可用性与协议测试,防止 Provider 测试替代正式模式覆盖。 + ## 空 MCP 覆盖不会清除 Codex 用户配置中的 MCP(2026-08-10) - 现象:以 `codex app-server -c 'mcp_servers={}'` 启动后,`thread/start` 仍发出用户配置中各 MCP server 的 startup 事件;若直接把这种进程当 AGC 节点 Agent,会出现 Codex 与 AGC 两套 ToolHost、副作用和审批边界。