修复普通图片类型迁移一致性
同步 active 结构化画布迁移摘要与完整性记录,保证清理后可继续保存。 在 API 与 SpacetimeDB 权威写入边界将精确 image 归一为空类型。 补充迁移后保存与写入边界回归测试并更新生产迁移契约。
This commit is contained in:
@@ -6917,3 +6917,8 @@
|
||||
- 一致性:`start-tauri-dev.mjs` 是端口选择权威,最终端口通过 Tauri CLI `--config` 覆盖 `build.devUrl`,通过 `GENARRATIVE_AGC_VITE_PORT` 传给 `beforeDevCommand` 和配套后端端口解析器,并通过 Vite CLI `--port` 启动严格监听。父启动器选定端口后,子启动器只允许严格使用同一端口,配套后端漂移必须跳过该预留端口,竞态占用必须失败关闭。
|
||||
- 安全边界:动态端口不恢复旧 Vite 复用。无法证明 worktree 归属的监听器仍不复用、不主动终止;同用户多 worktree 通过段内漂移并行,不通过共享未知服务并行。
|
||||
- 验证:公共端口映射、Linux 默认槽位与段内漂移、非 Linux 兼容漂移、Tauri 动态配置、启动前预检、进程树收束、AGC typecheck / 配置门禁、编码检查和差异检查必须通过。
|
||||
|
||||
## 2026-08-10 普通图片废弃 assetKind 的迁移与写入门禁
|
||||
|
||||
- 语义:普通静态图片的持久化 `asset_kind` 为 `NULL`,旧值 `image` 不再是合法业务类型。登录态 API 和 SpacetimeDB storage 的项目资源 / 账号素材创建边界统一将 trim 后精确等于 `image` 的输入归一为空;其它值不借本次迁移扩大或收紧既有校验。
|
||||
- 迁移:生产维护窗口内按 `asset → project-resource → showcase → canvas` 执行受 operator 鉴权、分批 dry-run、批次 SHA-256 绑定和 apply 后零残留复核的清理。active 结构化画布同步刷新 legacy / structured hash、完整性和 verified revision,但不推进业务 revision、不改 migration 状态或时间戳;缺失一致 active 迁移记录时整批失败关闭。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -94,7 +94,7 @@ BgFilter 对已经落入私有 OSS 的生成原图、动作抽取帧和手动去
|
||||
|
||||
角色动作正式字段收口使用 `node scripts/spacetime-normalize-editor-character-actions.mjs --database <database> --server-url <url>`,且同样只能由已授权 migration operator 执行。必须先发布包含 normalization cursor 索引和 `normalize_editor_character_animation_metadata_and_return` 的 SpacetimeDB 模块,在 API / worker 仍处于维护模式时先运行默认全量 dry-run;脚本固定按 `asset → project-resource → showcase → canvas` 扫描,普通 scope 每批最多 25 行,canvas 每批最多 5 行。全量 dry-run 会在不写库的情况下把 asset 计划结果投影给同 owner / task / 首帧对象精确匹配的 project-resource,再把前置 scope 的计划结果投影给 canvas 检查;因此同 task 的误标预览 MP4 会先按权威视频对象排除,最终图片序列会逐帧核对并补齐精确 `asset_object` 身份。canvas 中仍引用误标 preview resource 的普通 video layer 会按 project-resource 计划态 `video` 跳过,只有 layout 明确声明动作却指向视频,或资源规划本身失败时才形成 blocker。apply 时仍要求前置 scope 已按顺序物理完成,不能跳过 asset 直接让 project-resource 借未落库结果。历史 canvas 复制的 `sourceResourceId` 不是迁移证据,不要因它仍指向原角色而手工改库,补建资源会采用最终账号素材的 DB 血缘。出现 blocker 时脚本会打印 ID、原因、owner、project、task、对象身份和来源资源;先据此区分最终候选为零 / 多个、正式与旧版冲突、帧对象不匹配或缺失资源,不得跳过 scope。确认 dry-run 后追加 `--apply`,脚本会对每批重新 dry-run、携带该批 SHA-256 apply,并在最后从头要求四个 scope 均为零匹配、零 blocker。只有该复核通过后才发布移除 action fallback 的 API / Web。Stdb build artifact 和完整 release 包必须同时包含 `scripts/spacetime-normalize-editor-character-actions.mjs` 与 `scripts/spacetime-migration-common.mjs`。本地切换分支时若要避免 dev publish 因 schema 冲突使用 `-c=on-conflict` 清库,启动命令必须追加 `--preserve-database`,让冲突直接失败。
|
||||
|
||||
普通图片错误素材类型清理使用 `npm run spacetime:editor-image-asset-kind:clean -- --database <database> --server-url <url>`,只能由已授权 migration operator 执行。先进入维护模式并发布包含 `clean_editor_image_asset_kind_and_return` 的 SpacetimeDB module,并保持会写入旧值的在线 API / controller / worker 停止;随后运行默认全量 dry-run,核对 `asset → project-resource → showcase → canvas` 各 scope 的扫描数、命中行数、字段数和 blocker 均符合预期,再追加 `--apply`。脚本对每批重新 dry-run、绑定 SHA-256,最后自动从头复核零命中;任一画布数据异常都会只输出哈希化 ID、scope 与原因并停止,不能跳过。清理只处理精确业务旧值,不修改 `asset_object.asset_kind`、MIME、媒体类型、API 或常规 storage 写入语义;完成零残留复核后恢复已修正的应用版本,最后退出维护。Stdb build artifact 和完整 release 包必须同时包含 `scripts/spacetime-clean-editor-image-asset-kind.mjs` 与 `scripts/spacetime-migration-common.mjs`。
|
||||
普通图片错误素材类型清理使用 `npm run spacetime:editor-image-asset-kind:clean -- --database <database> --server-url <url>`,只能由已授权 migration operator 执行。先进入维护模式并发布包含 `clean_editor_image_asset_kind_and_return` 的 SpacetimeDB module,并保持旧版本 API / controller / worker 停止;随后运行默认全量 dry-run,核对 `asset → project-resource → showcase → canvas` 各 scope 的扫描数、命中行数、字段数和 blocker 均符合预期,再追加 `--apply`。脚本对每批重新 dry-run、绑定包含画布迁移摘要的 SHA-256,最后自动从头复核零命中;任一画布数据异常都会只输出哈希化 ID、scope 与原因并停止,不能跳过。清理只处理精确业务旧值,不修改 `asset_object.asset_kind`、MIME 或媒体类型;canvas 清理会在不推进业务 revision、不刷新迁移时间戳的前提下同步 active 结构化画布的 legacy / structured hash、完整性摘要和 verified revision。新版本 API 与 SpacetimeDB storage 的项目资源 / 账号素材创建边界会将 trim 后精确等于 `image` 的 `assetKind` 归一为 `NULL`,防止旧页面和滞留请求重新制造废弃值。完成零残留复核并确认 active 结构化画布可继续保存后恢复应用版本,最后退出维护。Stdb build artifact 和完整 release 包必须同时包含 `scripts/spacetime-clean-editor-image-asset-kind.mjs` 与 `scripts/spacetime-migration-common.mjs`。
|
||||
|
||||
自 2026-07-11 起,`Genarrative-Full-Build-And-Deploy` 的每日 04:00 timer 默认以 `DEPLOY_TARGET=development`、`STDB_API_ROLLOUT_MODE=normal` 对仅供开发使用的 dev 服务器执行 Stdb → API → Web 完整发布,不进入人工 rollout gate。三个下游 Build 都由 Full Job 显式传 `PUBLISH_AFTER_BUILD=false`,不得依赖下游 Job 默认值或提前各自发布;统一 Build 完成后仍由 Full Job 按固定顺序发布。人工维护窗口才选择 `pause-after-stdb`,且必须配置 `STDB_API_ROLLOUT_APPROVERS`。上文“定时构建缺少审批人时失败”的旧口径不再作为当前 dev 定时发布行为。
|
||||
|
||||
|
||||
@@ -160,6 +160,7 @@ pub(crate) fn editor_provider_reference_limit(model: &str) -> usize {
|
||||
}
|
||||
const EDITOR_CHARACTER_IMAGE_ASSET_KIND: &str = "editor_character_image";
|
||||
const EDITOR_CHARACTER_ANIMATION_CONTRACT_KIND: &str = "character-animation";
|
||||
const EDITOR_LEGACY_IMAGE_ASSET_KIND: &str = "image";
|
||||
pub(crate) const EDITOR_CHARACTER_IMAGE_ENTITY_KIND: &str = "editor_project";
|
||||
const EDITOR_CHARACTER_IMAGE_SLOT: &str = "character";
|
||||
const EDITOR_GENERATED_IMAGE_ASSET_KIND: &str = "editor_generated_image";
|
||||
@@ -2120,8 +2121,9 @@ pub async fn create_editor_project_resource(
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(payload): Json<EditorProjectResourceCreateRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let asset_kind = normalize_editor_persisted_asset_kind(payload.asset_kind);
|
||||
let generation_inputs_json = serialize_editor_generation_inputs(
|
||||
payload.asset_kind.as_deref(),
|
||||
asset_kind.as_deref(),
|
||||
sanitize_editor_untrusted_generation_inputs(payload.generation_inputs.clone()),
|
||||
)?;
|
||||
let image_sequence_frames_json =
|
||||
@@ -2146,7 +2148,7 @@ pub async fn create_editor_project_resource(
|
||||
provider: normalize_optional_string(payload.provider),
|
||||
task_id: normalize_optional_string(payload.task_id),
|
||||
source_resource_id: normalize_optional_string(payload.source_resource_id),
|
||||
asset_kind: normalize_optional_string(payload.asset_kind),
|
||||
asset_kind,
|
||||
generation_inputs_json,
|
||||
updated_at_micros: current_utc_micros(),
|
||||
image_sequence_frames_json,
|
||||
@@ -2294,8 +2296,9 @@ pub async fn create_editor_asset(
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(payload): Json<EditorAssetCreateRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let asset_kind = normalize_editor_persisted_asset_kind(payload.asset_kind);
|
||||
let generation_inputs_json = serialize_editor_generation_inputs(
|
||||
payload.asset_kind.as_deref(),
|
||||
asset_kind.as_deref(),
|
||||
sanitize_editor_untrusted_generation_inputs(payload.generation_inputs.clone()),
|
||||
)?;
|
||||
let image_sequence_frames_json =
|
||||
@@ -2320,7 +2323,7 @@ pub async fn create_editor_asset(
|
||||
model: normalize_optional_string(payload.model),
|
||||
provider: normalize_optional_string(payload.provider),
|
||||
task_id: normalize_optional_string(payload.task_id),
|
||||
asset_kind: normalize_optional_string(payload.asset_kind),
|
||||
asset_kind,
|
||||
generation_inputs_json,
|
||||
source_resource_id: None,
|
||||
now_micros: current_utc_micros(),
|
||||
@@ -11137,6 +11140,10 @@ pub(crate) fn normalize_optional_string(value: Option<String>) -> Option<String>
|
||||
.filter(|item| !item.is_empty())
|
||||
}
|
||||
|
||||
fn normalize_editor_persisted_asset_kind(value: Option<String>) -> Option<String> {
|
||||
normalize_optional_string(value).filter(|value| value != EDITOR_LEGACY_IMAGE_ASSET_KIND)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_editor_generated_asset_label(
|
||||
value: Option<String>,
|
||||
fallback: &str,
|
||||
@@ -14897,6 +14904,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_image_asset_kind_write_boundary_normalizes_only_exact_legacy_value() {
|
||||
assert_eq!(
|
||||
normalize_editor_persisted_asset_kind(Some(" image ".to_string())),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_editor_persisted_asset_kind(Some("scene".to_string())).as_deref(),
|
||||
Some("scene")
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_editor_persisted_asset_kind(Some("Image".to_string())).as_deref(),
|
||||
Some("Image")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_sequence_frame_serialization_rejects_frame_index() {
|
||||
let error = serialize_editor_image_sequence_frames(Some(json!([
|
||||
|
||||
@@ -2636,7 +2636,7 @@ fn prepare_editor_project_resource(
|
||||
let model = normalize_optional(input.model);
|
||||
let provider = normalize_optional(input.provider);
|
||||
let task_id = normalize_optional(input.task_id);
|
||||
let asset_kind = normalize_optional(input.asset_kind);
|
||||
let asset_kind = normalize_editor_persisted_asset_kind(input.asset_kind);
|
||||
let generation_inputs_json = normalize_optional(input.generation_inputs_json);
|
||||
let image_sequence_frames_json = normalize_optional(input.image_sequence_frames_json);
|
||||
let image_sequence_duration_ms = input.image_sequence_duration_ms;
|
||||
@@ -5474,7 +5474,7 @@ fn create_editor_asset(
|
||||
if group_task_id.is_some() {
|
||||
require_editor_generation_runtime_service_identity(ctx, caller)?;
|
||||
}
|
||||
let asset_kind = normalize_optional(input.asset_kind);
|
||||
let asset_kind = normalize_editor_persisted_asset_kind(input.asset_kind);
|
||||
let generation_inputs_json = normalize_optional(input.generation_inputs_json);
|
||||
let image_sequence_frames_json = normalize_optional(input.image_sequence_frames_json);
|
||||
validate_editor_generation_inputs(asset_kind.as_deref(), generation_inputs_json.as_deref())?;
|
||||
@@ -8400,6 +8400,10 @@ fn normalize_optional(value: Option<String>) -> Option<String> {
|
||||
.filter(|item| !item.is_empty())
|
||||
}
|
||||
|
||||
fn normalize_editor_persisted_asset_kind(value: Option<String>) -> Option<String> {
|
||||
normalize_optional(value).filter(|value| value != EDITOR_LEGACY_IMAGE_ASSET_KIND)
|
||||
}
|
||||
|
||||
fn normalize_title(value: &str) -> String {
|
||||
let title = value.trim();
|
||||
if title.is_empty() {
|
||||
@@ -10062,6 +10066,22 @@ fn verify_migration_layout(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn refresh_editor_image_asset_kind_layout_migration(
|
||||
mut migration: EditorCanvasLayoutMigration,
|
||||
source_layout_sha256: String,
|
||||
structured_layout_sha256: String,
|
||||
verified_revision: u64,
|
||||
integrity: &CanvasLayoutIntegrity,
|
||||
) -> EditorCanvasLayoutMigration {
|
||||
migration.source_layout_sha256 = source_layout_sha256;
|
||||
migration.structured_layout_sha256 = structured_layout_sha256;
|
||||
migration.verified_revision = verified_revision;
|
||||
migration.layer_count = integrity.layer_count;
|
||||
migration.dialog_count = integrity.dialog_count;
|
||||
migration.resource_refs_sha256 = integrity.resource_refs_sha256.clone();
|
||||
migration
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct EditorCanvasResourceLayoutPatch {
|
||||
layers_json: String,
|
||||
@@ -12899,6 +12919,11 @@ fn plan_editor_image_asset_kind_canvas(
|
||||
.filter(&canvas.canvas_id)
|
||||
.collect::<Vec<_>>();
|
||||
layer_rows.sort_by(|left, right| left.layer_key.cmp(&right.layer_key));
|
||||
let migration = ctx
|
||||
.db
|
||||
.editor_canvas_layout_migration()
|
||||
.canvas_id()
|
||||
.find(&canvas.canvas_id);
|
||||
|
||||
let mut source_hasher = Sha256::new();
|
||||
for value in [
|
||||
@@ -12914,6 +12939,8 @@ fn plan_editor_image_asset_kind_canvas(
|
||||
source_hasher.update(value.as_bytes());
|
||||
source_hasher.update([0]);
|
||||
}
|
||||
source_hasher.update(canvas.revision.to_le_bytes());
|
||||
source_hasher.update(canvas.layout_storage_version.to_le_bytes());
|
||||
for row in &layer_rows {
|
||||
for value in [
|
||||
row.layer_key.as_str(),
|
||||
@@ -12924,6 +12951,24 @@ fn plan_editor_image_asset_kind_canvas(
|
||||
source_hasher.update([0]);
|
||||
}
|
||||
}
|
||||
if let Some(migration) = migration.as_ref() {
|
||||
for value in [
|
||||
migration.canvas_id.as_str(),
|
||||
migration.project_id.as_str(),
|
||||
migration.owner_user_id.as_str(),
|
||||
migration.source_layout_sha256.as_str(),
|
||||
migration.structured_layout_sha256.as_str(),
|
||||
migration.resource_refs_sha256.as_str(),
|
||||
migration.status.as_str(),
|
||||
] {
|
||||
source_hasher.update(value.as_bytes());
|
||||
source_hasher.update([0]);
|
||||
}
|
||||
source_hasher.update(migration.verified_revision.to_le_bytes());
|
||||
source_hasher.update(migration.layer_count.to_le_bytes());
|
||||
source_hasher.update(migration.dialog_count.to_le_bytes());
|
||||
source_hasher.update(migration.migration_version.to_le_bytes());
|
||||
}
|
||||
let source_fingerprint = format!("{:x}", source_hasher.finalize());
|
||||
|
||||
let (canvas_layers_json, canvas_cleaned_count) =
|
||||
@@ -13012,7 +13057,7 @@ fn plan_editor_image_asset_kind_canvas(
|
||||
blocker: None,
|
||||
};
|
||||
}
|
||||
let blocker = match project.as_ref() {
|
||||
let mut blocker = match project.as_ref() {
|
||||
None => Some("命中普通图片旧值的画布缺少对应 editor_project".to_string()),
|
||||
Some(project) if project.owner_user_id != canvas.owner_user_id => {
|
||||
Some("命中普通图片旧值的 editor_canvas 与 editor_project owner 不一致".to_string())
|
||||
@@ -13028,6 +13073,23 @@ fn plan_editor_image_asset_kind_canvas(
|
||||
(Err(message), _) | (_, Err(message)) => Some(message),
|
||||
},
|
||||
};
|
||||
if blocker.is_none()
|
||||
&& canvas.layout_storage_version == EDITOR_CANVAS_LAYOUT_STORAGE_VERSION_STRUCTURED
|
||||
{
|
||||
blocker = match migration.as_ref() {
|
||||
None => Some("active 结构化图片画布缺少迁移记录".to_string()),
|
||||
Some(migration) if migration.status != EDITOR_CANVAS_LAYOUT_MIGRATION_STATUS_ACTIVE => {
|
||||
Some("结构化图片画布的迁移记录不是 active 状态".to_string())
|
||||
}
|
||||
Some(migration)
|
||||
if migration.project_id != canvas.project_id
|
||||
|| migration.owner_user_id != canvas.owner_user_id =>
|
||||
{
|
||||
Some("结构化图片画布与迁移记录的工程或 owner 不一致".to_string())
|
||||
}
|
||||
Some(_) => None,
|
||||
};
|
||||
}
|
||||
EditorImageAssetKindCanvasPlan {
|
||||
canvas_layers_json,
|
||||
project_layers_json,
|
||||
@@ -13044,6 +13106,20 @@ fn apply_editor_image_asset_kind_canvas_plan(
|
||||
mut canvas: EditorCanvas,
|
||||
plan: EditorImageAssetKindCanvasPlan,
|
||||
) -> Result<(), String> {
|
||||
let migration = ctx
|
||||
.db
|
||||
.editor_canvas_layout_migration()
|
||||
.canvas_id()
|
||||
.find(&canvas.canvas_id);
|
||||
if canvas.layout_storage_version == EDITOR_CANVAS_LAYOUT_STORAGE_VERSION_STRUCTURED
|
||||
&& migration.as_ref().is_none_or(|migration| {
|
||||
migration.status != EDITOR_CANVAS_LAYOUT_MIGRATION_STATUS_ACTIVE
|
||||
|| migration.project_id != canvas.project_id
|
||||
|| migration.owner_user_id != canvas.owner_user_id
|
||||
})
|
||||
{
|
||||
return Err("active 结构化图片画布缺少一致的迁移记录,拒绝清理".to_string());
|
||||
}
|
||||
let mut project = ctx
|
||||
.db
|
||||
.editor_project()
|
||||
@@ -13052,7 +13128,7 @@ fn apply_editor_image_asset_kind_canvas_plan(
|
||||
.ok_or_else(|| "图片画布对应的工程不存在".to_string())?;
|
||||
if canvas.layers_json != plan.canvas_layers_json {
|
||||
canvas.layers_json = plan.canvas_layers_json;
|
||||
ctx.db.editor_canvas().canvas_id().update(canvas);
|
||||
ctx.db.editor_canvas().canvas_id().update(canvas.clone());
|
||||
}
|
||||
if project.layers_json != plan.project_layers_json {
|
||||
project.layers_json = plan.project_layers_json;
|
||||
@@ -13071,6 +13147,40 @@ fn apply_editor_image_asset_kind_canvas_plan(
|
||||
row.item_json = patch.item_json;
|
||||
ctx.db.editor_canvas_layer().layer_key().update(row);
|
||||
}
|
||||
if let Some(migration) = migration {
|
||||
let resource_asset_kinds = project_resource_asset_kinds(
|
||||
ctx,
|
||||
canvas.project_id.as_str(),
|
||||
canvas.owner_user_id.as_str(),
|
||||
);
|
||||
let source_layout_sha256 = canonical_layout_sha256_with_resource_asset_kinds(
|
||||
canvas.layers_json.as_str(),
|
||||
&resource_asset_kinds,
|
||||
)?;
|
||||
let structured_layout_json = build_structured_canvas_layout_json(ctx, &canvas)?;
|
||||
let structured_layout_sha256 = canonical_layout_sha256_with_resource_asset_kinds(
|
||||
structured_layout_json.as_str(),
|
||||
&resource_asset_kinds,
|
||||
)?;
|
||||
let integrity = canvas_layout_integrity(structured_layout_json.as_str())?;
|
||||
let migration = refresh_editor_image_asset_kind_layout_migration(
|
||||
migration,
|
||||
source_layout_sha256,
|
||||
structured_layout_sha256.clone(),
|
||||
canvas.revision,
|
||||
&integrity,
|
||||
);
|
||||
verify_migration_layout(
|
||||
&migration,
|
||||
canvas.revision,
|
||||
structured_layout_sha256.as_str(),
|
||||
&integrity,
|
||||
)?;
|
||||
ctx.db
|
||||
.editor_canvas_layout_migration()
|
||||
.canvas_id()
|
||||
.update(migration);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -19707,4 +19817,97 @@ mod tests {
|
||||
assert!(sample.contains("idSha256"));
|
||||
assert!(sample.contains("布局不一致"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_image_asset_kind_write_boundary_normalizes_only_exact_legacy_value() {
|
||||
assert_eq!(
|
||||
normalize_editor_persisted_asset_kind(Some(" image ".to_string())),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_editor_persisted_asset_kind(Some("scene".to_string())).as_deref(),
|
||||
Some("scene")
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_editor_persisted_asset_kind(Some("Image".to_string())).as_deref(),
|
||||
Some("Image")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_image_asset_kind_cleanup_keeps_active_structured_canvas_save_verifiable() {
|
||||
let now = Timestamp::from_micros_since_unix_epoch(1_000_000);
|
||||
let activated_at = Timestamp::from_micros_since_unix_epoch(900_000);
|
||||
let legacy_json = json!([{
|
||||
"layerId": "layer-1",
|
||||
"resourceId": "resource-1",
|
||||
"assetKind": "image"
|
||||
}])
|
||||
.to_string();
|
||||
let structured_json = json!([{
|
||||
"layerId": "layer-1",
|
||||
"resourceId": "resource-1",
|
||||
"assetKindOverride": "image"
|
||||
}])
|
||||
.to_string();
|
||||
let (cleaned_legacy_json, legacy_cleaned_count) =
|
||||
clean_legacy_image_asset_kind_from_layout(legacy_json.as_str()).unwrap();
|
||||
let (cleaned_structured_json, structured_cleaned_count) =
|
||||
clean_legacy_image_asset_kind_from_layout(structured_json.as_str()).unwrap();
|
||||
assert_eq!((legacy_cleaned_count, structured_cleaned_count), (1, 1));
|
||||
|
||||
let resource_asset_kinds = BTreeMap::new();
|
||||
let source_hash = canonical_layout_sha256_with_resource_asset_kinds(
|
||||
cleaned_legacy_json.as_str(),
|
||||
&resource_asset_kinds,
|
||||
)
|
||||
.unwrap();
|
||||
let structured_hash = canonical_layout_sha256_with_resource_asset_kinds(
|
||||
cleaned_structured_json.as_str(),
|
||||
&resource_asset_kinds,
|
||||
)
|
||||
.unwrap();
|
||||
let integrity = canvas_layout_integrity(cleaned_structured_json.as_str()).unwrap();
|
||||
let migration = EditorCanvasLayoutMigration {
|
||||
canvas_id: "canvas-1".to_string(),
|
||||
project_id: "project-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
source_layout_sha256: "stale-source".to_string(),
|
||||
structured_layout_sha256: "stale-structured".to_string(),
|
||||
verified_revision: 6,
|
||||
layer_count: 9,
|
||||
dialog_count: 9,
|
||||
resource_refs_sha256: "stale-refs".to_string(),
|
||||
migration_version: EDITOR_CANVAS_LAYOUT_STORAGE_VERSION_STRUCTURED,
|
||||
status: EDITOR_CANVAS_LAYOUT_MIGRATION_STATUS_ACTIVE.to_string(),
|
||||
migrated_at: now,
|
||||
activated_at: Some(activated_at),
|
||||
rolled_back_at: None,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
let refreshed = refresh_editor_image_asset_kind_layout_migration(
|
||||
migration,
|
||||
source_hash.clone(),
|
||||
structured_hash,
|
||||
7,
|
||||
&integrity,
|
||||
);
|
||||
|
||||
assert_eq!(refreshed.source_layout_sha256, source_hash);
|
||||
assert_eq!(
|
||||
refreshed.status,
|
||||
EDITOR_CANVAS_LAYOUT_MIGRATION_STATUS_ACTIVE
|
||||
);
|
||||
assert_eq!(refreshed.migrated_at, now);
|
||||
assert_eq!(refreshed.activated_at, Some(activated_at));
|
||||
assert_eq!(refreshed.updated_at, now);
|
||||
verify_active_structured_canvas_migration(
|
||||
&refreshed,
|
||||
7,
|
||||
cleaned_structured_json.as_str(),
|
||||
&resource_asset_kinds,
|
||||
)
|
||||
.expect("cleaned active structured canvas should pass the next save verification");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user