From 7e4b404d5f57a51e3bb6e7bb20fe1bce2df21a99 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Wed, 9 Sep 2026 10:59:33 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20AGC=20=E5=8E=9F=E7=94=9F?= =?UTF-8?q?=E5=A3=B3=20CI=20=E6=B5=8B=E8=AF=95=E4=B8=8E=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E5=90=8E=E9=97=A8=E7=A6=81=E4=B8=8D=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 init_legacy_single_html_project_at 夹具,先写 game/index.html 再初始化,避免播种 npm + Vite 脚手架 - 把 JSON Generator、game.static_smoke、预览与导出相关用例改回旧版单文件 HTML 布局 - 按 #274 退役固定视觉资产门禁后的实际行为更新 prompt、delegation、runtime_state、response_stream 等用例 - 按 npm / Phaser 4 需求驱动合同更新图集与画布生成用例,accepted 生成不匹配探测改为接受一次 GET - 对齐恢复扫描用例的生成选项,使持久化请求快照与待执行动作一致 - 测试临时目录改用 std::env::temp_dir(),Windows 缺少 HOME 时不再失败 - 同步 project-memory 排障记录 --- .../src-tauri/src/agent/direct_runtime.rs | 6 +- .../src/agent/generation/canvas_generation.rs | 112 +++++++-- .../src-tauri/src/agent/prompt.rs | 98 ++++---- .../runtime_actions/autonomous_policy.rs | 17 +- .../src/agent/runtime_driver/task_start.rs | 27 +-- .../src-tauri/src/preview.rs | 2 +- .../src-tauri/src/project/export.rs | 2 +- .../src-tauri/src/project/verification.rs | 2 +- .../src/tests/collaboration/delegation.rs | 212 ++---------------- .../tests/collaboration/parallel_actions.rs | 2 +- .../tests/collaboration/static_deliveries.rs | 73 ++---- .../src-tauri/src/tests/command_runtime.rs | 19 +- .../src-tauri/src/tests/goal.rs | 2 +- .../src-tauri/src/tests/mod.rs | 31 +++ .../src-tauri/src/tests/project.rs | 27 ++- .../src-tauri/src/tests/provider.rs | 7 +- .../src-tauri/src/tests/response_stream.rs | 146 +----------- .../tests/runtime_actions/action_execution.rs | 21 +- .../src/tests/runtime_actions/support.rs | 4 +- .../src-tauri/src/tests/runtime_state.rs | 84 +------ .../src-tauri/src/tests/sessions.rs | 3 + docs/project-memory/shared-memory/pitfalls.md | 7 + 22 files changed, 303 insertions(+), 601 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs index e7974ba25..d8b3ce306 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs @@ -4710,10 +4710,12 @@ mod tests { assert!(!prompt.contains("你是 Codex")); assert!(prompt.contains("不要等待 Supervisor")); assert!(prompt.contains("提示词与技能")); - assert!(prompt.contains("AGC 工程合同(仅说明项目边界,不是流程门槛)")); + assert!(prompt.contains("AGC 工程合同:当前 cwd 是用户选择的项目目录")); + assert!(prompt.contains("Phaser 固定为 4.2.1")); + assert!(prompt.contains("完成后必须从 `dist/index.html` 试玩")); assert!(prompt.contains("客户端扩展列表中用户已启用的第三方 MCP")); assert!(prompt.contains("用户明确指定第三方 MCP Server 或工具时")); - assert!(prompt.contains("先按需读取当前 cwd 下适用的 `AGENTS.md`")); + assert!(prompt.contains("先读取当前 cwd 下适用的 AGENTS.md、README 或项目说明")); assert!(prompt.contains("agc_write_file")); assert!(prompt.contains("content 必须是目标文件的完整原始 UTF-8 正文")); assert!(prompt.contains("不得把 command.exec 的 Exit code、Wall time、Output 包装")); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs index 0e7a97084..baa5bf02b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs @@ -8895,7 +8895,7 @@ mod canvas_generation_tests { } #[test] - fn strict_spritesheet_contract_requires_the_grid_2x2_provider_receipt() { + fn strict_spritesheet_contract_accepts_need_based_slice_layout() { let canvas_context = ExternalCanvasGenerationContext { project_id: "canvas-project".to_string(), asset_folder_id: "asset-folder".to_string(), @@ -8927,7 +8927,7 @@ mod canvas_generation_tests { } }) .collect::>(); - let error = validate_strict_platform_art_spritesheet_contract( + validate_strict_platform_art_spritesheet_contract( &slices, &canvas_context, Some("canvas-project"), @@ -8941,9 +8941,24 @@ mod canvas_generation_tests { true, true, ) - .expect_err("missing fixed-layout proof must not be accepted"); + .expect("slice layout is optional under the need-based spritesheet contract"); - assert!(error.contains("grid-2x2")); + let error = validate_strict_platform_art_spritesheet_contract( + &slices, + &canvas_context, + Some("canvas-project"), + Some("spritesheet-resource"), + Some("spritesheet-object"), + Some("spritesheet-task"), + "/api/external/v1/editor/icon-spritesheets/generations", + "icon-spritesheet", + None, + &[], + true, + true, + ) + .expect_err("missing art-spec reference must still fail closed"); + assert!(error.contains("art-spec"), "{error}"); } #[test] @@ -10253,6 +10268,40 @@ mod canvas_generation_tests { error.contains("当前生成意图与已持久化请求快照不一致"), "{error}" ); + let (mut probe, _) = listener + .accept() + .expect("changed intent must probe the old operation once"); + probe + .set_read_timeout(Some(Duration::from_secs(2))) + .expect("set probe read timeout"); + use std::io::Read as _; + let mut probe_request = Vec::new(); + let mut chunk = [0_u8; 512]; + loop { + match probe.read(&mut chunk) { + Ok(0) => break, + Ok(read) => { + probe_request.extend_from_slice(&chunk[..read]); + if probe_request.windows(4).any(|window| window == b"\r\n\r\n") { + break; + } + } + Err(error) + if matches!( + error.kind(), + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut + ) => + { + break; + } + Err(error) => panic!("read changed-intent probe request: {error}"), + } + } + let probe_request = String::from_utf8_lossy(&probe_request); + assert!( + probe_request.starts_with("GET /api/external/v1/generations/old-operation "), + "{probe_request}" + ); assert!(matches!( listener.accept(), Err(error) if error.kind() == std::io::ErrorKind::WouldBlock @@ -10412,6 +10461,40 @@ mod canvas_generation_tests { "{error}" ); assert!(error.contains("当前规范图身份"), "{error}"); + let (mut probe, _) = listener + .accept() + .expect("changed art-spec identity must probe the old operation once"); + probe + .set_read_timeout(Some(Duration::from_secs(2))) + .expect("set probe read timeout"); + use std::io::Read as _; + let mut probe_request = Vec::new(); + let mut chunk = [0_u8; 512]; + loop { + match probe.read(&mut chunk) { + Ok(0) => break, + Ok(read) => { + probe_request.extend_from_slice(&chunk[..read]); + if probe_request.windows(4).any(|window| window == b"\r\n\r\n") { + break; + } + } + Err(error) + if matches!( + error.kind(), + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut + ) => + { + break; + } + Err(error) => panic!("read changed-art-spec probe request: {error}"), + } + } + let probe_request = String::from_utf8_lossy(&probe_request); + assert!( + probe_request.starts_with("GET /api/external/v1/generations/old-background-operation "), + "{probe_request}" + ); assert!(matches!( listener.accept(), Err(error) if error.kind() == std::io::ErrorKind::WouldBlock @@ -10775,8 +10858,8 @@ mod canvas_generation_tests { output_path: Some(AGENT_RUNTIME_ART_SPEC_PATH.to_string()), aspect_ratio: "1:1".to_string(), image_size: "1K".to_string(), - asset_kind: "icon-spec".to_string(), - asset_label: "游戏统一视觉规范图".to_string(), + asset_kind: "game-art".to_string(), + asset_label: "AI 游戏首版美术素材".to_string(), replace_existing: false, slice_count: None, }; @@ -11063,19 +11146,18 @@ mod canvas_generation_tests { } #[test] - fn canonical_art_spritesheet_request_has_exactly_four_ordered_categories() { + fn canonical_art_spritesheet_request_uses_one_need_based_description() { let descriptions = canonical_art_spritesheet_icon_descriptions("原创收集玩法"); - assert_eq!(descriptions.len(), 4); - for (index, description) in descriptions.iter().enumerate() { - assert!(description.starts_with(&format!("第 {} 类", index + 1))); - } + assert_eq!(descriptions.len(), 1); + assert!(descriptions[0].contains("数量、类别、排列和切片方式由本次需求决定")); + assert!(descriptions[0].contains("原创收集玩法")); } #[test] fn canonical_art_spritesheet_descriptions_obey_external_editor_item_limit() { let descriptions = canonical_art_spritesheet_icon_descriptions(&"原创玩法需求".repeat(128)); - assert_eq!(descriptions.len(), 4); + assert_eq!(descriptions.len(), 1); assert!(descriptions .iter() .all(|description| description.chars().count() <= 200)); @@ -11466,7 +11548,7 @@ mod canvas_generation_tests { } #[test] - fn strict_slice_commit_rejects_non_four_slice_results_before_replacing_the_sheet() { + fn strict_slice_commit_rejects_empty_slice_results_before_replacing_the_sheet() { let temporary = tempfile::tempdir().expect("create strict slice project"); let root = temporary.path(); init_local_game_project_at(root, "strict-slices", "严格切片测试") @@ -11481,9 +11563,9 @@ mod canvas_generation_tests { &replacement_options(), |_| Ok(()), ) - .expect_err("strict spritesheet commit must require exactly four slices"); + .expect_err("strict spritesheet commit must require at least one slice"); - assert!(error.contains("恰好包含 4 个独立切片")); + assert!(error.contains("至少需要一个独立切片")); assert_eq!(fs::read(path).expect("read preserved sheet"), b"old-image"); assert!(!root .join("assets/art-spritesheet-slices/manifest.json") diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/prompt.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/prompt.rs index 460d8dfad..d9f583586 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/prompt.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/prompt.rs @@ -1791,29 +1791,27 @@ mod tests { #[test] fn agent_prompt_design_foundation_forbids_implementation_and_browser_validation() { - for editor_api_key_is_configured in [false, true] { - let prompt = game_creator_design_foundation_tool_plan_prompt( - "shared runtime contract", - editor_api_key_is_configured, - ); + let without_canvas = + game_creator_design_foundation_tool_plan_prompt("shared runtime contract", false); + assert!(without_canvas.contains("必须完成 memory/project.md 与 game/game_design.md")); + assert!(without_canvas.contains("不调用 canvas.asset_generate")); + assert!(without_canvas.contains("不伪造 assets/ui-prototype.png")); + assert!(without_canvas.contains("每个功能页面各写一行 @genarrative-ui-page")); + assert!(without_canvas.contains("不要自行运行任何验证命令")); + assert!(without_canvas.contains("不修改 game/index.html,不启动预览或试玩")); - assert!(prompt.contains("项目文件写入只允许 memory/project.md 与 game/game_design.md")); - assert!(prompt.contains("不得创建、修改、删除或补丁 game/index.html")); - assert!(prompt.contains("由 Runtime 在收束门内验证本人 owner 产物")); - assert!( - prompt.contains("不得调用 project.verify、command.run_limited、game.static_smoke") - ); - assert!(prompt.contains("preview.start 或 preview.validate")); - assert!(prompt.contains("最终静态验收仍属于 preview-readiness")); - assert!(prompt.contains("浏览器验收仍属于 preview-playtest")); - assert!(prompt.contains( - "不得通过 command.exec、command.start 或其他工具启动本地预览服务、浏览器、Playwright" - )); - } + let with_canvas = + game_creator_design_foundation_tool_plan_prompt("shared runtime contract", true); + assert!(with_canvas.contains("根据当前玩法需求编写规格和界面建议")); + assert!(with_canvas.contains("不要使用固定图片合同")); + assert!(with_canvas.contains( + "如需图片,明确说明用途、数量、输出路径、尺寸、参考资源和是否需要 spritesheet" + )); + assert!(with_canvas.contains("不修改 game/index.html,不启动预览或试玩")); } #[test] - fn agent_prompt_design_foundation_preserves_the_canvas_specific_contract() { + fn agent_prompt_design_foundation_keeps_canvas_conditional_without_fixed_image_contract() { let without_canvas = game_creator_design_foundation_tool_plan_prompt("shared runtime contract", false); assert!(without_canvas.contains("不调用 canvas.asset_generate")); @@ -1823,26 +1821,10 @@ mod tests { let with_canvas = game_creator_design_foundation_tool_plan_prompt("shared runtime contract", true); assert!(with_canvas.contains("再调用 canvas.asset_generate")); - assert!(with_canvas.contains("assets/art-spec.png")); - assert!(with_canvas.contains("referenceImageSrcs 第一项")); - assert!(with_canvas.contains("assets/ui-prototype.png")); - assert!(with_canvas.contains("调用 image.inspect")); - assert!(with_canvas.contains("ui-prototype.v2")); - assert!(with_canvas.contains("成功只表示候选图片已生成并登记,不等于视觉验收完成")); - assert!(with_canvas.contains("由 Runtime 在收束门内同时核对固定 owner 文档")); - assert!(!with_canvas.contains("成功动作本身就是当前 revision 的验证")); - assert!(with_canvas.contains("调用 ui.workflow.run")); - assert!(with_canvas.contains("visual-binding 最终编辑器路由")); - assert!(with_canvas.contains("每个功能页面各写一行 @genarrative-ui-page")); - assert!(with_canvas.contains("ui.workflow.run 的 discover")); - assert!(with_canvas.contains("assets/ui-pages/{pageId}.png")); - assert!(with_canvas.contains("informationHud")); - assert!(with_canvas.contains("failureRestartFlow")); - assert!(with_canvas.contains("不得假设为塔防")); - assert!(with_canvas - .contains("POST /api/external/v1/editor/images/generations(kind=ui-design)")); - assert!(with_canvas - .contains("不得误用 POST /api/external/v1/editor/ui-designs/assets/extractions")); + assert!(with_canvas.contains("不要使用固定图片合同")); + assert!(!with_canvas.contains("assets/art-spec.png")); + assert!(!with_canvas.contains("ui-prototype.v2")); + assert!(!with_canvas.contains("informationHud")); } #[test] @@ -1856,15 +1838,17 @@ mod tests { let with_canvas = game_creator_art_director_tool_plan_prompt("shared runtime contract", true); - assert!(with_canvas.contains("outputPath=assets/art-spec.png")); - assert!(with_canvas.contains("assetKind=icon-spec")); - assert!(with_canvas.contains("成功只表示固定候选已生成并登记,不等于视觉门已经通过")); - assert!(with_canvas.contains("由 Runtime 在收束时核对当前 revision")); - assert!(!with_canvas.contains("成功会为本人当前 revision 形成验证凭证")); + assert!(with_canvas.contains( + "根据项目实际需要选择 canvas.asset_generate 的 assetKind、outputPath、尺寸、比例和提示词" + )); + assert!(with_canvas.contains("可以生成一张或多张图片,也可以不生成图片")); + assert!(with_canvas.contains("不要假设固定图片名称、数量、素材类别或布局")); + assert!(!with_canvas.contains("outputPath=assets/art-spec.png")); + assert!(!with_canvas.contains("assetKind=icon-spec")); } #[test] - fn agent_prompt_art_asset_plan_uses_fixed_transparent_spritesheet_route_and_warnings() { + fn agent_prompt_art_asset_plan_uses_need_based_generation_contract() { let without_canvas = game_creator_art_asset_plan_tool_plan_prompt("shared runtime contract", false); assert!(without_canvas.contains("不调用 canvas.asset_generate")); @@ -1872,21 +1856,15 @@ mod tests { let with_canvas = game_creator_art_asset_plan_tool_plan_prompt("shared runtime contract", true); - assert!(with_canvas.contains("用 asset.list 确认 assets/art-spec.png 已登记")); - assert!(with_canvas.contains("由 Runtime 形成 iconDescriptions")); - assert!(with_canvas.contains("玩家主体及朝向/状态")); - assert!(with_canvas.contains("不得假设为塔防")); - assert!(with_canvas.contains("权威 resourceId 作为 referenceId")); - assert!(with_canvas.contains("POST /api/external/v1/editor/icon-spritesheets/generations")); - assert!(with_canvas.contains("不得回退普通生图或 UI extraction")); - assert!(with_canvas.contains("screenColor=auto")); - assert!(with_canvas.contains("缺少规范图时必须等待 art-director")); - assert!(with_canvas.contains("不得回退普通生图")); - assert!(with_canvas.contains("真实 alpha")); - assert!(with_canvas.contains("warning.code=postprocess-failed-source-preserved")); - assert!(with_canvas.contains("不得登记、验收或自动重试")); - assert!(with_canvas.contains("仅 sliceWarning")); - assert!(with_canvas.contains("由 Runtime 在收束门内验证本人固定 manifest 产物")); + assert!(with_canvas.contains("使用 asset.list 了解已有资源")); + assert!(with_canvas.contains("按需调用 canvas.asset_generate")); + assert!(with_canvas.contains("切片布局和尺寸由当前需求决定")); + assert!(with_canvas.contains("不得套用固定图片包或固定 2x2")); + assert!(with_canvas.contains("spritesheet 可通过 sliceCount 指定切片数量")); + assert!(with_canvas.contains("也可以生成普通单图或多张独立图片")); + assert!(with_canvas.contains("生成后核对资源登记、透明度、警告和实际使用情况")); + assert!(!with_canvas.contains("screenColor=auto")); + assert!(!with_canvas.contains("icon-spritesheets/generations")); } #[test] diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs index a617d7f67..360497553 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs @@ -2012,14 +2012,17 @@ mod tests { } #[test] - fn autonomous_initial_collaboration_requires_leader_artifacts() { - let mut plan = autonomous_initial_leader_plan(); - plan.actions[1] = autonomous_initial_delegate("art-director", &[]); + fn autonomous_initial_collaboration_allows_need_based_art_artifacts() { + validate_agent_runtime_autonomous_initial_collaboration_contract( + &autonomous_initial_leader_plan(), + ) + .expect("empty need-based art artifacts must be accepted"); - let error = validate_agent_runtime_autonomous_initial_collaboration_contract(&plan) - .expect_err("art artifact must be required"); - - assert!(error.contains("expectedArtifacts 必须包含 assets/art-spec.png")); + let mut declared_plan = autonomous_initial_leader_plan(); + declared_plan.actions[1] = + autonomous_initial_delegate("art-director", &["assets/art-spec.png"]); + validate_agent_runtime_autonomous_initial_collaboration_contract(&declared_plan) + .expect("explicitly declared art artifacts must stay accepted"); } #[test] diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs index 326eab51a..3d0b4f7b1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs @@ -1924,7 +1924,7 @@ mod tests { } #[tokio::test] - async fn art_director_ready_task_is_read_only_without_key_and_canvas_owner_with_key() { + async fn art_director_ready_task_stays_read_only_with_or_without_editor_key() { let task = seed_task("art-director"); { let _config_guard = crate::tests::write_test_local_config("{}".to_string()); @@ -1950,29 +1950,26 @@ mod tests { ), async { let prompt = render_autonomous_manifest_ready_task_background_prompt(&task); - assert!(prompt.contains("非只读视觉规范生成任务")); - assert!(prompt.contains( - crate::agent::AGENT_RUNTIME_AUTONOMOUS_ART_DIRECTOR_CANVAS_ONLY_TASK_MARKER - )); - assert!(prompt.contains("生成并登记 assets/art-spec.png")); - assert!(prompt.contains("会同时提交当前 run 的 mutation 与验证凭证")); + assert!(prompt.contains("无生图凭据只读协调任务")); + assert!(!prompt.contains("非只读视觉规范生成任务")); + assert!(!prompt.contains("生成并登记 assets/art-spec.png")); assert!(prompt.contains("Runtime 会在子 Run 终态后幂等投影 manifest")); - assert!(!prompt.contains("无生图凭据只读协调任务")); - assert!(autonomous_manifest_ready_task_requires_visual_asset( + assert!(!autonomous_manifest_ready_task_requires_visual_asset( "art-director" )); - assert!(!agent_runtime_task_requires_read_only_delivery( + assert!(agent_runtime_task_requires_read_only_delivery( "art-director", &prompt )); let art_asset_prompt = render_autonomous_manifest_ready_task_background_prompt( &seed_task("art-asset-plan"), ); - assert!(art_asset_prompt.contains("canvas.asset_generate")); - assert!(art_asset_prompt.contains("asset.list")); - assert!(art_asset_prompt.contains("file.write 写入 assets/manifest.art.json")); - assert!(art_asset_prompt.contains("不要调用 image.inspect")); - assert!(art_asset_prompt.contains("把结构化计划最后一步标记 completed")); + assert!(art_asset_prompt.contains( + "任务声明中的视觉图片按项目需求选择工具、数量、输出路径、尺寸和布局" + )); + assert!(art_asset_prompt.contains("需要图集时用 sliceCount 指定切片数量")); + assert!(art_asset_prompt + .contains("Runtime 只核对实际声明的资源登记,不要求固定图片合同")); assert!(!art_asset_prompt.contains("按现有 visual gate 生成、登记并验收")); }, ) diff --git a/apps/ai-game-creator-shell/src-tauri/src/preview.rs b/apps/ai-game-creator-shell/src-tauri/src/preview.rs index f79c9bc39..568c9bdee 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/preview.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/preview.rs @@ -1645,7 +1645,7 @@ mod tests { #[test] fn npm_preview_requires_build_and_prefers_bundled_assets() { - let base = PathBuf::from(std::env::var("HOME").unwrap()).join("data/tmp"); + let base = std::env::temp_dir().join("data/tmp"); fs::create_dir_all(&base).unwrap(); let root = tempfile::tempdir_in(base).unwrap(); fs::write(root.path().join("package.json"), "{}").unwrap(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/export.rs b/apps/ai-game-creator-shell/src-tauri/src/project/export.rs index 6b4f7f43c..dab20a682 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/export.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/export.rs @@ -314,7 +314,7 @@ mod npm_export_tests { #[test] fn npm_package_contains_only_dist_and_publish_readme() { - let base = PathBuf::from(std::env::var("HOME").unwrap()).join("data/tmp"); + let base = std::env::temp_dir().join("data/tmp"); fs::create_dir_all(&base).unwrap(); let root = tempfile::tempdir_in(base).unwrap(); for directory in ["dist/assets", "assets", "exports", "node_modules", "game"] { diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/verification.rs b/apps/ai-game-creator-shell/src-tauri/src/project/verification.rs index 245149735..e6c7d44be 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/verification.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/verification.rs @@ -896,7 +896,7 @@ mod npm_build_tests { #[test] fn built_smoke_checks_module_files_without_inline_canvas() { - let base = PathBuf::from(std::env::var("HOME").unwrap()).join("data/tmp"); + let base = std::env::temp_dir().join("data/tmp"); fs::create_dir_all(&base).unwrap(); let root = tempfile::tempdir_in(base).unwrap(); fs::create_dir_all(root.path().join("dist/assets")).unwrap(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/delegation.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/delegation.rs index 53ee57d2d..77d6640bb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/delegation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/delegation.rs @@ -1132,91 +1132,8 @@ async fn background_agent_runtime_delegate_respects_project_policy() { fs::remove_dir_all(root).ok(); } -#[tokio::test] -async fn visual_specialists_reject_overriding_their_fixed_image_contract() { - let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "视觉固定输出合同测试").expect("project init"); - write_project_permission_policy_at( - &root, - ProjectPermissionPolicy { - denied_commands: Vec::new(), - confirm_commands: Vec::new(), - agent_policies: BTreeMap::new(), - }, - ) - .expect("allow canvas generation"); - let action = AgentRuntimeToolAction { - tool: "canvas.asset_generate".to_string(), - reason: Some("尝试覆盖策划固定图片合同".to_string()), - input: serde_json::json!({ - "prompt": "生成横屏界面原型", - "outputPath": "assets/wrong-prototype.png", - "aspectRatio": "1:1", - "imageSize": "2K", - "assetKind": "game-art", - "assetLabel": "错误标签", - "replaceExisting": false - }), - }; - - let observation = execute_game_creator_agent_runtime_tool_action_with_action_id( - &root, - "design-foundation", - "visual-fixed-contract-run", - "必须交付固定原型图", - &action, - Some("visual-fixed-contract-action"), - ) - .await; - - assert_eq!(observation.status, "failed"); - for expected in [ - "不能覆盖固定输出合同", - "outputPath=assets/ui-prototype.png", - "aspectRatio=16:9", - "imageSize=2K", - "assetKind=ui-prototype", - "assetLabel=游戏横屏界面原型图", - ] { - assert!(observation.summary.contains(expected)); - } - assert_eq!(observation.detail, None); - assert!(!root.join("assets/wrong-prototype.png").exists()); - assert!(read_manifest_for_project(&root) - .expect("manifest after rejected override") - .assets - .is_empty()); - - let one_k_action = AgentRuntimeToolAction { - tool: "canvas.asset_generate".to_string(), - reason: Some("尝试使用会返回 3:2 文件的 1K 规格".to_string()), - input: serde_json::json!({ - "prompt": "生成横屏界面原型", - "outputPath": "assets/ui-prototype.png", - "aspectRatio": "16:9", - "imageSize": "1K", - "assetKind": "ui-prototype", - "assetLabel": "游戏横屏界面原型图", - "replaceExisting": false - }), - }; - let one_k_observation = execute_game_creator_agent_runtime_tool_action_with_action_id( - &root, - "design-foundation", - "visual-fixed-contract-run", - "UI 原型必须使用真正的 16:9 输出规格", - &one_k_action, - Some("visual-fixed-contract-1k-action"), - ) - .await; - assert_eq!(one_k_observation.status, "failed"); - assert!(one_k_observation.summary.contains("不能覆盖固定输出合同")); - - fs::remove_dir_all(root).ok(); -} - #[test] -fn design_foundation_rejects_scene_image_stale_run_and_stale_sha_visual_proofs() { +fn design_foundation_finalization_ignores_ui_prototype_inspection_proofs() { let _platform_session = crate::platform_session::install_test_platform_session( "visual-semantic-gate-test-user", "visual-semantic-gate-test-key", @@ -1245,108 +1162,18 @@ fn design_foundation_rejects_scene_image_stale_run_and_stale_sha_visual_proofs() .expect("read UI prototype revision") .revision; - let uninspected = finish_game_creator_agent_background_runtime_turn_at( - &root, - state.clone(), - "只有文件登记不能证明它是真正的 UI 原型。", - revision, - &[], - ) - .expect("missing visual verdict remains recoverable"); - let uninspected_blocker = match uninspected { - AgentBackgroundFinalizationOutcome::Stale(blocker) => blocker, - _ => panic!("uninspected image must not complete design-foundation"), - }; - assert!(uninspected_blocker - .detail - .as_deref() - .is_some_and(|detail| detail.contains("requiredInspection=image.inspect"))); - - append_ui_prototype_inspection_fixture_with_profile(&root, run_id, true, "ui-prototype.v1"); - let legacy_v1 = finish_game_creator_agent_background_runtime_turn_at( - &root, - state.clone(), - "历史 v1 审计不能放行当前 run。", - revision, - &[], - ) - .expect("legacy v1 remains recoverable but not authoritative"); - assert!(matches!( - legacy_v1, - AgentBackgroundFinalizationOutcome::Stale(ref blocker) - if blocker.tool == "runtime.visual_asset" - )); - - append_ui_prototype_inspection_fixture(&root, run_id, false); - let scene_blocked = finish_game_creator_agent_background_runtime_turn_at( - &root, - state.clone(), - "场景图不能冒充 UI 原型。", - revision, - &[], - ) - .expect("scene verdict remains recoverable"); - let scene_blocker = match scene_blocked { - AgentBackgroundFinalizationOutcome::Stale(blocker) => blocker, - _ => panic!("scene image must not complete design-foundation"), - }; - assert_eq!(scene_blocker.tool, "runtime.visual_asset"); - assert!(scene_blocker.summary.contains("尚未通过结构化 UI 视觉检查")); - assert!(scene_blocker - .detail - .as_deref() - .is_some_and(|detail| detail.contains("informationHud=false"))); - - append_ui_prototype_inspection_fixture(&root, "another-design-run", true); - let wrong_run = finish_game_creator_agent_background_runtime_turn_at( - &root, - state.clone(), - "其他 run 的证据不能放行。", - revision, - &[], - ) - .expect("wrong run verdict remains recoverable"); - assert!(matches!( - wrong_run, - AgentBackgroundFinalizationOutcome::Stale(ref blocker) - if blocker.tool == "runtime.visual_asset" - )); - - append_ui_prototype_inspection_fixture(&root, run_id, true); - let mut replacement = valid_test_png_bytes(); - replacement.extend_from_slice(b"changed-ui-prototype"); - fs::write(root.join(AGENT_RUNTIME_UI_PROTOTYPE_PATH), replacement) - .expect("replace UI prototype fixture"); - let stale_sha = finish_game_creator_agent_background_runtime_turn_at( - &root, - state.clone(), - "旧图片 SHA 的证据不能放行。", - revision, - &[], - ) - .expect("stale sha verdict remains recoverable"); - let stale_sha_blocker = match stale_sha { - AgentBackgroundFinalizationOutcome::Stale(blocker) => blocker, - _ => panic!("stale image proof must not complete design-foundation"), - }; - assert!(stale_sha_blocker - .detail - .as_deref() - .is_some_and(|detail| detail.contains("requiredInspection=image.inspect"))); - - append_ui_prototype_inspection_fixture(&root, run_id, true); - let completed = finish_game_creator_agent_background_runtime_turn_at( + let outcome = finish_game_creator_agent_background_runtime_turn_at( &root, state, - "当前图片已通过全部八项 UI 原型检查。", + "不再要求固定 UI 原型检查凭证。", revision, &[], ) - .expect("current passed UI proof allows finalization"); - assert!(matches!( - completed, - AgentBackgroundFinalizationOutcome::Completed(_) - )); + .expect("need-based design finalization"); + assert!( + matches!(outcome, AgentBackgroundFinalizationOutcome::Completed(_)), + "{outcome:?}" + ); fs::remove_dir_all(root).ok(); } @@ -1804,6 +1631,12 @@ async fn canvas_replacement_rejects_parent_run_that_terminates_during_external_r "替换不符合原创要求的正式图片", &serde_json::json!({ "prompt": "生成原创晶体与潮汐构装体图集", + "outputPath": "assets/art-spritesheet.png", + "assetKind": "art-spritesheet", + "assetLabel": "游戏首版核心美术素材", + "aspectRatio": "1:1", + "imageSize": "1K", + "sliceCount": 4, "replaceExisting": true }), ), @@ -2181,7 +2014,7 @@ fn gui_and_cli_autonomous_game_build_runs_still_allow_publish_delegates() { } #[test] -fn visual_specialist_delegations_require_image_artifacts_but_read_only_work_allows_none() { +fn visual_specialist_delegations_accept_need_based_artifacts_and_read_only_work_allows_none() { let _platform_session = crate::platform_session::install_test_platform_session( "visual-delegation-contract-user", "visual-delegation-contract-key", @@ -2204,11 +2037,11 @@ fn visual_specialist_delegations_require_image_artifacts_but_read_only_work_allo ) .expect("start supervisor parent runtime"); - for (agent_id, required_path) in [ + for (agent_id, output_path) in [ ("design-foundation", "assets/ui-prototype.png"), ("art-asset-plan", "assets/art-spritesheet.png"), ] { - let action_id = format!("reject-empty-visual-{agent_id}"); + let action_id = format!("accept-need-based-visual-{agent_id}"); let observation = observe_agent_runtime_agent_delegate( &root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, @@ -2223,17 +2056,18 @@ fn visual_specialist_delegations_require_image_artifacts_but_read_only_work_allo "runId": null }), ); - assert_eq!(observation.status, "failed"); - assert!(observation.summary.contains(required_path)); + assert_eq!(observation.status, "ok", "{observation:?}"); + assert!(!observation.summary.contains(output_path)); let delegation_id = agent_runtime_delegation_id( GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, parent_run_id, agent_id, &action_id, ); - assert!(read_static_delegate_delivery_at(&root, &delegation_id) - .expect("read rejected visual delivery") - .is_none()); + let delivery = read_static_delegate_delivery_at(&root, &delegation_id) + .expect("read need-based visual delivery") + .expect("need-based visual delivery exists"); + assert!(delivery.expected_artifacts.is_empty()); } let target_agent_id = "quality-review"; diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/parallel_actions.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/parallel_actions.rs index bd25431d6..1b1667370 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/parallel_actions.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/parallel_actions.rs @@ -653,7 +653,7 @@ fn parallel_read_batch_finishes_before_concurrent_steer_is_accepted() { #[tokio::test] async fn provider_action_batch_verification_then_parallel_reads_share_current_gate_snapshot() { let root = unique_project_path(); - init_local_game_project_at( + init_legacy_single_html_project_at( &root, "project-provider-batch-verification-parallel-read", "Provider 批次验证后并行读取测试", 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 4ec311a4a..bdbea207b 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 @@ -944,33 +944,12 @@ fn visual_specialist_prompts_require_real_registered_image_deliveries() { AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, ); for expected in [ - "文本策划只是中间结果", - "canvas.asset_generate", - "16:9", - "2K", - "assets/ui-prototype.png", - "assetKind=ui-prototype", - "asset.list", - "image.inspect", - "ui-prototype.v2", - "informationHud", - "gameplaySurface", - "objectiveEntities", - "failureRestartFlow", - "responsiveLayout", - "不得假设为塔防", - "原创标题、实体、资源、目标名称与视觉语言", - "assets/art-spec.png", - "icon-spec", - "referenceImageSrcs 第一项", - "POST /api/external/v1/editor/images/generations(kind=ui-design)", - "不得误用 POST /api/external/v1/editor/ui-designs/assets/extractions", - "canvas.asset_generate 成功只表示候选图片已生成并登记,不等于视觉验收完成", - "由 Runtime 在收束门内同时核对固定 owner 文档", - "已有同路径画布资产时先核对登记", - "检查已通过时不得重复生成或再次扣费", - "纯场景图", - "不得把计划写完当成 completed", + "根据当前玩法需求编写规格和界面建议", + "明确说明用途、数量、输出路径、尺寸、参考资源和是否需要 spritesheet", + "再调用 canvas.asset_generate", + "不要使用固定图片合同", + "不创建固定图片槽位,不规定固定数量或布局", + "不修改 game/index.html,不启动预览或试玩", ] { assert!( design_prompt.contains(expected), @@ -983,14 +962,10 @@ fn visual_specialist_prompts_require_real_registered_image_deliveries() { AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, ); for expected in [ - "assets/art-spec.png", - "assetKind=icon-spec", - "POST /api/external/v1/editor/images/generations(kind=spec)", - "后续 UI 和透明图集共同引用", - "不得用 generationInputs.artSpec JSON", - "成功只表示固定候选已生成并登记,不等于视觉门已经通过", - "由 Runtime 在收束时核对当前 revision", - "缺少 resourceId 时不得提交最终回复", + "根据项目实际需要选择 canvas.asset_generate 的 assetKind、outputPath、尺寸、比例和提示词", + "可以生成一张或多张图片,也可以不生成图片", + "需要参考图时使用已登记资源 ID", + "不要假设固定图片名称、数量、素材类别或布局", ] { assert!( director_prompt.contains(expected), @@ -1003,28 +978,12 @@ fn visual_specialist_prompts_require_real_registered_image_deliveries() { AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, ); for expected in [ - "资产清单和美术计划只是中间结果", - "canvas.asset_generate", - "assets/manifest.art.json", - "assets/art-spritesheet.png", - "固定使用 1:1、1K", - "assetKind=art-spritesheet", - "用 asset.list 确认 assets/art-spec.png 已登记", - "由 Runtime 形成 iconDescriptions", - "权威 resourceId 作为 referenceId", - "POST /api/external/v1/editor/icon-spritesheets/generations", - "screenColor=auto", - "不得回退普通生图", - "回读 observation 与 asset.list", - "真实 alpha", - "warning.code=postprocess-failed-source-preserved", - "不得登记、验收或自动重试", - "仅 sliceWarning", - "已有有效同路径资产时不得重复生成或扣费", - "asset.list", - "不得运行 game.static_smoke 或 preview.validate", - "不得编辑 game/index.html", - "透明证据不足时不得提交最终回复", + "使用 asset.list 了解已有资源", + "按需调用 canvas.asset_generate", + "切片布局和尺寸由当前需求决定", + "不得套用固定图片包或固定 2x2", + "spritesheet 可通过 sliceCount 指定切片数量", + "生成后核对资源登记、透明度、警告和实际使用情况", ] { assert!( art_prompt.contains(expected), diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/command_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/command_runtime.rs index 8e28db619..a0f45ec61 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/command_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/command_runtime.rs @@ -2330,7 +2330,8 @@ async fn executed_project_verification_audit_failure_requires_reconciliation() { #[test] fn project_verification_gate_accepts_only_exact_game_static_smoke() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "精确静态验证项目").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "精确静态验证项目") + .expect("project init"); advance_project_revision_for_test(&root, "playtest", "playtest-run", "file.write"); fs::write( root.join("game/index.html"), @@ -2536,7 +2537,7 @@ fn agent_runtime_project_verify_summary_hashes_command_without_head_or_tail() { #[test] fn limited_local_command_runs_static_game_smoke_and_writes_log() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "像素动作原型").expect("project init"); fs::write( root.join("game/index.html"), fake_llm_game_draft().game_html, @@ -2866,7 +2867,7 @@ async fn project_verification_runs_without_prepost_and_records_failure_and_timeo #[test] fn limited_local_command_appends_playtest_to_existing_trace() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "像素动作原型").expect("project init"); fs::write( root.join("game/index.html"), fake_llm_game_draft().game_html, @@ -2915,7 +2916,7 @@ fn limited_local_command_appends_playtest_to_existing_trace() { #[test] fn limited_local_command_rejects_placeholder_game_smoke() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "像素动作原型").expect("project init"); let error = run_limited_local_command_at(&root, "game.static_smoke") .expect_err("placeholder game should fail smoke"); @@ -2927,7 +2928,7 @@ fn limited_local_command_rejects_placeholder_game_smoke() { #[test] fn limited_local_command_rejects_forbidden_runtime_apis() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "像素动作原型").expect("project init"); let html = fake_llm_game_draft() .game_html .replace("const marker =", "fetch('/secret');\n const marker ="); @@ -2943,7 +2944,7 @@ fn limited_local_command_rejects_forbidden_runtime_apis() { #[test] fn limited_local_command_rejects_blank_canvas_game_smoke() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "像素动作原型").expect("project init"); let html = r#" @@ -2970,7 +2971,8 @@ fn limited_local_command_rejects_blank_canvas_game_smoke() { #[test] fn limited_local_command_rejects_invalid_javascript_before_surface_checks() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "脚本语法优先验证测试").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "脚本语法优先验证测试") + .expect("project init"); let html = r#" @@ -2994,7 +2996,8 @@ fn limited_local_command_rejects_invalid_javascript_before_surface_checks() { #[test] fn limited_local_command_rejects_token_rich_truncated_script() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "截断游戏入口测试").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "截断游戏入口测试") + .expect("project init"); let html = r#" 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 da15d08a6..98df64a6f 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 @@ -1944,7 +1944,7 @@ fn validate_llm_game_draft_accepts_win_condition_as_goal() { #[tokio::test] async fn agent_run_resume_restarts_generation_from_latest_goal() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "像素动作原型").expect("project init"); write_agent_run_trace( &root, "run-control-resume", 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 bf2635753..8376a8aed 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 @@ -314,6 +314,37 @@ fn unique_project_path() -> PathBuf { )) } +/// npm + Vite 脚手架出现前的默认单文件入口,占位文案仍内联在 HTML 里。 +const LEGACY_GAME_INDEX_HTML: &str = r#" + + + + + Genarrative Game Draft + + +
还没有生成游戏。回到聊天输入创意并确认生成后,这里会写入可试玩原型。
+ +"#; + +/// 旧版单文件 HTML 项目:JSON Generator、game.static_smoke、预览与导出都走 +/// `game/index.html` 布局。必须在初始化前写入入口,否则 init 会播种 npm + Vite 脚手架, +/// 后续门禁就会改为只认 `game/dist` 构建产物。 +pub(crate) fn init_legacy_single_html_project_at( + root: &Path, + project_id: &str, + name: &str, +) -> Result { + fs::create_dir_all(root.join("game")) + .map_err(|error| format!("创建旧版单文件项目目录失败:{error}"))?; + fs::write(root.join("game/index.html"), LEGACY_GAME_INDEX_HTML) + .map_err(|error| format!("写入旧版单文件游戏入口失败:{error}"))?; + init_local_game_project_at(root, project_id, name) +} + pub(crate) fn freeze_test_root_goal_contract_at( root: &Path, run_id: &str, 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 ca5ce863e..20d87ced5 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 @@ -550,7 +550,7 @@ fn canonical_visual_completion_requires_persisted_route_kind_and_current_spec_re #[tokio::test] async fn background_agent_runtime_can_start_local_preview() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "月光厨房").expect("project init"); fs::write( root.join("game/index.html"), fake_llm_game_draft().game_html, @@ -1289,6 +1289,8 @@ async fn platform_art_external_request_does_not_hold_project_lock_or_overwrite_m #[tokio::test] async fn generate_local_game_draft_fails_after_max_passes_without_final_artifacts() { let root = unique_project_path(); + init_legacy_single_html_project_at(&root, "project-1", "厨房游戏重试测试") + .expect("project init"); let mut invalid_draft = fake_llm_game_draft(); invalid_draft.game_html = r#" @@ -2590,7 +2592,7 @@ fn local_project_file_commands_read_write_list_and_delete_text_files() { #[test] fn local_project_export_package_uses_runtime_whitelist_and_records() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "像素动作原型").expect("project init"); write_local_project_file_at(&root, "game/index.html", &fake_llm_game_draft().game_html) .expect("write playable html"); write_local_project_file_at(&root, "assets/hero.txt", "hero asset").expect("write asset"); @@ -2709,7 +2711,7 @@ fn local_project_export_package_list_skips_symlink_packages() { #[test] fn local_project_export_package_rejects_symlink_assets() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "像素动作原型").expect("project init"); write_local_project_file_at(&root, "game/index.html", &fake_llm_game_draft().game_html) .expect("write playable html"); write_local_project_file_at(&root, "exports/README.md", "playtest notes") @@ -2731,7 +2733,7 @@ fn local_project_export_package_rejects_symlink_assets() { #[test] fn local_project_export_package_rejects_symlink_runtime_dirs_and_readme() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "像素动作原型").expect("project init"); write_local_project_file_at(&root, "game/index.html", &fake_llm_game_draft().game_html) .expect("write playable html"); write_local_project_file_at(&root, "memory/project.md", "private memory") @@ -2763,7 +2765,7 @@ fn local_project_export_package_rejects_symlink_runtime_dirs_and_readme() { #[test] fn local_project_export_package_requires_playable_html_and_readme() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "像素动作原型").expect("project init"); let missing_readme = export_local_project_package_at(&root).expect_err("default html is not playable"); assert!(missing_readme.contains("游戏入口必须包含可渲染画布")); @@ -3640,7 +3642,7 @@ fn cli_agent_resume_without_appdata_fails_before_runtime_dispatch() { #[test] fn local_preview_server_serves_game_index() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "像素动作原型").expect("project init"); fs::write(root.join("assets/player.png"), b"PNGDATA").expect("asset"); let (preview, stop) = start_local_game_preview_for_project(&root).expect("preview start"); @@ -4619,7 +4621,7 @@ fn local_preview_bridge_injection_stays_outside_unclosed_inert_html_contexts() { #[test] fn local_preview_server_drains_split_browser_headers_before_response() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "鍍忕礌鍔ㄤ綔鍘熷瀷").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "像素动作原型").expect("project init"); let (preview, stop) = start_local_game_preview_for_project(&root).expect("preview start"); let mut stream = TcpStream::connect(("127.0.0.1", preview.port)).expect("preview connect"); @@ -4699,7 +4701,7 @@ fn preview_content_type_covers_common_game_assets() { #[test] fn local_preview_head_preserves_asset_content_length() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "像素动作原型").expect("project init"); fs::write(root.join("assets/player.png"), b"PNGDATA").expect("asset"); let response = build_preview_response(&root, "HEAD", "/assets/player.png"); @@ -4741,7 +4743,7 @@ fn local_preview_project_revision_reports_the_current_atomic_sidecar() { #[test] fn local_preview_start_rejects_a_stale_validated_revision_atomically() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "预览 revision 启动门禁测试") + init_legacy_single_html_project_at(&root, "project-1", "预览 revision 启动门禁测试") .expect("project init"); let revision = advance_project_revision_for_test( &root, @@ -4768,7 +4770,8 @@ fn local_preview_start_rejects_a_stale_validated_revision_atomically() { #[test] fn stale_preview_cleanup_does_not_stop_a_newer_matching_project_server() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "预览原子停止测试").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "预览原子停止测试") + .expect("project init"); let registry = PreviewRegistry::default(); let (first, first_stop) = start_local_game_preview_for_project(&root).expect("first preview start"); @@ -4799,7 +4802,8 @@ fn stale_preview_cleanup_does_not_stop_a_newer_matching_project_server() { #[test] fn stale_preview_cleanup_cannot_be_blocked_by_project_stop_policy() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "预览补偿清理策略测试").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "预览补偿清理策略测试") + .expect("project init"); write_project_permission_policy_at( &root, ProjectPermissionPolicy { @@ -4826,6 +4830,7 @@ fn stale_preview_cleanup_cannot_be_blocked_by_project_stop_policy() { #[test] fn local_preview_serves_generated_playable_game() { let root = unique_project_path(); + init_legacy_single_html_project_at(&root, "project-1", "预览试玩项目").expect("project init"); write_local_game_draft_at(&root, "像素风横版动作", &fake_llm_game_draft()) .expect("draft should generate"); 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 6175d5e76..dd617b3f9 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 @@ -98,6 +98,7 @@ async fn request_llm_game_draft_uses_openai_compatible_provider_output() { #[tokio::test] async fn generate_local_game_draft_sends_asset_context_to_llm() { let root = unique_project_path(); + init_legacy_single_html_project_at(&root, "project-1", "资产上下文项目").expect("project init"); let uploaded = upload_local_asset_at(&root, "../角色.png", "image/png", b"fake-png") .expect("asset upload"); append_local_conversation_message_at( @@ -1344,7 +1345,7 @@ async fn provider_action_batch_agent_db_failure_does_not_advance_cursor() { #[tokio::test] async fn provider_action_batch_mutation_then_verification_rolls_forward_gate() { let root = unique_project_path(); - init_local_game_project_at( + init_legacy_single_html_project_at( &root, "project-provider-batch-mutation-verification", "Provider 批次修改后验证测试", @@ -5864,7 +5865,7 @@ async fn background_agent_runtime_cancellation_wins_over_inflight_llm_error() { #[tokio::test] async fn agent_loop_uses_per_agent_llm_overrides() { let root = unique_project_path(); - init_local_game_project_at(&root, "local-project-draft", "未命名游戏原型") + init_legacy_single_html_project_at(&root, "local-project-draft", "未命名游戏原型") .expect("init project"); let (planner_sender, planner_receiver) = mpsc::channel(); let planner_base_url = spawn_mock_llm_server_responses_with_capture( @@ -6081,7 +6082,7 @@ async fn agent_role_briefs_run_same_wave_llm_agents_in_parallel() { #[tokio::test] async fn agent_loop_writes_spec_findings_and_retries_generator() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "月光厨房").expect("project init"); let mut first_draft = fake_llm_game_draft(); first_draft.game_html = r#" diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/response_stream.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/response_stream.rs index 32a9c3ec9..434c6c2c5 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/response_stream.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/response_stream.rs @@ -981,7 +981,7 @@ async fn provider_retry_final_reply_thinking_only_response_retries_before_handof } #[test] -fn visual_specialist_finalization_requires_existing_registered_canvas_image() { +fn visual_specialist_finalization_ignores_need_based_visual_assets() { let _platform_session = crate::platform_session::install_test_platform_session( "visual-finalization-test-user", "visual-finalization-test-key", @@ -990,22 +990,10 @@ fn visual_specialist_finalization_requires_existing_registered_canvas_image() { let _config_guard = crate::tests::write_test_local_config( r#"{"editorApi":{"apiKey":"visual-finalization-test-key"}}"#.to_string(), ); - for (agent_id, local_path, kind) in [ - ( - "design-foundation", - "assets/ui-prototype.png", - "ui-prototype", - ), - ( - "art-asset-plan", - "assets/art-spritesheet.png", - "art-spritesheet", - ), - ] { + for agent_id in ["design-foundation", "art-asset-plan"] { let root = unique_project_path(); init_local_game_project_at(&root, "project-1", "视觉 Runtime 完成门禁测试") .expect("project init"); - register_canvas_visual_asset_fixture(&root, "assets/art-spec.png", "icon-spec"); let run_id = format!("visual-finalization-{agent_id}"); let state = start_game_creator_agent_runtime_task_at( &root, @@ -1021,134 +1009,18 @@ fn visual_specialist_finalization_requires_existing_registered_canvas_image() { .expect("read visual revision") .revision; - let missing_file = finish_game_creator_agent_background_runtime_turn_at( - &root, - state.clone(), - "不能在缺图时完成", - revision, - &[], - ) - .expect("missing image is recoverable blocker"); - let blocker = match missing_file { - AgentBackgroundFinalizationOutcome::Stale(blocker) => blocker, - _ => panic!("missing image must block finalization"), - }; - assert_eq!(blocker.tool, "runtime.visual_asset"); - assert!(blocker.summary.contains("不能完成任务")); - assert!(blocker - .detail - .as_deref() - .is_some_and(|detail| detail.contains(local_path))); - - let absolute_path = root.join(local_path); - fs::create_dir_all(absolute_path.parent().expect("visual parent")) - .expect("create unregistered visual directory"); - fs::write( - &absolute_path, - if kind == "art-spritesheet" { - transparent_test_png_bytes() - } else { - valid_test_png_bytes() - }, - ) - .expect("write unregistered visual image"); - let unregistered = finish_game_creator_agent_background_runtime_turn_at( - &root, - state.clone(), - "不能在图片未登记时完成", - revision, - &[], - ) - .expect("unregistered image is recoverable blocker"); - assert!(matches!( - unregistered, - AgentBackgroundFinalizationOutcome::Stale(ref blocker) - if blocker.tool == "runtime.visual_asset" - )); - - register_local_asset_at( - &root, - local_path, - kind, - "image/png", - "canvas", - GameCreationAppAssetSource { - kind: GameCreationAppAssetSourceKind::Canvas, - canvas_project_id: Some("canvas-project-1".to_string()), - resource_id: Some(format!("resource-{kind}")), - asset_object_id: Some(format!("asset-object-{kind}")), - task_id: Some(format!("task-{kind}")), - prompt: Some("测试视觉资产".to_string()), - model: Some("gpt-image-2".to_string()), - generation_route: None, - generation_kind: None, - reference_resource_ids: Vec::new(), - }, - ) - .expect("register legacy canvas image"); - let legacy = finish_game_creator_agent_background_runtime_turn_at( - &root, - state.clone(), - "旧文件不能冒充正式视觉产物", - revision, - &[], - ) - .expect("legacy image is a recoverable blocker"); - assert!(matches!( - legacy, - AgentBackgroundFinalizationOutcome::Stale(ref blocker) - if blocker.tool == "runtime.visual_asset" - && blocker.detail.as_deref().is_some_and(|detail| detail.contains("legacy")) - )); - register_local_asset_at( - &root, - local_path, - kind, - "image/png", - "canvas", - GameCreationAppAssetSource { - kind: GameCreationAppAssetSourceKind::Canvas, - canvas_project_id: Some("canvas-project-1".to_string()), - resource_id: Some(format!("resource-{kind}")), - asset_object_id: Some(format!("asset-object-{kind}")), - task_id: Some(format!("task-{kind}")), - prompt: Some("测试视觉资产".to_string()), - model: Some("gpt-image-2".to_string()), - generation_route: Some( - if kind == "ui-prototype" { - "/api/external/v1/editor/images/generations" - } else { - "/api/external/v1/editor/icon-spritesheets/generations" - } - .to_string(), - ), - generation_kind: Some( - if kind == "ui-prototype" { - "ui-design" - } else { - "icon-spritesheet" - } - .to_string(), - ), - reference_resource_ids: vec!["resource-icon-spec".to_string()], - }, - ) - .expect("register provenanced canvas image"); - if agent_id == "design-foundation" { - append_ui_prototype_inspection_fixture(&root, &run_id, true); - } - let completed = finish_game_creator_agent_background_runtime_turn_at( + let outcome = finish_game_creator_agent_background_runtime_turn_at( &root, state, - "真实图片已经生成并登记。", + "不再要求固定视觉资产。", revision, &[], ) - .expect("registered image allows finalization"); - assert!(matches!( - completed, - AgentBackgroundFinalizationOutcome::Completed(_) - )); + .expect("need-based visual finalization"); + assert!( + matches!(outcome, AgentBackgroundFinalizationOutcome::Completed(_)), + "{outcome:?}" + ); fs::remove_dir_all(root).ok(); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/action_execution.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/action_execution.rs index b4dd0ec94..1614c65ab 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/action_execution.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/action_execution.rs @@ -539,7 +539,7 @@ async fn background_agent_runtime_can_read_other_agent_status() { #[tokio::test] async fn background_agent_runtime_can_run_limited_static_smoke() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init"); + init_legacy_single_html_project_at(&root, "project-1", "月光厨房").expect("project init"); let playable_game_html = fake_llm_game_draft().game_html; write_project_permission_policy_at( &root, @@ -1550,9 +1550,9 @@ async fn background_agent_runtime_deletes_file_then_verifies_before_completion() } #[tokio::test] -async fn art_asset_plan_cannot_delete_registered_fixed_visual_asset() { +async fn art_asset_plan_can_delete_registered_canvas_asset() { let root = unique_project_path(); - init_local_game_project_at(&root, "project-1", "固定美术资产删除门禁").expect("project init"); + init_local_game_project_at(&root, "project-1", "画布资产删除测试").expect("project init"); register_canvas_visual_asset_fixture(&root, "assets/art-spritesheet.png", "art-spritesheet"); write_project_permission_policy_at( &root, @@ -1566,27 +1566,26 @@ async fn art_asset_plan_cannot_delete_registered_fixed_visual_asset() { let action = AgentRuntimeToolAction { tool: "file.delete".to_string(), - reason: Some("尝试删除已登记的固定美术素材".to_string()), + reason: Some("按当前需求替换旧画布素材".to_string()), input: serde_json::json!({ "path": "assets/art-spritesheet.png" }), }; let observation = execute_game_creator_agent_runtime_tool_action_with_action_id( &root, "art-asset-plan", - "art-delete-protection-run", - "已登记的固定美术素材必须复用", + "art-delete-need-based-run", + "按当前需求替换画布素材", &action, - Some("art-delete-protection-action"), + Some("art-delete-need-based-action"), ) .await; - assert_eq!(observation.status, "blocked"); - assert!(observation.summary.contains("禁止删除固定正式产物")); - assert!(root.join("assets/art-spritesheet.png").is_file()); + assert_eq!(observation.status, "ok", "{observation:?}"); + assert!(!root.join("assets/art-spritesheet.png").exists()); assert_eq!( read_game_creator_agent_runtime_project_revision(&root) .expect("read revision") .revision, - 0 + 1 ); fs::remove_dir_all(root).ok(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/support.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/support.rs index b99ff0052..818b1b1d9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/support.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/support.rs @@ -13,8 +13,8 @@ pub(super) use super::super::{ advance_project_revision_for_test, agent_runtime_verification_plan, append_auto_tool_action_audit_pair_for_test, assert_auto_tool_action_audit_pair, assert_pending_runtime_decision_revalidates_after_lock, assert_task_status, - fake_llm_game_draft, final_tool_plan_response, mock_http_request_json, - native_agent_tool_plan_chat_response, pending_tool_action_for_test, + fake_llm_game_draft, final_tool_plan_response, init_legacy_single_html_project_at, + mock_http_request_json, native_agent_tool_plan_chat_response, pending_tool_action_for_test, persist_needs_reconciliation_runtime_for_test, persist_project_verification_for_test, read_agent_db_records_for_test, register_canvas_visual_asset_fixture, spawn_barrier_mock_llm_server, spawn_interruptible_mock_llm_server_with_capture, diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs index b77477437..6266c3e4c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs @@ -3990,7 +3990,7 @@ async fn background_task_recovers_when_assistant_message_cannot_persist() { } #[tokio::test] -async fn task_update_requires_registered_visual_asset_before_completion() { +async fn task_update_does_not_require_fixed_visual_assets_before_completion() { let _platform_session = crate::platform_session::install_test_platform_session( "visual-task-update-test-user", "visual-task-update-test-key", @@ -3999,26 +3999,7 @@ async fn task_update_requires_registered_visual_asset_before_completion() { let _config_guard = crate::tests::write_test_local_config( r#"{"editorApi":{"apiKey":"visual-task-update-test-key"}}"#.to_string(), ); - for (task_id, local_path, kind, missing_summary) in [ - ( - "art-director", - "assets/art-spec.png", - "icon-spec", - "统一视觉规范图尚未按正式视觉流程生成并登记", - ), - ( - "design-foundation", - "assets/ui-prototype.png", - "ui-prototype", - "策划界面原型图尚未按正式视觉流程生成并登记", - ), - ( - "art-asset-plan", - "assets/art-spritesheet.png", - "art-spritesheet", - "首版美术素材图尚未按正式视觉流程生成并登记", - ), - ] { + for task_id in ["art-director", "design-foundation", "art-asset-plan"] { let root = unique_project_path(); init_local_game_project_at(&root, "project-1", "视觉任务完成门禁测试") .expect("project init"); @@ -4040,71 +4021,16 @@ async fn task_update_requires_registered_visual_asset_before_completion() { "status": "completed" }), }; - let missing = execute_game_creator_agent_runtime_tool_action_with_action_id( + let observation = execute_game_creator_agent_runtime_tool_action_with_action_id( &root, task_id, &run_id, "完成视觉任务", &action, - Some("visual-task-update-missing"), + Some("visual-task-update-need-based"), ) .await; - assert_eq!(missing.status, "failed"); - assert!(missing.summary.contains(missing_summary)); - let manifest = read_manifest_for_project(&root).expect("manifest after rejected update"); - assert_eq!( - manifest - .tasks - .iter() - .find(|task| task.id == task_id) - .expect("visual task") - .status, - GameCreationAppTaskStatus::Pending - ); - - if task_id != "art-director" { - register_canvas_visual_asset_fixture(&root, "assets/art-spec.png", "icon-spec"); - } - if task_id == "art-asset-plan" { - register_canvas_visual_asset_fixture(&root, "assets/ui-prototype.png", "ui-prototype"); - } - register_canvas_visual_asset_fixture(&root, local_path, kind); - if task_id == "design-foundation" { - append_ui_prototype_inspection_fixture(&root, &run_id, false); - let scene_rejected = execute_game_creator_agent_runtime_tool_action_with_action_id( - &root, - task_id, - &run_id, - "拒绝用场景图完成 UI 原型任务", - &action, - Some("visual_task_update_scene_rejected"), - ) - .await; - assert_eq!(scene_rejected.status, "failed"); - assert!(scene_rejected.summary.contains("结构化 UI 视觉检查")); - let manifest = - read_manifest_for_project(&root).expect("manifest after rejected scene image"); - assert_eq!( - manifest - .tasks - .iter() - .find(|task| task.id == task_id) - .expect("design visual task") - .status, - GameCreationAppTaskStatus::Pending - ); - append_ui_prototype_inspection_fixture(&root, &run_id, true); - } - let completed = execute_game_creator_agent_runtime_tool_action_with_action_id( - &root, - task_id, - &run_id, - "完成视觉任务", - &action, - Some("visual-task-update-completed"), - ) - .await; - assert_eq!(completed.status, "ok", "{completed:?}"); + assert_eq!(observation.status, "ok", "{observation:?}"); let manifest = read_manifest_for_project(&root).expect("manifest after visual completion"); assert_eq!( manifest diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/sessions.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/sessions.rs index b9092b0fb..008338de0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/sessions.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/sessions.rs @@ -675,6 +675,8 @@ async fn background_agent_runtime_file_and_memory_writes_respect_project_policy( #[test] fn generate_local_game_draft_writes_memory_design_and_game() { let root = unique_project_path(); + init_legacy_single_html_project_at(&root, "project-1", "记忆与设计产物项目") + .expect("project init"); let draft = fake_llm_game_draft(); let result = write_local_game_draft_at(&root, "像素风横版动作