diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs b/apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs index 37c757da8..fbd020f82 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs @@ -1608,6 +1608,8 @@ pub(crate) fn read_manifest(path: &Path) -> Result Result Result<(), String> { + let supported = shared_contracts::game_creation_app::GAME_CREATION_APP_MANIFEST_SCHEMA_VERSION; + if schema_version != supported { + return Err(format!( + "manifest schemaVersion 不受支持:{schema_version}(当前支持 {supported})" + )); + } + Ok(()) +} + fn install_manifest_temp_with( path: &Path, temp_path: &Path, @@ -1721,6 +1749,8 @@ fn write_manifest_locked( manifest: &GameCreationAppManifest, allowed_version_removals: &dyn Fn(&GameCreationAppManifest) -> Vec, ) -> Result<(), String> { + validate_manifest_schema_version(&manifest.schema_version) + .map_err(|error| format!("校验 manifest schema 版本失败:{error}"))?; validate_game_iteration_versions(&manifest.versions) .map_err(|error| format!("校验 manifest 项目版本失败:{error}"))?; let payload = serde_json::to_string_pretty(manifest) diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/manifest/import_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/project/manifest/import_tests.rs index b226c4158..5a1ef2390 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/manifest/import_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/manifest/import_tests.rs @@ -396,3 +396,77 @@ fn rejects_non_godot_directory_without_writing_agent_metadata() { assert!(!root.join(".agent").exists()); fs::remove_dir_all(root).ok(); } + +fn write_raw_manifest_fixture(workspace: &Path, payload: &serde_json::Value) -> (PathBuf, String) { + let manifest_path = workspace.join(".agent/manifest.json"); + fs::create_dir_all(manifest_path.parent().expect("manifest parent")) + .expect("create manifest parent"); + let fixture = format!( + "{}\n", + serde_json::to_string_pretty(payload).expect("serialize manifest fixture") + ); + fs::write(&manifest_path, &fixture).expect("write manifest fixture"); + (manifest_path, fixture) +} + +/// 读侧必须明确接受当前版本:只有「拒绝未知版本」的负向断言挡不住「无条件拒绝」这种改法。 +#[test] +fn manifest_read_accepts_the_current_schema_version() { + let workspace = godot_import_test_path("current-manifest-schema"); + let mut manifest = new_game_creation_app_manifest("schema-project", "Schema"); + manifest.goal = Some("做一个像素动作原型".to_string()); + manifest.godot_project_root = Some("game-source".to_string()); + assert_eq!( + manifest.schema_version, + shared_contracts::game_creation_app::GAME_CREATION_APP_MANIFEST_SCHEMA_VERSION + ); + let manifest_path = workspace.join(".agent/manifest.json"); + write_manifest(&manifest_path, &manifest).expect("write current manifest"); + + let read = read_manifest(&manifest_path).expect("current schemaVersion must be accepted"); + + // 全字段回读相等,同时证明 `deny_unknown_fields` 没有把已知字段/可选字段一起拒掉。 + assert_eq!(read, manifest); + assert_eq!(read.project_id, "schema-project"); + assert_eq!(read.godot_project_root.as_deref(), Some("game-source")); + fs::remove_dir_all(workspace).ok(); +} + +/// 读到未知 `schemaVersion` 必须失败关闭,而且**只读失败**:不能在失败路径上顺手把 +/// 「新版本文件」按本客户端的结构重写一遍。 +#[test] +fn manifest_read_rejects_an_unsupported_schema_version_without_rewriting_the_file() { + let workspace = godot_import_test_path("unsupported-manifest-schema"); + let mut payload = + serde_json::to_value(new_game_creation_app_manifest("schema-project", "Schema")) + .expect("serialize manifest fixture"); + payload["schemaVersion"] = serde_json::json!("game-creation-app.manifest.v2"); + let (manifest_path, fixture) = write_raw_manifest_fixture(&workspace, &payload); + + let error = read_manifest(&manifest_path) + .expect_err("an unsupported manifest schemaVersion must fail closed"); + + assert!(error.contains("schemaVersion"), "unexpected error: {error}"); + assert!(error.contains("game-creation-app.manifest.v2"), "{error}"); + assert_eq!( + fs::read_to_string(&manifest_path).expect("re-read manifest fixture"), + fixture + ); + fs::remove_dir_all(workspace).ok(); +} + +/// 写侧同一口径:本客户端永远不许把未知 `schemaVersion` 写进项目,且失败发生在落盘之前。 +#[test] +fn manifest_write_rejects_an_unsupported_schema_version_before_touching_the_file() { + let workspace = godot_import_test_path("unsupported-manifest-write-schema"); + let manifest_path = workspace.join(".agent/manifest.json"); + let mut manifest = new_game_creation_app_manifest("schema-project", "Schema"); + manifest.schema_version = "game-creation-app.manifest.v2".to_string(); + + let error = write_manifest(&manifest_path, &manifest) + .expect_err("writing an unsupported manifest schemaVersion must fail closed"); + + assert!(error.contains("schemaVersion"), "unexpected error: {error}"); + assert!(!manifest_path.exists()); + fs::remove_dir_all(workspace).ok(); +}