修复 AGC 原生壳 CI 测试与迁移后门禁不一致
Project CI / Repository checks (pull_request) Successful in 2m55s
Project CI / Frontend tests (pull_request) Successful in 3m13s
Project CI / Backend tests (pull_request) Successful in 6m11s
Project CI / Native shell tests (pull_request) Has been cancelled

- 新增 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 排障记录
This commit is contained in:
2026-09-09 10:59:33 +08:00
parent a0e6a83691
commit 7e4b404d5f
22 changed files with 303 additions and 601 deletions
@@ -4710,10 +4710,12 @@ mod tests {
assert!(!prompt.contains("你是 Codex")); assert!(!prompt.contains("你是 Codex"));
assert!(prompt.contains("不要等待 Supervisor")); assert!(prompt.contains("不要等待 Supervisor"));
assert!(prompt.contains("提示词与技能")); 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"));
assert!(prompt.contains("用户明确指定第三方 MCP Server 或工具时")); 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("agc_write_file"));
assert!(prompt.contains("content 必须是目标文件的完整原始 UTF-8 正文")); assert!(prompt.contains("content 必须是目标文件的完整原始 UTF-8 正文"));
assert!(prompt.contains("不得把 command.exec 的 Exit code、Wall time、Output 包装")); assert!(prompt.contains("不得把 command.exec 的 Exit code、Wall time、Output 包装"));
@@ -8895,7 +8895,7 @@ mod canvas_generation_tests {
} }
#[test] #[test]
fn strict_spritesheet_contract_requires_the_grid_2x2_provider_receipt() { fn strict_spritesheet_contract_accepts_need_based_slice_layout() {
let canvas_context = ExternalCanvasGenerationContext { let canvas_context = ExternalCanvasGenerationContext {
project_id: "canvas-project".to_string(), project_id: "canvas-project".to_string(),
asset_folder_id: "asset-folder".to_string(), asset_folder_id: "asset-folder".to_string(),
@@ -8927,7 +8927,7 @@ mod canvas_generation_tests {
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let error = validate_strict_platform_art_spritesheet_contract( validate_strict_platform_art_spritesheet_contract(
&slices, &slices,
&canvas_context, &canvas_context,
Some("canvas-project"), Some("canvas-project"),
@@ -8941,9 +8941,24 @@ mod canvas_generation_tests {
true, true,
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] #[test]
@@ -10253,6 +10268,40 @@ mod canvas_generation_tests {
error.contains("当前生成意图与已持久化请求快照不一致"), error.contains("当前生成意图与已持久化请求快照不一致"),
"{error}" "{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!( assert!(matches!(
listener.accept(), listener.accept(),
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock Err(error) if error.kind() == std::io::ErrorKind::WouldBlock
@@ -10412,6 +10461,40 @@ mod canvas_generation_tests {
"{error}" "{error}"
); );
assert!(error.contains("当前规范图身份"), "{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!( assert!(matches!(
listener.accept(), listener.accept(),
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock 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()), output_path: Some(AGENT_RUNTIME_ART_SPEC_PATH.to_string()),
aspect_ratio: "1:1".to_string(), aspect_ratio: "1:1".to_string(),
image_size: "1K".to_string(), image_size: "1K".to_string(),
asset_kind: "icon-spec".to_string(), asset_kind: "game-art".to_string(),
asset_label: "游戏统一视觉规范图".to_string(), asset_label: "AI 游戏首版美术素材".to_string(),
replace_existing: false, replace_existing: false,
slice_count: None, slice_count: None,
}; };
@@ -11063,19 +11146,18 @@ mod canvas_generation_tests {
} }
#[test] #[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("原创收集玩法"); let descriptions = canonical_art_spritesheet_icon_descriptions("原创收集玩法");
assert_eq!(descriptions.len(), 4); assert_eq!(descriptions.len(), 1);
for (index, description) in descriptions.iter().enumerate() { assert!(descriptions[0].contains("数量、类别、排列和切片方式由本次需求决定"));
assert!(description.starts_with(&format!("{}", index + 1))); assert!(descriptions[0].contains("原创收集玩法"));
}
} }
#[test] #[test]
fn canonical_art_spritesheet_descriptions_obey_external_editor_item_limit() { fn canonical_art_spritesheet_descriptions_obey_external_editor_item_limit() {
let descriptions = canonical_art_spritesheet_icon_descriptions(&"原创玩法需求".repeat(128)); let descriptions = canonical_art_spritesheet_icon_descriptions(&"原创玩法需求".repeat(128));
assert_eq!(descriptions.len(), 4); assert_eq!(descriptions.len(), 1);
assert!(descriptions assert!(descriptions
.iter() .iter()
.all(|description| description.chars().count() <= 200)); .all(|description| description.chars().count() <= 200));
@@ -11466,7 +11548,7 @@ mod canvas_generation_tests {
} }
#[test] #[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 temporary = tempfile::tempdir().expect("create strict slice project");
let root = temporary.path(); let root = temporary.path();
init_local_game_project_at(root, "strict-slices", "严格切片测试") init_local_game_project_at(root, "strict-slices", "严格切片测试")
@@ -11481,9 +11563,9 @@ mod canvas_generation_tests {
&replacement_options(), &replacement_options(),
|_| Ok(()), |_| 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_eq!(fs::read(path).expect("read preserved sheet"), b"old-image");
assert!(!root assert!(!root
.join("assets/art-spritesheet-slices/manifest.json") .join("assets/art-spritesheet-slices/manifest.json")
@@ -1791,29 +1791,27 @@ mod tests {
#[test] #[test]
fn agent_prompt_design_foundation_forbids_implementation_and_browser_validation() { fn agent_prompt_design_foundation_forbids_implementation_and_browser_validation() {
for editor_api_key_is_configured in [false, true] { let without_canvas =
let prompt = game_creator_design_foundation_tool_plan_prompt( game_creator_design_foundation_tool_plan_prompt("shared runtime contract", false);
"shared runtime contract", assert!(without_canvas.contains("必须完成 memory/project.md 与 game/game_design.md"));
editor_api_key_is_configured, 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")); let with_canvas =
assert!(prompt.contains("不得创建、修改、删除或补丁 game/index.html")); game_creator_design_foundation_tool_plan_prompt("shared runtime contract", true);
assert!(prompt.contains("由 Runtime 在收束门内验证本人 owner 产物")); assert!(with_canvas.contains("根据当前玩法需求编写规格和界面建议"));
assert!( assert!(with_canvas.contains("不要使用固定图片合同"));
prompt.contains("不得调用 project.verify、command.run_limited、game.static_smoke") assert!(with_canvas.contains(
); "如需图片,明确说明用途、数量、输出路径、尺寸、参考资源和是否需要 spritesheet"
assert!(prompt.contains("preview.start 或 preview.validate")); ));
assert!(prompt.contains("最终静态验收仍属于 preview-readiness")); assert!(with_canvas.contains("不修改 game/index.html,不启动预览或试玩"));
assert!(prompt.contains("浏览器验收仍属于 preview-playtest"));
assert!(prompt.contains(
"不得通过 command.exec、command.start 或其他工具启动本地预览服务、浏览器、Playwright"
));
}
} }
#[test] #[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 = let without_canvas =
game_creator_design_foundation_tool_plan_prompt("shared runtime contract", false); game_creator_design_foundation_tool_plan_prompt("shared runtime contract", false);
assert!(without_canvas.contains("不调用 canvas.asset_generate")); assert!(without_canvas.contains("不调用 canvas.asset_generate"));
@@ -1823,26 +1821,10 @@ mod tests {
let with_canvas = let with_canvas =
game_creator_design_foundation_tool_plan_prompt("shared runtime contract", true); game_creator_design_foundation_tool_plan_prompt("shared runtime contract", true);
assert!(with_canvas.contains("再调用 canvas.asset_generate")); assert!(with_canvas.contains("再调用 canvas.asset_generate"));
assert!(with_canvas.contains("assets/art-spec.png")); assert!(with_canvas.contains("不要使用固定图片合同"));
assert!(with_canvas.contains("referenceImageSrcs 第一项")); assert!(!with_canvas.contains("assets/art-spec.png"));
assert!(with_canvas.contains("assets/ui-prototype.png")); assert!(!with_canvas.contains("ui-prototype.v2"));
assert!(with_canvas.contains("调用 image.inspect")); assert!(!with_canvas.contains("informationHud"));
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/generationskind=ui-design"));
assert!(with_canvas
.contains("不得误用 POST /api/external/v1/editor/ui-designs/assets/extractions"));
} }
#[test] #[test]
@@ -1856,15 +1838,17 @@ mod tests {
let with_canvas = let with_canvas =
game_creator_art_director_tool_plan_prompt("shared runtime contract", true); game_creator_art_director_tool_plan_prompt("shared runtime contract", true);
assert!(with_canvas.contains("outputPath=assets/art-spec.png")); assert!(with_canvas.contains(
assert!(with_canvas.contains("assetKind=icon-spec")); "根据项目实际需要选择 canvas.asset_generate 的 assetKind、outputPath、尺寸、比例和提示词"
assert!(with_canvas.contains("成功只表示固定候选已生成并登记,不等于视觉门已经通过")); ));
assert!(with_canvas.contains("由 Runtime 在收束时核对当前 revision")); assert!(with_canvas.contains("可以生成一张或多张图片,也可以不生成图片"));
assert!(!with_canvas.contains("成功会为本人当前 revision 形成验证凭证")); assert!(with_canvas.contains("不要假设固定图片名称、数量、素材类别或布局"));
assert!(!with_canvas.contains("outputPath=assets/art-spec.png"));
assert!(!with_canvas.contains("assetKind=icon-spec"));
} }
#[test] #[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 = let without_canvas =
game_creator_art_asset_plan_tool_plan_prompt("shared runtime contract", false); game_creator_art_asset_plan_tool_plan_prompt("shared runtime contract", false);
assert!(without_canvas.contains("不调用 canvas.asset_generate")); assert!(without_canvas.contains("不调用 canvas.asset_generate"));
@@ -1872,21 +1856,15 @@ mod tests {
let with_canvas = let with_canvas =
game_creator_art_asset_plan_tool_plan_prompt("shared runtime contract", true); 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("使用 asset.list 了解已有资源"));
assert!(with_canvas.contains("由 Runtime 形成 iconDescriptions")); assert!(with_canvas.contains("按需调用 canvas.asset_generate"));
assert!(with_canvas.contains("玩家主体及朝向/状态")); assert!(with_canvas.contains("切片布局和尺寸由当前需求决定"));
assert!(with_canvas.contains("不得假设为塔防")); assert!(with_canvas.contains("不得套用固定图片包或固定 2x2"));
assert!(with_canvas.contains("权威 resourceId 作为 referenceId")); assert!(with_canvas.contains("spritesheet 可通过 sliceCount 指定切片数量"));
assert!(with_canvas.contains("POST /api/external/v1/editor/icon-spritesheets/generations")); assert!(with_canvas.contains("也可以生成普通单图或多张独立图片"));
assert!(with_canvas.contains("不得回退普通生图或 UI extraction")); assert!(with_canvas.contains("生成后核对资源登记、透明度、警告和实际使用情况"));
assert!(with_canvas.contains("screenColor=auto")); assert!(!with_canvas.contains("screenColor=auto"));
assert!(with_canvas.contains("缺少规范图时必须等待 art-director")); assert!(!with_canvas.contains("icon-spritesheets/generations"));
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 产物"));
} }
#[test] #[test]
@@ -2012,14 +2012,17 @@ mod tests {
} }
#[test] #[test]
fn autonomous_initial_collaboration_requires_leader_artifacts() { fn autonomous_initial_collaboration_allows_need_based_art_artifacts() {
let mut plan = autonomous_initial_leader_plan(); validate_agent_runtime_autonomous_initial_collaboration_contract(
plan.actions[1] = autonomous_initial_delegate("art-director", &[]); &autonomous_initial_leader_plan(),
)
.expect("empty need-based art artifacts must be accepted");
let error = validate_agent_runtime_autonomous_initial_collaboration_contract(&plan) let mut declared_plan = autonomous_initial_leader_plan();
.expect_err("art artifact must be required"); declared_plan.actions[1] =
autonomous_initial_delegate("art-director", &["assets/art-spec.png"]);
assert!(error.contains("expectedArtifacts 必须包含 assets/art-spec.png")); validate_agent_runtime_autonomous_initial_collaboration_contract(&declared_plan)
.expect("explicitly declared art artifacts must stay accepted");
} }
#[test] #[test]
@@ -1924,7 +1924,7 @@ mod tests {
} }
#[tokio::test] #[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 task = seed_task("art-director");
{ {
let _config_guard = crate::tests::write_test_local_config("{}".to_string()); let _config_guard = crate::tests::write_test_local_config("{}".to_string());
@@ -1950,29 +1950,26 @@ mod tests {
), ),
async { async {
let prompt = render_autonomous_manifest_ready_task_background_prompt(&task); let prompt = render_autonomous_manifest_ready_task_background_prompt(&task);
assert!(prompt.contains("非只读视觉规范生成任务")); assert!(prompt.contains("无生图凭据只读协调任务"));
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("生成并登记 assets/art-spec.png"));
assert!(prompt.contains("会同时提交当前 run 的 mutation 与验证凭证"));
assert!(prompt.contains("Runtime 会在子 Run 终态后幂等投影 manifest")); 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" "art-director"
)); ));
assert!(!agent_runtime_task_requires_read_only_delivery( assert!(agent_runtime_task_requires_read_only_delivery(
"art-director", "art-director",
&prompt &prompt
)); ));
let art_asset_prompt = render_autonomous_manifest_ready_task_background_prompt( let art_asset_prompt = render_autonomous_manifest_ready_task_background_prompt(
&seed_task("art-asset-plan"), &seed_task("art-asset-plan"),
); );
assert!(art_asset_prompt.contains("canvas.asset_generate")); assert!(art_asset_prompt.contains(
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("需要图集时用 sliceCount 指定切片数量"));
assert!(art_asset_prompt.contains("把结构化计划最后一步标记 completed")); assert!(art_asset_prompt
.contains("Runtime 只核对实际声明的资源登记,不要求固定图片合同"));
assert!(!art_asset_prompt.contains("按现有 visual gate 生成、登记并验收")); assert!(!art_asset_prompt.contains("按现有 visual gate 生成、登记并验收"));
}, },
) )
@@ -1645,7 +1645,7 @@ mod tests {
#[test] #[test]
fn npm_preview_requires_build_and_prefers_bundled_assets() { 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(); fs::create_dir_all(&base).unwrap();
let root = tempfile::tempdir_in(base).unwrap(); let root = tempfile::tempdir_in(base).unwrap();
fs::write(root.path().join("package.json"), "{}").unwrap(); fs::write(root.path().join("package.json"), "{}").unwrap();
@@ -314,7 +314,7 @@ mod npm_export_tests {
#[test] #[test]
fn npm_package_contains_only_dist_and_publish_readme() { 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(); fs::create_dir_all(&base).unwrap();
let root = tempfile::tempdir_in(base).unwrap(); let root = tempfile::tempdir_in(base).unwrap();
for directory in ["dist/assets", "assets", "exports", "node_modules", "game"] { for directory in ["dist/assets", "assets", "exports", "node_modules", "game"] {
@@ -896,7 +896,7 @@ mod npm_build_tests {
#[test] #[test]
fn built_smoke_checks_module_files_without_inline_canvas() { 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(); fs::create_dir_all(&base).unwrap();
let root = tempfile::tempdir_in(base).unwrap(); let root = tempfile::tempdir_in(base).unwrap();
fs::create_dir_all(root.path().join("dist/assets")).unwrap(); fs::create_dir_all(root.path().join("dist/assets")).unwrap();
@@ -1132,91 +1132,8 @@ async fn background_agent_runtime_delegate_respects_project_policy() {
fs::remove_dir_all(root).ok(); 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] #[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( let _platform_session = crate::platform_session::install_test_platform_session(
"visual-semantic-gate-test-user", "visual-semantic-gate-test-user",
"visual-semantic-gate-test-key", "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") .expect("read UI prototype revision")
.revision; .revision;
let uninspected = finish_game_creator_agent_background_runtime_turn_at( let outcome = 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(
&root, &root,
state, state,
"当前图片已通过全部八项 UI 原型检查。", "不再要求固定 UI 原型检查凭证",
revision, revision,
&[], &[],
) )
.expect("current passed UI proof allows finalization"); .expect("need-based design finalization");
assert!(matches!( assert!(
completed, matches!(outcome, AgentBackgroundFinalizationOutcome::Completed(_)),
AgentBackgroundFinalizationOutcome::Completed(_) "{outcome:?}"
)); );
fs::remove_dir_all(root).ok(); 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!({ &serde_json::json!({
"prompt": "生成原创晶体与潮汐构装体图集", "prompt": "生成原创晶体与潮汐构装体图集",
"outputPath": "assets/art-spritesheet.png",
"assetKind": "art-spritesheet",
"assetLabel": "游戏首版核心美术素材",
"aspectRatio": "1:1",
"imageSize": "1K",
"sliceCount": 4,
"replaceExisting": true "replaceExisting": true
}), }),
), ),
@@ -2181,7 +2014,7 @@ fn gui_and_cli_autonomous_game_build_runs_still_allow_publish_delegates() {
} }
#[test] #[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( let _platform_session = crate::platform_session::install_test_platform_session(
"visual-delegation-contract-user", "visual-delegation-contract-user",
"visual-delegation-contract-key", "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"); .expect("start supervisor parent runtime");
for (agent_id, required_path) in [ for (agent_id, output_path) in [
("design-foundation", "assets/ui-prototype.png"), ("design-foundation", "assets/ui-prototype.png"),
("art-asset-plan", "assets/art-spritesheet.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( let observation = observe_agent_runtime_agent_delegate(
&root, &root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
@@ -2223,17 +2056,18 @@ fn visual_specialist_delegations_require_image_artifacts_but_read_only_work_allo
"runId": null "runId": null
}), }),
); );
assert_eq!(observation.status, "failed"); assert_eq!(observation.status, "ok", "{observation:?}");
assert!(observation.summary.contains(required_path)); assert!(!observation.summary.contains(output_path));
let delegation_id = agent_runtime_delegation_id( let delegation_id = agent_runtime_delegation_id(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id, parent_run_id,
agent_id, agent_id,
&action_id, &action_id,
); );
assert!(read_static_delegate_delivery_at(&root, &delegation_id) let delivery = read_static_delegate_delivery_at(&root, &delegation_id)
.expect("read rejected visual delivery") .expect("read need-based visual delivery")
.is_none()); .expect("need-based visual delivery exists");
assert!(delivery.expected_artifacts.is_empty());
} }
let target_agent_id = "quality-review"; let target_agent_id = "quality-review";
@@ -653,7 +653,7 @@ fn parallel_read_batch_finishes_before_concurrent_steer_is_accepted() {
#[tokio::test] #[tokio::test]
async fn provider_action_batch_verification_then_parallel_reads_share_current_gate_snapshot() { async fn provider_action_batch_verification_then_parallel_reads_share_current_gate_snapshot() {
let root = unique_project_path(); let root = unique_project_path();
init_local_game_project_at( init_legacy_single_html_project_at(
&root, &root,
"project-provider-batch-verification-parallel-read", "project-provider-batch-verification-parallel-read",
"Provider 批次验证后并行读取测试", "Provider 批次验证后并行读取测试",
@@ -944,33 +944,12 @@ fn visual_specialist_prompts_require_real_registered_image_deliveries() {
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
); );
for expected in [ for expected in [
"文本策划只是中间结果", "根据当前玩法需求编写规格和界面建议",
"canvas.asset_generate", "明确说明用途、数量、输出路径、尺寸、参考资源和是否需要 spritesheet",
"16:9", "再调用 canvas.asset_generate",
"2K", "不要使用固定图片合同",
"assets/ui-prototype.png", "不创建固定图片槽位,不规定固定数量或布局",
"assetKind=ui-prototype", "不修改 game/index.html,不启动预览或试玩",
"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/generationskind=ui-design",
"不得误用 POST /api/external/v1/editor/ui-designs/assets/extractions",
"canvas.asset_generate 成功只表示候选图片已生成并登记,不等于视觉验收完成",
"由 Runtime 在收束门内同时核对固定 owner 文档",
"已有同路径画布资产时先核对登记",
"检查已通过时不得重复生成或再次扣费",
"纯场景图",
"不得把计划写完当成 completed",
] { ] {
assert!( assert!(
design_prompt.contains(expected), design_prompt.contains(expected),
@@ -983,14 +962,10 @@ fn visual_specialist_prompts_require_real_registered_image_deliveries() {
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
); );
for expected in [ for expected in [
"assets/art-spec.png", "根据项目实际需要选择 canvas.asset_generate 的 assetKind、outputPath、尺寸、比例和提示词",
"assetKind=icon-spec", "可以生成一张或多张图片,也可以不生成图片",
"POST /api/external/v1/editor/images/generationskind=spec", "需要参考图时使用已登记资源 ID",
"后续 UI 和透明图集共同引用", "不要假设固定图片名称、数量、素材类别或布局",
"不得用 generationInputs.artSpec JSON",
"成功只表示固定候选已生成并登记,不等于视觉门已经通过",
"由 Runtime 在收束时核对当前 revision",
"缺少 resourceId 时不得提交最终回复",
] { ] {
assert!( assert!(
director_prompt.contains(expected), director_prompt.contains(expected),
@@ -1003,28 +978,12 @@ fn visual_specialist_prompts_require_real_registered_image_deliveries() {
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
); );
for expected in [ for expected in [
"资产清单和美术计划只是中间结果", "使用 asset.list 了解已有资源",
"canvas.asset_generate", "按需调用 canvas.asset_generate",
"assets/manifest.art.json", "切片布局和尺寸由当前需求决定",
"assets/art-spritesheet.png", "不得套用固定图片包或固定 2x2",
"固定使用 1:1、1K", "spritesheet 可通过 sliceCount 指定切片数量",
"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",
"透明证据不足时不得提交最终回复",
] { ] {
assert!( assert!(
art_prompt.contains(expected), art_prompt.contains(expected),
@@ -2330,7 +2330,8 @@ async fn executed_project_verification_audit_failure_requires_reconciliation() {
#[test] #[test]
fn project_verification_gate_accepts_only_exact_game_static_smoke() { fn project_verification_gate_accepts_only_exact_game_static_smoke() {
let root = unique_project_path(); 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"); advance_project_revision_for_test(&root, "playtest", "playtest-run", "file.write");
fs::write( fs::write(
root.join("game/index.html"), root.join("game/index.html"),
@@ -2536,7 +2537,7 @@ fn agent_runtime_project_verify_summary_hashes_command_without_head_or_tail() {
#[test] #[test]
fn limited_local_command_runs_static_game_smoke_and_writes_log() { fn limited_local_command_runs_static_game_smoke_and_writes_log() {
let root = unique_project_path(); 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( fs::write(
root.join("game/index.html"), root.join("game/index.html"),
fake_llm_game_draft().game_html, fake_llm_game_draft().game_html,
@@ -2866,7 +2867,7 @@ async fn project_verification_runs_without_prepost_and_records_failure_and_timeo
#[test] #[test]
fn limited_local_command_appends_playtest_to_existing_trace() { fn limited_local_command_appends_playtest_to_existing_trace() {
let root = unique_project_path(); 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( fs::write(
root.join("game/index.html"), root.join("game/index.html"),
fake_llm_game_draft().game_html, fake_llm_game_draft().game_html,
@@ -2915,7 +2916,7 @@ fn limited_local_command_appends_playtest_to_existing_trace() {
#[test] #[test]
fn limited_local_command_rejects_placeholder_game_smoke() { fn limited_local_command_rejects_placeholder_game_smoke() {
let root = unique_project_path(); 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") let error = run_limited_local_command_at(&root, "game.static_smoke")
.expect_err("placeholder game should fail smoke"); .expect_err("placeholder game should fail smoke");
@@ -2927,7 +2928,7 @@ fn limited_local_command_rejects_placeholder_game_smoke() {
#[test] #[test]
fn limited_local_command_rejects_forbidden_runtime_apis() { fn limited_local_command_rejects_forbidden_runtime_apis() {
let root = unique_project_path(); 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() let html = fake_llm_game_draft()
.game_html .game_html
.replace("const marker =", "fetch('/secret');\n const marker ="); .replace("const marker =", "fetch('/secret');\n const marker =");
@@ -2943,7 +2944,7 @@ fn limited_local_command_rejects_forbidden_runtime_apis() {
#[test] #[test]
fn limited_local_command_rejects_blank_canvas_game_smoke() { fn limited_local_command_rejects_blank_canvas_game_smoke() {
let root = unique_project_path(); 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#"<!doctype html> let html = r#"<!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<body> <body>
@@ -2970,7 +2971,8 @@ fn limited_local_command_rejects_blank_canvas_game_smoke() {
#[test] #[test]
fn limited_local_command_rejects_invalid_javascript_before_surface_checks() { fn limited_local_command_rejects_invalid_javascript_before_surface_checks() {
let root = unique_project_path(); 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#"<!doctype html> let html = r#"<!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<body> <body>
@@ -2994,7 +2996,8 @@ fn limited_local_command_rejects_invalid_javascript_before_surface_checks() {
#[test] #[test]
fn limited_local_command_rejects_token_rich_truncated_script() { fn limited_local_command_rejects_token_rich_truncated_script() {
let root = unique_project_path(); 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#"<!doctype html> let html = r#"<!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<body> <body>
@@ -1944,7 +1944,7 @@ fn validate_llm_game_draft_accepts_win_condition_as_goal() {
#[tokio::test] #[tokio::test]
async fn agent_run_resume_restarts_generation_from_latest_goal() { async fn agent_run_resume_restarts_generation_from_latest_goal() {
let root = unique_project_path(); 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( write_agent_run_trace(
&root, &root,
"run-control-resume", "run-control-resume",
@@ -314,6 +314,37 @@ fn unique_project_path() -> PathBuf {
)) ))
} }
/// npm + Vite 脚手架出现前的默认单文件入口,占位文案仍内联在 HTML 里。
const LEGACY_GAME_INDEX_HTML: &str = r#"<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Genarrative Game Draft</title>
<style>
body { margin: 0; display: grid; min-height: 100vh; place-items: center; background: #101827; color: #d9e7ff; font: 16px system-ui, sans-serif; }
main { width: min(720px, calc(100vw - 32px)); }
</style>
</head>
<body><main>还没有生成游戏。回到聊天输入创意并确认生成后,这里会写入可试玩原型。</main></body>
</html>
"#;
/// 旧版单文件 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<InitLocalProjectResult, String> {
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( pub(crate) fn freeze_test_root_goal_contract_at(
root: &Path, root: &Path,
run_id: &str, run_id: &str,
@@ -550,7 +550,7 @@ fn canonical_visual_completion_requires_persisted_route_kind_and_current_spec_re
#[tokio::test] #[tokio::test]
async fn background_agent_runtime_can_start_local_preview() { async fn background_agent_runtime_can_start_local_preview() {
let root = unique_project_path(); 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( fs::write(
root.join("game/index.html"), root.join("game/index.html"),
fake_llm_game_draft().game_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] #[tokio::test]
async fn generate_local_game_draft_fails_after_max_passes_without_final_artifacts() { async fn generate_local_game_draft_fails_after_max_passes_without_final_artifacts() {
let root = unique_project_path(); 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(); let mut invalid_draft = fake_llm_game_draft();
invalid_draft.game_html = r#"<!doctype html> invalid_draft.game_html = r#"<!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
@@ -2590,7 +2592,7 @@ fn local_project_file_commands_read_write_list_and_delete_text_files() {
#[test] #[test]
fn local_project_export_package_uses_runtime_whitelist_and_records() { fn local_project_export_package_uses_runtime_whitelist_and_records() {
let root = unique_project_path(); 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) write_local_project_file_at(&root, "game/index.html", &fake_llm_game_draft().game_html)
.expect("write playable html"); .expect("write playable html");
write_local_project_file_at(&root, "assets/hero.txt", "hero asset").expect("write asset"); 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] #[test]
fn local_project_export_package_rejects_symlink_assets() { fn local_project_export_package_rejects_symlink_assets() {
let root = unique_project_path(); 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) write_local_project_file_at(&root, "game/index.html", &fake_llm_game_draft().game_html)
.expect("write playable html"); .expect("write playable html");
write_local_project_file_at(&root, "exports/README.md", "playtest notes") write_local_project_file_at(&root, "exports/README.md", "playtest notes")
@@ -2731,7 +2733,7 @@ fn local_project_export_package_rejects_symlink_assets() {
#[test] #[test]
fn local_project_export_package_rejects_symlink_runtime_dirs_and_readme() { fn local_project_export_package_rejects_symlink_runtime_dirs_and_readme() {
let root = unique_project_path(); 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) write_local_project_file_at(&root, "game/index.html", &fake_llm_game_draft().game_html)
.expect("write playable html"); .expect("write playable html");
write_local_project_file_at(&root, "memory/project.md", "private memory") 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] #[test]
fn local_project_export_package_requires_playable_html_and_readme() { fn local_project_export_package_requires_playable_html_and_readme() {
let root = unique_project_path(); 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 = let missing_readme =
export_local_project_package_at(&root).expect_err("default html is not playable"); export_local_project_package_at(&root).expect_err("default html is not playable");
assert!(missing_readme.contains("游戏入口必须包含可渲染画布")); assert!(missing_readme.contains("游戏入口必须包含可渲染画布"));
@@ -3640,7 +3642,7 @@ fn cli_agent_resume_without_appdata_fails_before_runtime_dispatch() {
#[test] #[test]
fn local_preview_server_serves_game_index() { fn local_preview_server_serves_game_index() {
let root = unique_project_path(); 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"); fs::write(root.join("assets/player.png"), b"PNGDATA").expect("asset");
let (preview, stop) = start_local_game_preview_for_project(&root).expect("preview start"); 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] #[test]
fn local_preview_server_drains_split_browser_headers_before_response() { fn local_preview_server_drains_split_browser_headers_before_response() {
let root = unique_project_path(); 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 (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"); 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] #[test]
fn local_preview_head_preserves_asset_content_length() { fn local_preview_head_preserves_asset_content_length() {
let root = unique_project_path(); 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"); fs::write(root.join("assets/player.png"), b"PNGDATA").expect("asset");
let response = build_preview_response(&root, "HEAD", "/assets/player.png"); 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] #[test]
fn local_preview_start_rejects_a_stale_validated_revision_atomically() { fn local_preview_start_rejects_a_stale_validated_revision_atomically() {
let root = unique_project_path(); 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"); .expect("project init");
let revision = advance_project_revision_for_test( let revision = advance_project_revision_for_test(
&root, &root,
@@ -4768,7 +4770,8 @@ fn local_preview_start_rejects_a_stale_validated_revision_atomically() {
#[test] #[test]
fn stale_preview_cleanup_does_not_stop_a_newer_matching_project_server() { fn stale_preview_cleanup_does_not_stop_a_newer_matching_project_server() {
let root = unique_project_path(); 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 registry = PreviewRegistry::default();
let (first, first_stop) = let (first, first_stop) =
start_local_game_preview_for_project(&root).expect("first preview start"); 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] #[test]
fn stale_preview_cleanup_cannot_be_blocked_by_project_stop_policy() { fn stale_preview_cleanup_cannot_be_blocked_by_project_stop_policy() {
let root = unique_project_path(); 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( write_project_permission_policy_at(
&root, &root,
ProjectPermissionPolicy { ProjectPermissionPolicy {
@@ -4826,6 +4830,7 @@ fn stale_preview_cleanup_cannot_be_blocked_by_project_stop_policy() {
#[test] #[test]
fn local_preview_serves_generated_playable_game() { fn local_preview_serves_generated_playable_game() {
let root = unique_project_path(); 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()) write_local_game_draft_at(&root, "像素风横版动作", &fake_llm_game_draft())
.expect("draft should generate"); .expect("draft should generate");
@@ -98,6 +98,7 @@ async fn request_llm_game_draft_uses_openai_compatible_provider_output() {
#[tokio::test] #[tokio::test]
async fn generate_local_game_draft_sends_asset_context_to_llm() { async fn generate_local_game_draft_sends_asset_context_to_llm() {
let root = unique_project_path(); 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") let uploaded = upload_local_asset_at(&root, "../角色.png", "image/png", b"fake-png")
.expect("asset upload"); .expect("asset upload");
append_local_conversation_message_at( append_local_conversation_message_at(
@@ -1344,7 +1345,7 @@ async fn provider_action_batch_agent_db_failure_does_not_advance_cursor() {
#[tokio::test] #[tokio::test]
async fn provider_action_batch_mutation_then_verification_rolls_forward_gate() { async fn provider_action_batch_mutation_then_verification_rolls_forward_gate() {
let root = unique_project_path(); let root = unique_project_path();
init_local_game_project_at( init_legacy_single_html_project_at(
&root, &root,
"project-provider-batch-mutation-verification", "project-provider-batch-mutation-verification",
"Provider 批次修改后验证测试", "Provider 批次修改后验证测试",
@@ -5864,7 +5865,7 @@ async fn background_agent_runtime_cancellation_wins_over_inflight_llm_error() {
#[tokio::test] #[tokio::test]
async fn agent_loop_uses_per_agent_llm_overrides() { async fn agent_loop_uses_per_agent_llm_overrides() {
let root = unique_project_path(); 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"); .expect("init project");
let (planner_sender, planner_receiver) = mpsc::channel(); let (planner_sender, planner_receiver) = mpsc::channel();
let planner_base_url = spawn_mock_llm_server_responses_with_capture( 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] #[tokio::test]
async fn agent_loop_writes_spec_findings_and_retries_generator() { async fn agent_loop_writes_spec_findings_and_retries_generator() {
let root = unique_project_path(); 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(); let mut first_draft = fake_llm_game_draft();
first_draft.game_html = r#"<!doctype html> first_draft.game_html = r#"<!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
@@ -981,7 +981,7 @@ async fn provider_retry_final_reply_thinking_only_response_retries_before_handof
} }
#[test] #[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( let _platform_session = crate::platform_session::install_test_platform_session(
"visual-finalization-test-user", "visual-finalization-test-user",
"visual-finalization-test-key", "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( let _config_guard = crate::tests::write_test_local_config(
r#"{"editorApi":{"apiKey":"visual-finalization-test-key"}}"#.to_string(), r#"{"editorApi":{"apiKey":"visual-finalization-test-key"}}"#.to_string(),
); );
for (agent_id, local_path, kind) in [ for agent_id in ["design-foundation", "art-asset-plan"] {
(
"design-foundation",
"assets/ui-prototype.png",
"ui-prototype",
),
(
"art-asset-plan",
"assets/art-spritesheet.png",
"art-spritesheet",
),
] {
let root = unique_project_path(); let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "视觉 Runtime 完成门禁测试") init_local_game_project_at(&root, "project-1", "视觉 Runtime 完成门禁测试")
.expect("project init"); .expect("project init");
register_canvas_visual_asset_fixture(&root, "assets/art-spec.png", "icon-spec");
let run_id = format!("visual-finalization-{agent_id}"); let run_id = format!("visual-finalization-{agent_id}");
let state = start_game_creator_agent_runtime_task_at( let state = start_game_creator_agent_runtime_task_at(
&root, &root,
@@ -1021,134 +1009,18 @@ fn visual_specialist_finalization_requires_existing_registered_canvas_image() {
.expect("read visual revision") .expect("read visual revision")
.revision; .revision;
let missing_file = finish_game_creator_agent_background_runtime_turn_at( let outcome = 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(
&root, &root,
state, state,
"真实图片已经生成并登记", "不再要求固定视觉资产",
revision, revision,
&[], &[],
) )
.expect("registered image allows finalization"); .expect("need-based visual finalization");
assert!(matches!( assert!(
completed, matches!(outcome, AgentBackgroundFinalizationOutcome::Completed(_)),
AgentBackgroundFinalizationOutcome::Completed(_) "{outcome:?}"
)); );
fs::remove_dir_all(root).ok(); fs::remove_dir_all(root).ok();
} }
@@ -539,7 +539,7 @@ async fn background_agent_runtime_can_read_other_agent_status() {
#[tokio::test] #[tokio::test]
async fn background_agent_runtime_can_run_limited_static_smoke() { async fn background_agent_runtime_can_run_limited_static_smoke() {
let root = unique_project_path(); 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; let playable_game_html = fake_llm_game_draft().game_html;
write_project_permission_policy_at( write_project_permission_policy_at(
&root, &root,
@@ -1550,9 +1550,9 @@ async fn background_agent_runtime_deletes_file_then_verifies_before_completion()
} }
#[tokio::test] #[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(); 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"); register_canvas_visual_asset_fixture(&root, "assets/art-spritesheet.png", "art-spritesheet");
write_project_permission_policy_at( write_project_permission_policy_at(
&root, &root,
@@ -1566,27 +1566,26 @@ async fn art_asset_plan_cannot_delete_registered_fixed_visual_asset() {
let action = AgentRuntimeToolAction { let action = AgentRuntimeToolAction {
tool: "file.delete".to_string(), tool: "file.delete".to_string(),
reason: Some("尝试删除已登记的固定美术素材".to_string()), reason: Some("按当前需求替换旧画布素材".to_string()),
input: serde_json::json!({ "path": "assets/art-spritesheet.png" }), input: serde_json::json!({ "path": "assets/art-spritesheet.png" }),
}; };
let observation = execute_game_creator_agent_runtime_tool_action_with_action_id( let observation = execute_game_creator_agent_runtime_tool_action_with_action_id(
&root, &root,
"art-asset-plan", "art-asset-plan",
"art-delete-protection-run", "art-delete-need-based-run",
"已登记的固定美术素材必须复用", "按当前需求替换画布素材",
&action, &action,
Some("art-delete-protection-action"), Some("art-delete-need-based-action"),
) )
.await; .await;
assert_eq!(observation.status, "blocked"); assert_eq!(observation.status, "ok", "{observation:?}");
assert!(observation.summary.contains("禁止删除固定正式产物")); assert!(!root.join("assets/art-spritesheet.png").exists());
assert!(root.join("assets/art-spritesheet.png").is_file());
assert_eq!( assert_eq!(
read_game_creator_agent_runtime_project_revision(&root) read_game_creator_agent_runtime_project_revision(&root)
.expect("read revision") .expect("read revision")
.revision, .revision,
0 1
); );
fs::remove_dir_all(root).ok(); fs::remove_dir_all(root).ok();
@@ -13,8 +13,8 @@ pub(super) use super::super::{
advance_project_revision_for_test, agent_runtime_verification_plan, advance_project_revision_for_test, agent_runtime_verification_plan,
append_auto_tool_action_audit_pair_for_test, assert_auto_tool_action_audit_pair, append_auto_tool_action_audit_pair_for_test, assert_auto_tool_action_audit_pair,
assert_pending_runtime_decision_revalidates_after_lock, assert_task_status, assert_pending_runtime_decision_revalidates_after_lock, assert_task_status,
fake_llm_game_draft, final_tool_plan_response, mock_http_request_json, fake_llm_game_draft, final_tool_plan_response, init_legacy_single_html_project_at,
native_agent_tool_plan_chat_response, pending_tool_action_for_test, 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, persist_needs_reconciliation_runtime_for_test, persist_project_verification_for_test,
read_agent_db_records_for_test, register_canvas_visual_asset_fixture, read_agent_db_records_for_test, register_canvas_visual_asset_fixture,
spawn_barrier_mock_llm_server, spawn_interruptible_mock_llm_server_with_capture, spawn_barrier_mock_llm_server, spawn_interruptible_mock_llm_server_with_capture,
@@ -3990,7 +3990,7 @@ async fn background_task_recovers_when_assistant_message_cannot_persist() {
} }
#[tokio::test] #[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( let _platform_session = crate::platform_session::install_test_platform_session(
"visual-task-update-test-user", "visual-task-update-test-user",
"visual-task-update-test-key", "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( let _config_guard = crate::tests::write_test_local_config(
r#"{"editorApi":{"apiKey":"visual-task-update-test-key"}}"#.to_string(), r#"{"editorApi":{"apiKey":"visual-task-update-test-key"}}"#.to_string(),
); );
for (task_id, local_path, kind, missing_summary) in [ for task_id in ["art-director", "design-foundation", "art-asset-plan"] {
(
"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",
"首版美术素材图尚未按正式视觉流程生成并登记",
),
] {
let root = unique_project_path(); let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "视觉任务完成门禁测试") init_local_game_project_at(&root, "project-1", "视觉任务完成门禁测试")
.expect("project init"); .expect("project init");
@@ -4040,71 +4021,16 @@ async fn task_update_requires_registered_visual_asset_before_completion() {
"status": "completed" "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, &root,
task_id, task_id,
&run_id, &run_id,
"完成视觉任务", "完成视觉任务",
&action, &action,
Some("visual-task-update-missing"), Some("visual-task-update-need-based"),
) )
.await; .await;
assert_eq!(missing.status, "failed"); assert_eq!(observation.status, "ok", "{observation:?}");
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:?}");
let manifest = read_manifest_for_project(&root).expect("manifest after visual completion"); let manifest = read_manifest_for_project(&root).expect("manifest after visual completion");
assert_eq!( assert_eq!(
manifest manifest
@@ -675,6 +675,8 @@ async fn background_agent_runtime_file_and_memory_writes_respect_project_policy(
#[test] #[test]
fn generate_local_game_draft_writes_memory_design_and_game() { fn generate_local_game_draft_writes_memory_design_and_game() {
let root = unique_project_path(); let root = unique_project_path();
init_legacy_single_html_project_at(&root, "project-1", "记忆与设计产物项目")
.expect("project init");
let draft = fake_llm_game_draft(); let draft = fake_llm_game_draft();
let result = write_local_game_draft_at(&root, "像素风横版动作 </script><script>", &draft) let result = write_local_game_draft_at(&root, "像素风横版动作 </script><script>", &draft)
.expect("draft should generate"); .expect("draft should generate");
@@ -761,6 +763,7 @@ fn generate_local_game_draft_writes_memory_design_and_game() {
#[test] #[test]
fn generate_local_game_draft_appends_short_and_long_memory() { fn generate_local_game_draft_appends_short_and_long_memory() {
let root = unique_project_path(); let root = unique_project_path();
init_legacy_single_html_project_at(&root, "project-1", "短长记忆项目").expect("project init");
let draft = fake_llm_game_draft(); let draft = fake_llm_game_draft();
write_local_game_draft_at(&root, "第一版横版动作", &draft).expect("first draft"); write_local_game_draft_at(&root, "第一版横版动作", &draft).expect("first draft");
@@ -4991,3 +4991,10 @@
- 外层 `start-tauri-dev.mjs` 应在启动 Tauri 前完成配套开发服务准备,并统一收束自有服务进程树;不要让冷编译和数据库发布挤占 Tauri 的前端就绪等待。自动发布必须保留数据库,不能靠清库解决启动问题。 - 外层 `start-tauri-dev.mjs` 应在启动 Tauri 前完成配套开发服务准备,并统一收束自有服务进程树;不要让冷编译和数据库发布挤占 Tauri 的前端就绪等待。自动发布必须保留数据库,不能靠清库解决启动问题。
- CLI 与 standalone 可能是两个独立软链接。必须同时核对 `spacetime --version``spacetimedb-standalone --version`,不能把 CLI 的版本记录当作宿主版本证明;PATH 中存在宿主时启动器检查两者一致。更换宿主前停机备份数据,按原目录启动,不通过清库处理版本错配。 - CLI 与 standalone 可能是两个独立软链接。必须同时核对 `spacetime --version``spacetimedb-standalone --version`,不能把 CLI 的版本记录当作宿主版本证明;PATH 中存在宿主时启动器检查两者一致。更换宿主前停机备份数据,按原目录启动,不通过清库处理版本错配。
- Router 配置缺失不应只在首次请求时报错。API/All 启动必须先校验官方地址、固定模型、provisioning secret、管理员 Token 和凭据加密密钥;否则服务看似 healthy,但登录后的 provisioning/模型调用才延迟失败。 - Router 配置缺失不应只在首次请求时报错。API/All 启动必须先校验官方地址、固定模型、provisioning secret、管理员 Token 和凭据加密密钥;否则服务看似 healthy,但登录后的 provisioning/模型调用才延迟失败。
## AGC 产物迁移与固定视觉门禁退役后旧测试未同步(2026-09-09)
- **现象**master 的 `Native shell tests``1498247e9` 起连续失败,失败数从 31 增到 52;`Backend tests` 偶发 `crates.io` 拉包 `CONNECT tunnel failed, response 502`
- **原因**`1498247e9`AGC 产物迁移 npm + Phaser 4)让新项目默认播种 `game/package.json``game/dist`,但测试仍按 `init_local_game_project_at` + `game/index.html` 单文件布局写夹具,预览、静态检查和导出因此全部卡在缺失的 `game/dist``1f904d28e`#274 MCP 能力暴露)又退役了固定视觉资产完成门禁、固定图片合同和 grid-2x2 切片合同,旧断言仍在验证已经删除的行为。
- **处理**:单文件 HTML 场景改用 `tests::init_legacy_single_html_project_at`(先写 `game/index.html` 再初始化,避免播种 npm 脚手架);固定视觉门禁用例改为断言当前“按项目需求决定图片数量、路径和切片”的行为;验证 accepted 生成不匹配探测的用例改为接受一次 GET 探测;测试临时目录改用 `std::env::temp_dir()` 而不是 `HOME`Windows 本地也能跑。
- **验证**`cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --test-threads=1 <改动用例>` 76 条通过,`node scripts/check-encoding.mjs``cargo fmt --check``git diff --check` 通过;cargo 拉包 502 是 runner 代理的瞬时上游故障,与本次测试修复无关,重跑即可确认。