From d69668e078fd32678e1415705890624917bfd7e8 Mon Sep 17 00:00:00 2001 From: Linghong Date: Sun, 20 Sep 2026 17:25:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=85=E7=90=86=E6=8F=90=E7=A4=BA=E8=AF=8D?= =?UTF-8?q?=E8=84=86=E5=BC=B1=E6=B5=8B=E8=AF=95=E5=B9=B6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=88=86=E7=89=87=E5=A4=B1=E8=B4=A5=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除绑定提示词措辞的断言和测试专用的重复 Direct 回合编排 通过生产函数验证消息转发、文件登记和版本幂等行为 保留参数修复和上下文压缩测试并更新提示词来源校验 保留分片失败详情并区分选中用例数与失败数,补充脚本测试 同步测试边界和定向验证文档 --- .../scripts/run-rust-shell-test-shards.mjs | 36 +-- .../run-rust-shell-test-shards.test.mjs | 70 +++++ .../src/agent/codex_app_server/mod.rs | 15 +- .../src-tauri/src/agent/direct_runtime/mod.rs | 243 +++--------------- .../runtime_actions/provider_tool_plan.rs | 21 +- .../src-tauri/src/tests/provider.rs | 20 -- .../planning_strategy/tool_planning.rs | 18 +- .../shared-memory/development-workflow.md | 4 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 6 + 9 files changed, 169 insertions(+), 264 deletions(-) create mode 100644 apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.test.mjs diff --git a/apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs b/apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs index 9e0ae58bb..9a9a9bbd8 100644 --- a/apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs +++ b/apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs @@ -312,34 +312,15 @@ function runShard(executable, shardIndex, shardCount, shardTestNames) { }, ); - const failureLines = []; - let inFailureList = false; + // Rust 的首个 failures: 后有空行,不能按空行结束采集,否则会丢掉 panic 详情。 + // 只保存有界尾部;成功时不输出,失败时优先输出完整失败段。 + let stdoutTail = ''; let stderr = ''; - const consumeLine = (rawLine) => { - const line = rawLine.replace(/\r$/, ''); - if (line.includes('failures:')) { - inFailureList = true; - return; - } - if (inFailureList) { - if (line.trim().length === 0) { - inFailureList = false; - return; - } - failureLines.push(line.trim()); - } - }; child.stdout.setEncoding('utf8'); child.stderr.setEncoding('utf8'); - let stdoutBuffer = ''; child.stdout.on('data', (chunk) => { - stdoutBuffer += chunk; - const lines = stdoutBuffer.split('\n'); - stdoutBuffer = lines.pop() ?? ''; - for (const line of lines) { - consumeLine(line); - } + stdoutTail = (stdoutTail + chunk).slice(-64_000); }); child.stderr.on('data', (chunk) => { stderr += chunk; @@ -356,12 +337,17 @@ function runShard(executable, shardIndex, shardCount, shardTestNames) { }); }); child.on('close', (code) => { + const output = stdoutTail.replace(/\r\n/g, '\n'); + const failureStart = output.indexOf('failures:\n'); resolve({ label, ok: code === 0, durationMs: Date.now() - startedAt, testCount: shardTestNames.length, - failures: failureLines, + failures: output + .slice(failureStart < 0 ? 0 : failureStart) + .trim() + .split('\n'), stderr, }); }); @@ -438,7 +424,7 @@ async function main() { } failed = true; console.error( - `[rust-shards] ${result.label} FAILED: ${result.testCount} test(s) in ${formatDuration(result.durationMs)}`, + `[rust-shards] ${result.label} FAILED (selected ${result.testCount} test(s)) in ${formatDuration(result.durationMs)}`, ); for (const failure of result.failures) { console.error(`[rust-shards] ${failure}`); diff --git a/apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.test.mjs b/apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.test.mjs new file mode 100644 index 000000000..cf86a6a60 --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.test.mjs @@ -0,0 +1,70 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const runner = fileURLToPath( + new URL('./run-rust-shell-test-shards.mjs', import.meta.url), +); + +function runFixture(t, source) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agc-shard-output-')); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + fs.mkdirSync(path.join(root, 'src')); + fs.writeFileSync( + path.join(root, 'Cargo.toml'), + '[package]\nname = "shard-output-fixture"\nversion = "0.1.0"\nedition = "2021"\n', + ); + fs.writeFileSync(path.join(root, 'src/lib.rs'), source); + const result = spawnSync( + process.execPath, + [ + runner, + `--manifest=${path.join(root, 'Cargo.toml')}`, + '--target-kind=lib', + '--no-locked', + '--shards=1', + `--shard-tmp-root=${path.join(root, 'tmp')}`, + ], + { + encoding: 'utf8', + timeout: 60_000, + windowsHide: true, + env: { ...process.env, CARGO_TARGET_DIR: path.join(root, 'target') }, + }, + ); + assert.ifError(result.error); + return { status: result.status, output: result.stdout + result.stderr }; +} + +test('failed shard retains panic details and separates selected count from failures', (t) => { + const result = runFixture( + t, + ` +#[test] +fn passing_case() {} +#[test] +fn failing_case() { + assert_eq!(1, 2, "shard panic evidence"); +} +`, + ); + assert.equal(result.status, 1, result.output); + assert.match(result.output, /FAILED \(selected 2 test\(s\)\)/); + assert.match(result.output, /failing_case/); + assert.match(result.output, /panicked at src[\\/]lib\.rs:/); + assert.match(result.output, /shard panic evidence/); + assert.match(result.output, /left: 1/); + assert.match(result.output, /right: 2/); + assert.match(result.output, /1 passed; 1 failed/); +}); + +test('successful shard keeps its compact summary', (t) => { + const result = runFixture(t, '#[test]\nfn passing_case() {}\n'); + assert.equal(result.status, 0, result.output); + assert.match(result.output, /shard 1\/1 ok: 1 test\(s\)/); + assert.doesNotMatch(result.output, /test passing_case \.\.\. ok/); +}); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs index a45af869a..31e173735 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs @@ -5225,10 +5225,17 @@ mod tests { #[test] fn direct_codex_turn_keeps_system_prompt_out_of_user_input() { - let request = LlmRunRequest::single_turn("AGC 系统规则", "制作一个可玩的游戏"); - assert_eq!(direct_codex_base_instructions(&request), "AGC 系统规则"); - assert_eq!(direct_codex_user_prompt(&request), "制作一个可玩的游戏"); - assert!(!direct_codex_user_prompt(&request).contains("AGC 系统规则")); + let system = + crate::agent::direct_runtime::build_direct_codex_system_prompt_with_creation_type( + Path::new("."), + Some("art"), + ) + .expect("creation context"); + for prompt in ["你好", "今天多少号?", "画一个橙色陶罐角色"] { + let request = LlmRunRequest::single_turn(system.clone(), prompt); + assert_eq!(direct_codex_base_instructions(&request), system); + assert_eq!(direct_codex_user_prompt(&request), prompt); + } } #[tokio::test] diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs index 5051bd9c4..bcef0e4fa 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs @@ -5283,67 +5283,6 @@ mod direct_turn_stream_writer_tests { } } -/// Default product path: one user message becomes one turn on the same -/// project-bound Codex app-server thread. The client does not classify the -/// intent or perform hidden art, preview, repair, or another LLM workflow. If -/// Codex actually changes the canonical game files, the client performs only -/// deterministic resource/version projection so its own workspace reflects -/// the files now on disk. -async fn run_direct_game_creator_turn_with( - root: &Path, - prompt: &str, - run_turn: F, -) -> Result -where - F: FnOnce(String, String) -> Fut, - Fut: Future>, -{ - run_direct_game_creator_turn_with_creation_type(root, prompt, None, run_turn).await -} - -async fn run_direct_game_creator_turn_with_creation_type( - root: &Path, - prompt: &str, - creation_type: Option<&str>, - run_turn: F, -) -> Result -where - F: FnOnce(String, String) -> Fut, - Fut: Future>, -{ - emit_direct_game_creator_progress(root, "codex.turn", "陶泥儿正在处理这条消息"); - let previous_output_fingerprint = direct_codex_output_fingerprint(root); - let base_system_prompt = - build_direct_codex_system_prompt_with_creation_type(root, creation_type).map_err( - |error| DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error), - )?; - let engine_contract = direct_engine_three_dimensional_contract(root, prompt); - let system_prompt = match engine_contract.as_deref() { - Some(contract) => format!("{contract}\n{base_system_prompt}") - .chars() - .take(MAX_DIRECT_SYSTEM_PROMPT_CHARS) - .collect(), - None => base_system_prompt, - }; - let reply = run_turn(system_prompt, prompt.to_string()) - .await - .map_err(|error| { - DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error) - })?; - if direct_codex_output_fingerprint(root) != previous_output_fingerprint { - emit_direct_game_creator_progress( - root, - "project.sync", - "检测到游戏文件更新,正在同步客户端资源", - ); - sync_direct_codex_project_file_projection_at(root, Some(&previous_output_fingerprint)) - .map_err(|error| { - DirectCodexTurnFailure::new(DirectCodexFailureStage::VersionRegistration, error) - })?; - } - Ok(reply) -} - #[allow(dead_code)] async fn run_direct_game_creator_turn_with_private_editor_credentials( root: &Path, @@ -6442,159 +6381,27 @@ mod tests { .is_err()); } - #[tokio::test] - async fn direct_creation_type_keeps_the_original_user_prompt_unchanged() { - let root = tempfile::tempdir().expect("temp dir"); - init_local_game_project_at(root.path(), "direct-art-context", "素材上下文测试") - .expect("init project"); - - let reply = run_direct_game_creator_turn_with_creation_type( - root.path(), - "画一个橙色陶罐角色", - Some("art"), - |system, prompt| async move { - assert!(system.contains("art / 做素材")); - assert_eq!(prompt, "画一个橙色陶罐角色"); - assert!(!prompt.contains("初始意图")); - Ok("已理解素材需求。".to_string()) - }, - ) - .await - .expect("direct creation type turn"); - - assert_eq!(reply, "已理解素材需求。"); - } - - #[tokio::test] - async fn default_direct_turn_forwards_a_greeting_without_client_generation_workflow() { - let root = tempfile::tempdir().expect("temp dir"); - init_local_game_project_at(root.path(), "direct-chat-only", "纯对话测试") - .expect("init project"); - std::fs::write( - root.path().join("game/index.html"), - "before", - ) - .expect("index"); - std::fs::write(root.path().join("game/style.css"), "body { color: black; }") - .expect("style"); - std::fs::write(root.path().join("game/game.js"), "console.info('before');") - .expect("script"); - let manifest_before = - std::fs::read(root.path().join(".agent/manifest.json")).expect("manifest before"); - let index_before = - std::fs::read(root.path().join("game/index.html")).expect("index before"); - let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); - let captured = std::sync::Arc::clone(&seen); - - let reply = - run_direct_game_creator_turn_with(root.path(), "你好", move |system, prompt| { - let captured = std::sync::Arc::clone(&captured); - async move { - captured - .lock() - .expect("capture direct handoff") - .push((system, prompt)); - Ok("你好!我是陶泥儿。".to_string()) - } - }) - .await - .expect("greeting reply"); - - assert_eq!(reply, "你好!我是陶泥儿。"); - let calls = seen.lock().expect("read direct handoff"); - assert_eq!(calls.len(), 1); - assert_eq!(calls[0].1, "你好"); - assert!(calls[0].0.contains("普通对话")); - assert!(calls[0].0.contains("不触碰工作区")); - assert_eq!( - std::fs::read(root.path().join("game/index.html")).expect("index after"), - index_before - ); - assert_eq!( - std::fs::read(root.path().join(".agent/manifest.json")).expect("manifest after"), - manifest_before - ); - assert!( - !root - .path() - .join(".agent/runtime/direct-codex-browser-validation") - .exists(), - "ordinary chat must not start client browser validation" - ); - } - - #[tokio::test] - async fn default_direct_turn_forwards_a_date_question_without_client_art_or_version_side_effects( - ) { - let root = tempfile::tempdir().expect("temp dir"); - init_local_game_project_at(root.path(), "direct-date-only", "日期对话测试") - .expect("init project"); - let manifest_before = - std::fs::read(root.path().join(".agent/manifest.json")).expect("manifest before"); - let reply = run_direct_game_creator_turn_with( - root.path(), - "今天多少号?", - |system, prompt| async move { - assert!(system.contains("普通对话")); - assert!(system.contains("不触碰工作区")); - assert_eq!(prompt, "今天多少号?"); - Ok("今天是测试日期。".to_string()) - }, - ) - .await - .expect("date reply"); - - assert_eq!(reply, "今天是测试日期。"); - assert_eq!( - std::fs::read(root.path().join(".agent/manifest.json")).expect("manifest after"), - manifest_before - ); - assert!( - !root.path().join("assets/art-spec.png").exists() - && !root - .path() - .join("assets/direct-game-background.png") - .exists() - && !root.path().join("assets/art-spritesheet.png").exists(), - "ordinary chat must not prepare a TaoNier art package" - ); - } - - #[tokio::test] - async fn default_direct_turn_projects_changed_game_files_without_supervisor_or_art() { + #[test] + fn direct_file_projection_registers_changes_without_art_and_preserves_unchanged_versions() { let root = tempfile::tempdir().expect("temp dir"); init_local_game_project_at(root.path(), "direct-file-projection", "文件投影测试") .expect("init project"); - let write_root = root.path().to_path_buf(); + let before = direct_codex_output_fingerprint(root.path()); + let files = [ + ("game/index.html", ""), + ("game/style.css", "canvas { background: red; }"), + ("game/game.js", "requestAnimationFrame(() => {});"), + ]; + for (path, content) in files { + std::fs::write(root.path().join(path), content).expect("game file"); + } - let reply = run_direct_game_creator_turn_with( - root.path(), - "创建一个纯色方块游戏", - move |system, prompt| { - let write_root = write_root.clone(); - async move { - assert!(system.contains("唯一执行主体")); - assert_eq!(prompt, "创建一个纯色方块游戏"); - std::fs::write( - write_root.join("game/index.html"), - "", - ) - .expect("index"); - std::fs::write(write_root.join("game/style.css"), "canvas { background: red; }") - .expect("style"); - std::fs::write(write_root.join("game/game.js"), "requestAnimationFrame(() => {});") - .expect("script"); - Ok("已写入三个游戏文件。".to_string()) - } - }, - ) - .await - .expect("direct file projection"); + sync_direct_codex_project_file_projection_at(root.path(), Some(&before)) + .expect("project changed files"); - assert_eq!(reply, "已写入三个游戏文件。"); let manifest = read_manifest(&root.path().join(".agent/manifest.json")).expect("projected manifest"); - for (local_path, kind, media_type) in direct_codex_game_outputs(&root.path()) { + for (local_path, kind, media_type) in direct_codex_game_outputs(root.path()) { assert!(manifest.assets.iter().any(|asset| { asset.local_path == local_path && asset.kind == kind @@ -6611,6 +6418,28 @@ mod tests { .map(|task| task.status.clone()), Some(GameCreationAppTaskStatus::Completed) ); + let revision = read_game_creator_agent_runtime_project_revision(root.path()) + .expect("project revision"); + let unchanged = direct_codex_output_fingerprint(root.path()); + sync_direct_codex_project_file_projection_at(root.path(), Some(&unchanged)) + .expect("project unchanged files"); + let after = + read_manifest(&root.path().join(".agent/manifest.json")).expect("unchanged manifest"); + assert_eq!(after.assets, manifest.assets); + assert_eq!(after.versions, manifest.versions); + assert_eq!( + read_game_creator_agent_runtime_project_revision(root.path()) + .expect("unchanged revision") + .revision, + revision.revision + ); + for (path, content) in files { + assert_eq!( + std::fs::read_to_string(root.path().join(path)) + .expect("game file after projection"), + content + ); + } assert!(!root.path().join("assets/art-spec.png").exists()); assert!(!root.path().join(".agent/runtime/runtimes").exists()); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_tool_plan.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_tool_plan.rs index a5b9409d7..95da61ea8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_tool_plan.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_tool_plan.rs @@ -1338,16 +1338,25 @@ mod supervisor_collaboration_repair_tests { #[test] fn collaboration_repair_instruction_composes_the_generated_fragment() { + let protocol_error = "missing collaboration"; + let fragment = required_runtime_prompt_section( + RUNTIME_PROMPT_PROVIDER_AUTONOMOUS_INITIAL_COLLABORATION_REPAIR_SECTION, + ) + .trim(); let instruction = provider_collaboration_repair_instruction( - "missing collaboration", + protocol_error, RUNTIME_PROMPT_PROVIDER_AUTONOMOUS_INITIAL_COLLABORATION_REPAIR_SECTION, ); - assert!(instruction.starts_with( - "上一条输出不符合工具计划协议:missing collaboration\n本片段适用于 GUI / CLI" - )); - assert!(instruction.contains("本次修复原生工具目录")); - assert_eq!(instruction.matches("一次性建立完整首批合同").count(), 1); + assert_eq!( + instruction, + format!( + prompt_text!("recovery.collaboration_protocol"), + fragment, + protocol_error = protocol_error + ) + ); + assert_eq!(instruction.matches(fragment).count(), 1); assert!(instruction.contains("design-director")); assert!(instruction.contains("art-director")); assert!(instruction.contains("code-director")); diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs index ff102528d..15d0cdabc 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs @@ -798,25 +798,6 @@ fn provider_transient_retry_uses_configured_max_retries_for_every_run_profile() fs::remove_dir_all(root).ok(); } -#[test] -fn autonomous_game_build_tool_plan_guidance_bounds_each_source_payload() { - let guidance = AGENT_RUNTIME_AUTONOMOUS_TOOL_PLAN_PAYLOAD_GUIDANCE; - for expected in [ - "最多提交一个", - "不得超过 8000 字符", - "合计不得超过 10000 字符", - "可运行且保留扩展点的紧凑 scaffold", - "后续 planning 轮次", - "闭合的