修复旧精修事务稳定入口迁移
Project CI / Frontend tests (pull_request) Successful in 4m36s
Project CI / Repository checks (pull_request) Successful in 4m57s
Project CI / Backend tests (pull_request) Successful in 5m43s
Project CI / Native shell tests (pull_request) Successful in 16m22s

从已校验旧事务快照推导原稳定入口路径

允许稳定入口缺失时在下一次提交重建

补充旧事务连续精修回归测试与文档
This commit is contained in:
2026-08-22 17:31:20 +08:00
parent b4788d6f87
commit 52a58f7514
4 changed files with 133 additions and 6 deletions
@@ -3017,7 +3017,24 @@ fn asset_canvas_runtime_entry_path(
if journal.asset_id != asset_id {
continue;
}
let Some(path) = journal.runtime_entry_relative_path else {
let candidate_path = match journal.runtime_entry_relative_path.clone() {
Some(path) => Some(path),
None => {
let journal_manifest_before: GameCreationAppManifest = read_asset_canvas_snapshot(
root,
&journal.commit_id,
"manifest.before.json",
&journal.manifest_before_sha256,
)?;
journal_manifest_before
.assets
.iter()
.find(|asset| asset.id == asset_id)
.map(|asset| asset.local_path.clone())
.filter(|path| !path.starts_with("assets/canvas/"))
}
};
let Some(path) = candidate_path else {
continue;
};
let is_newer = historical
@@ -3043,10 +3060,7 @@ fn asset_canvas_runtime_entry_path(
if source.local_path.starts_with("assets/canvas/") {
return Ok(None);
}
let path = resolve_local_project_path(root, &source.local_path)?;
if !path.is_file() {
return Ok(None);
}
resolve_local_project_path(root, &source.local_path)?;
Ok(Some(source.local_path.clone()))
}
@@ -584,6 +584,118 @@ fn refine_preserves_source_and_records_non_destructive_lineage() {
);
}
#[test]
fn legacy_refine_transaction_preserves_runtime_entry_for_next_commit() {
let directory = tempfile::tempdir().expect("create legacy runtime entry fixture");
init_local_game_project_at(directory.path(), PROJECT_ID, "旧事务精修项目")
.expect("initialize project");
fs::write(
directory.path().join("assets/source.png"),
png_bytes([190, 40, 90, 255]),
)
.expect("write source asset");
let manifest_path = directory.path().join(".agent/manifest.json");
let mut manifest = read_manifest(&manifest_path).expect("read manifest");
manifest.assets.push(GameCreationAppAssetManifestEntry {
id: "source-asset".to_string(),
kind: "illustration".to_string(),
media_type: "image/png".to_string(),
local_path: "assets/source.png".to_string(),
source: GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Uploaded,
canvas_project_id: None,
resource_id: None,
asset_object_id: None,
task_id: None,
prompt: None,
model: None,
generation_route: None,
generation_kind: None,
reference_resource_ids: Vec::new(),
},
});
write_manifest(&manifest_path, &manifest).expect("register source asset");
let draft = create_asset_canvas_draft_at(
directory.path(),
&create_input(
directory.path(),
&Uuid::new_v4().to_string(),
AssetCanvasIntent::Refine,
Some("source-asset".to_string()),
),
)
.expect("create refine draft")
.draft;
let fixture = Fixture {
directory,
draft: draft.clone(),
png: png_bytes([30, 90, 210, 255]),
};
let staged = stage_image(&fixture, &draft);
let first_input = commit_input(
&fixture,
&draft,
&staged,
Uuid::new_v4().to_string(),
Uuid::new_v4().to_string(),
);
commit_asset_canvas_at(fixture.root(), &first_input)
.expect("commit legacy runtime entry image");
let mut legacy_journal = read_asset_canvas_journal(fixture.root(), &first_input.commit_id)
.expect("read legacy journal")
.expect("legacy journal retained");
legacy_journal.runtime_entry_relative_path = None;
legacy_journal.runtime_entry_refreshed = false;
write_asset_canvas_journal(fixture.root(), &legacy_journal)
.expect("persist legacy journal shape");
fs::write(
fixture.root().join("assets/source.png"),
png_bytes([1, 2, 3, 255]),
)
.expect("corrupt legacy runtime entry");
let draft_after_first =
read_asset_canvas_draft_locked(fixture.root(), PROJECT_ID, &draft.draft_id)
.expect("read draft after legacy commit")
.expect("draft retained after legacy commit");
let second_png = png_bytes([210, 180, 40, 255]);
let staged_second = stage_asset_canvas_image_at(
fixture.root(),
&StageAssetCanvasImageInput {
project_path: project_path(fixture.root()),
expected_project_id: PROJECT_ID.to_string(),
draft_id: draft.draft_id.clone(),
expected_draft_revision: draft_after_first.revision,
media_type: "image/png".to_string(),
bytes: second_png.clone(),
},
)
.expect("stage second legacy image");
let second_input = commit_input(
&fixture,
&draft_after_first,
&staged_second,
Uuid::new_v4().to_string(),
Uuid::new_v4().to_string(),
);
commit_asset_canvas_at(fixture.root(), &second_input)
.expect("commit after legacy runtime entry migration");
assert_eq!(
fs::read(fixture.root().join("assets/source.png")).expect("read migrated runtime entry"),
second_png
);
let second_journal = read_asset_canvas_journal(fixture.root(), &second_input.commit_id)
.expect("read migrated journal")
.expect("migrated journal retained");
assert_eq!(
second_journal.runtime_entry_relative_path.as_deref(),
Some("assets/source.png")
);
assert!(second_journal.runtime_entry_refreshed);
}
#[test]
fn candidate_commit_freezes_layer_and_media_identity_and_keeps_draft_editable() {
let fixture = initialize_refine_fixture();
@@ -14353,6 +14353,7 @@
- 图片精修的 manifest 继续指向不可变正式版本 `assets/canvas/<name>--<commitId>.png`;游戏源码已引用的原路径(例如 `assets/direct-game-background.png`)是稳定运行入口,不要求代码改写。
- “设为最终图”提交、幂等重放和事务恢复都会校验不可变版本的字节、SHA-256、尺寸后刷新稳定入口。稳定入口缺失或损坏时,以不可变版本为恢复来源。
- 旧事务 journal 尚无稳定入口字段时,后续精修必须从该事务已校验的 `manifest.before.json` 迁移原入口路径,不能因为当前 manifest 已指向 `assets/canvas/**` 而丢失游戏运行入口。
## 2026-08-21 JavaScript 工程统一为 npm workspaces
@@ -911,7 +911,7 @@ cancelling
| A35 | 精修文件名包含历史提交后缀 | 后续精修重新打开当前 `localPath`,或再次生成 / 设为最终图 | 统一剥离文件名末尾一个或多个 `--<uuid>` 后缀并规范化为合法 1..=80 字符显示名;生成与最终提交使用同一结果 |
| A36 | 确定性提交参数无效 | 候选提交名称或用途在校验阶段失败 | 在读取候选、staging、transaction 或 ledger 写入前零副作用失败;UI 作为输入校验错误允许继续编辑,不触发安全恢复 |
| A37 | 候选首次确认 | 生成完成后与旧 autosave 并发 | 前端先同步 authoritative layers 并保存确认;确认前后端把未确认候选层合回旧保存,重启恢复可从私有 ledger 重建候选层,确认后的显式删除仍允许 |
| A38 | 稳定运行入口 | 精修替换已在游戏源码中引用的图片 | manifest 指向不可变正式版本,同时原稳定入口路径不变并刷新为新版本字节;幂等重放和事务恢复会修复缺失或不匹配入口,游戏源码不需要改路径 |
| A38 | 稳定运行入口 | 精修替换已在游戏源码中引用的图片,或继续精修旧版本事务创建的资源 | manifest 指向不可变正式版本,同时原稳定入口路径不变并刷新为新版本字节;新事务可从旧事务 `manifest.before.json` 迁移稳定入口身份,幂等重放和事务恢复会修复缺失或不匹配入口,游戏源码不需要改路径 |
阶段一至五最终审计只有在矩阵对应的纯模型、共享 React、Web adapter、Tauri adapter、Rust 持久化与 AppSurface 测试全部通过后,才可宣称图片素材创作正式闭环完成。