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/design-agent/phase-context/consultant-tail.md b/apps/ai-game-creator-shell/src-tauri/design-agent/phase-context/consultant-tail.md index a898efdd7..27badf79c 100644 --- a/apps/ai-game-creator-shell/src-tauri/design-agent/phase-context/consultant-tail.md +++ b/apps/ai-game-creator-shell/src-tauri/design-agent/phase-context/consultant-tail.md @@ -1,4 +1,3 @@ -顾问阶段遵照用户的具体指示行动。 +顾问阶段遵照用户的具体指示行动,不自主推进项目或主动安排下一步,不提交阶段审批。 根据用户指示回答问题、读取相关文档、修改工作区文件,并说明改动可能影响的已有产物。 涉及方向性变化或多个可行方案时,先向用户说明影响并等待用户决定。 -顾问阶段以完成用户当前请求并汇报结果为结束点。 diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/decision-log.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/decision-log.md index 081e5680c..956502bb4 100644 --- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/decision-log.md +++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/decision-log.md @@ -3,7 +3,7 @@ 版本:v3 | 规则:台账放活队列——design 只放结论、分析只放论证、决定与开放问题住这里。编号连续不复用;被推翻的行标 overturned 挂新行,不删行。 状态六态:`confirmed`(用户亲口/亲选)/ `auto_decided`(技术类代决,必带理由+推翻条件,用户一键可翻)/ `default_pending`(默认建议兜底,用户未点头)/ `prototype_pending`(待原型验证)/ `pending_user`(等用户拍板)/ `overturned`(被推翻,挂旧行编号)。 -> 编号口径:D-01~D-13 与 exemplars/stardew-analysis.md 台账节选一致(D-04~D-06、D-08~D-10、D-12 原为"就地小权衡,直接登记未开条目",此处按登记口径展开);D-14 起为技术文档期新增,与 stardew-tdd-tech.md 开放问题回执互引。 +> 编号口径:D-01~D-13 与 templates/stardew-analysis.md 台账节选一致(D-04~D-06、D-08~D-10、D-12 原为"就地小权衡,直接登记未开条目",此处按登记口径展开);D-14 起为技术文档期新增,与 stardew-tdd-tech.md 开放问题回执互引。 ## 当前待办(活队列) diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/stardew-top-design.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/stardew-top-design.md index 6cf31bf21..83549c0d6 100644 --- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/stardew-top-design.md +++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/exemplars/stardew-top-design.md @@ -1,7 +1,7 @@ # 顶层设计:《星露谷物语》 ## 顶层定位与规模锚点 -顶层设计让玩家每天都在想: +顶层不是做长线农场生产线,也不是做以探索战斗为主的活动清单,而是让玩家每天都在想: > "今天做什么?——下雨天不用浇水,正好下矿井;回来的路上把罗宾的生日礼物送了。" | 项 | 定义 | diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/architecture.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/architecture.md index 7b892a27c..6ce3933c9 100644 --- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/architecture.md +++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/architecture.md @@ -5,7 +5,7 @@ name: game-gdd-architecture description: 写游戏策划案(GDD)系统架构时使用。在顶层设计定稿之后, 把顶层的系统范围表正式切成 Sxx 系统:编号、职责、依赖、数据流、优先级, 并向系统文档站交付目录映射与 MVP 闭环。配套:templates/architecture.md、 - templates/analysis.md(全局一份)、exemplars/stardew-architecture.md、exemplars/stardew-analysis.md(全局一份)。 + templates/analysis.md(全局一份)、exemplars/stardew-architecture.md、templates/stardew-analysis.md(全局一份)。 --- # 系统架构写法(策划 agent · 系统架构分册) diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/concept.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/concept.md index 98b84c3c3..c9c1eaf77 100644 --- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/concept.md +++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/concept.md @@ -5,7 +5,7 @@ name: game-gdd-concept description: 写游戏策划案(GDD)概念层时使用。把一句话游戏想法写成一份 "一次写对、之后不动"的立项概念文档——它是后续所有设计争议的仲裁依据。 任何游戏类型通用。配套:templates/concept-design.md、templates/analysis.md(全局一份)、 - exemplars/stardew-concept.md、exemplars/stardew-analysis.md(全局一份)。 + exemplars/stardew-concept.md、templates/stardew-analysis.md(全局一份)。 --- # 概念层写法(策划 agent · 概念层分册) diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/top_design.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/top_design.md index 7bf0e02c0..1105370bd 100644 --- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/top_design.md +++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/skills/top_design.md @@ -5,7 +5,7 @@ name: game-gdd-top-design description: 写游戏策划案(GDD)顶层设计时使用。在概念层定稿之后, 回答"玩家为什么一直玩"——把概念变成可玩的时间结构(循环/资源/取舍/节奏), 并向架构层交付系统范围。配套:templates/top-design.md、templates/analysis.md(全局一份)、 - exemplars/stardew-top-design.md、exemplars/stardew-analysis.md(全局一份)。 + exemplars/stardew-top-design.md、templates/stardew-analysis.md(全局一份)。 --- # 顶层设计写法(策划 agent · 顶层设计分册) @@ -45,7 +45,7 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定 | # | 节 | 是什么 | 为什么写 | 和谁咬合 | |---|---|---|---|---| -| 1 | 顶层定位与规模锚点 | 承概念定稿 + "让玩家每天都在想"念头句 + 规模参数表(循环单位/段落/复杂度/长期主轴) | 循环单位定错全盘错;定位句防止顶层漂离概念 | 承概念层"概念定稿";念头句是概念层玩家念头的时间维度版 | +| 1 | 顶层定位与规模锚点 | 承概念定稿 + 按需说明易混淆方向及排除理由 + "让玩家每天都在想"念头句 + 规模参数表(循环单位/段落/复杂度/长期主轴) | 循环单位定错全盘错;定位句防止顶层漂离概念 | 承概念层"概念定稿";念头句是概念层玩家念头的时间维度版 | | 2 | 设计目标 | 几种回报、如何互相供给 | 回报并列=小游戏拼盘;互相供给才是循环 | 供给关系落到 4~5 的循环里 | | 3 | 核心推动力 | 按项目实际存在的即时、阶段或长期推动力组织 | 玩家"什么时候被什么推着走"的推动结构 | 与实际节奏结构对应 | | 4 | 大循环 | 跨较长时间的循环:文字箭头 + 核心循环图 | 长期留存的结构骨架 | 与 5、7 三层互检:大循环的每环应有小循环供血 | @@ -88,6 +88,7 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定 (本节是带写法要领的教学版;实际填写的纯净模板在 templates/top-design.md) ### 1. 顶层定位与规模锚点 +承接概念定稿说明核心定位;存在容易混淆的方向时,说明排除方向及理由,表述按项目需要组织。 顶层设计让玩家每天都在想: > "__(玩家每天惦记的那件事)" 规模锚点表:循环单位 / 段落构成 / 操作复杂度 / 经营复杂度 / 长期主轴排序。 diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/stardew-analysis.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/stardew-analysis.md index 1cea05649..a08d153c6 100644 --- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/stardew-analysis.md +++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/stardew-analysis.md @@ -1,4 +1,4 @@ -### C1 例子_星露谷_分析.md(分析金样;→ exemplars/stardew-analysis.md) +### C1 例子_星露谷_分析.md(分析金样;→ templates/stardew-analysis.md) # 分析:《星露谷物语》 diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/top-design.md b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/top-design.md index 8478a85fd..437fcc545 100644 --- a/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/top-design.md +++ b/apps/ai-game-creator-shell/src-tauri/design-agent/resources/templates/top-design.md @@ -5,6 +5,9 @@ # 顶层设计:《游戏名》 ## 顶层定位与规模锚点 +核心定位:__。 +容易混淆的方向及排除理由(按需):__。 + 顶层设计让玩家每天都在想: > "__" diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/system-prompt.md b/apps/ai-game-creator-shell/src-tauri/design-agent/system-prompt.md index 17abb4df0..a85f940bf 100644 --- a/apps/ai-game-creator-shell/src-tauri/design-agent/system-prompt.md +++ b/apps/ai-game-creator-shell/src-tauri/design-agent/system-prompt.md @@ -7,12 +7,12 @@ 正式策划文档在文档头部写明版本标记,例如“版本:v1”。由你自行维护版本号:只有整体修订、阶段性定稿或用户意见造成实质内容变化时才递增;错别字、措辞润色、单个局部修改和小范围补充不单独递增。 -阶段审批是每个阶段的最终检查,将已完成的本阶段产物交给用户检阅。提交前,解决所有影响本阶段完成的关键问题,或明确说明它们不阻塞本阶段交付,并更新相关产物。可以保留不阻塞当前阶段的后续事项和待原型验证项。 +阶段审批是五个策划阶段各自的最终检查,将已完成的本阶段产物交给用户检阅。需要用户选择的关键问题先通过问询解决,阶段审批不承担问询功能。提交前,解决所有影响本阶段完成的关键问题,或明确说明它们不阻塞本阶段交付,并更新相关产物。可以保留不阻塞当前阶段的后续事项和待原型验证项。 -过程文档用于记录关键依据、决定和待办。阶段内优先完成主要设计内容;只有稳定且影响后续工作的决定才需要同步到多个过程文档。阶段提交前,补齐影响验收的关键记录。 +过程文档用于记录关键依据、决定和待办,不要求实时完整,也不应重复正式设计文档。阶段内优先完成主要设计内容;只有稳定且影响后续工作的决定才需要同步到多个过程文档。阶段提交前,补齐影响验收的关键记录。 阶段获批后,产物中已经采用的方案作为后续工作的依据,并保留原有决策来源。用户主动质疑或出现新的约束冲突时,再重新讨论相关决定。 -用户说“继续”时,继续推进当前阶段最有价值的工作。判断本阶段已完成并准备交用户检阅时,应调用 `submit_phase_for_approval`;只有该工具调用成功,才算正式提交审批。 +用户说“继续”时,继续推进当前阶段最有价值的工作。在五个策划阶段中,判断本阶段已完成并准备交用户检阅时,应调用 `submit_phase_for_approval`;只有该工具调用成功,才算正式提交审批。 用户口头表示已经批准或要求进入下一阶段时,先调用 `get_workflow_status` 确认 Runtime 当前阶段。只有用户批准正式审批请求后,Runtime 才会推进阶段;审批工具是推进阶段的唯一方式。 diff --git a/apps/ai-game-creator-shell/src-tauri/design-agent/tools.json b/apps/ai-game-creator-shell/src-tauri/design-agent/tools.json index ef7b62ea9..ff06e6f44 100644 --- a/apps/ai-game-creator-shell/src-tauri/design-agent/tools.json +++ b/apps/ai-game-creator-shell/src-tauri/design-agent/tools.json @@ -9,5 +9,5 @@ {"type":"function","function":{"name":"write_file","description":"创建或覆盖工作目录内的 UTF-8 文本文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}},"required":["path","content"],"additionalProperties":false}}}, {"type":"function","function":{"name":"search_text","description":"在工作目录内搜索文本。","parameters":{"type":"object","properties":{"query":{"type":"string"},"path":{"type":"string"}},"required":["query"],"additionalProperties":false}}}, {"type":"function","function":{"name":"ask_clarification","description":"向用户展示多选项问询澄清卡片,选项数2-4。多选一场景时优先使用本工具,其他场景可以纯文本进行问询。每轮最多调用一次。","parameters":{"type":"object","properties":{"question":{"type":"string"},"options":{"type":"array","items":{"type":"string"}}},"required":["question"],"additionalProperties":false}}}, - {"type":"function","function":{"name":"submit_phase_for_approval","description":"提交当前策划阶段供用户审批。当你判断当前阶段已经完成并准备交用户检阅时必须调用。用户批准后 Runtime 自动进入下一阶段。","parameters":{"type":"object","properties":{},"additionalProperties":false}}} + {"type":"function","function":{"name":"submit_phase_for_approval","description":"提交五个策划阶段中的当前阶段供用户审批。当你判断当前阶段已经完成并准备交用户检阅时必须调用。用户批准后 Runtime 自动进入下一阶段。","parameters":{"type":"object","properties":{},"additionalProperties":false}}} ] 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 4af83acb7..499ae87af 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 @@ -5248,10 +5248,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 34dd42ca3..281fdd710 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 @@ -5287,67 +5287,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, @@ -6482,159 +6421,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 @@ -6651,6 +6458,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 6fa6ea57f..aca51816b 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 轮次", - "闭合的