修复issue160
Project CI / Backend tests (pull_request) Failing after 13s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled

This commit is contained in:
2026-08-12 13:20:20 +00:00
parent 82957fe1d4
commit 7cbf33de96
5 changed files with 141 additions and 91 deletions
@@ -6489,9 +6489,8 @@ fn validate_editor_pixel_art_snap_placeholder_exists(
return Ok(());
}
// 中文注释:首个事务若以 DialogMissing 成功、但 HTTP 响应丢失,同 operation 的稳定
// project resource 已存在,而占位按定义仍然不存在。该形状必须允许继续走到原子 procedure
// 的 exact compare-and-return;否则幂等重放会被这个处理前门禁反向拦成 409。
let expected_task_id = format!("pixel-art-snap-{dialog_id}");
// project resource 已存在,而占位按定义仍然不存在。该形状必须允许到达紧随其后的
// stable-result guard,统一返回 GET-only 权威对账;否则会被这个处理前门禁反向拦住。
let expected_resource_id = format!(
"{EDITOR_RESOURCE_ID_PREFIX}{}",
editor_pixel_art_stable_record_suffix(
@@ -6505,7 +6504,6 @@ fn validate_editor_pixel_art_snap_placeholder_exists(
resource.resource_id == expected_resource_id
&& resource.owner_user_id == owner_user_id
&& resource.project_id == project_id
&& resource.task_id.as_deref() == Some(expected_task_id.as_str())
}) {
return Ok(());
}
@@ -6524,16 +6522,6 @@ struct EditorPixelArtSourceResolution {
object_key: String,
asset_kind: Option<String>,
generation_input_reference: Option<Value>,
existing_result_generation_inputs: Option<Option<Value>>,
// 外层 Some 表示稳定结果资源已存在;内层 None 保留“记录存在但缺 object_key”的损坏形状。
existing_result_object_key: Option<Option<String>>,
}
fn resolve_editor_pixel_art_persisted_generation_inputs(
authoritative: Option<Value>,
existing_result: Option<Option<Value>>,
) -> Option<Value> {
existing_result.unwrap_or(authoritative)
}
fn push_editor_pixel_art_source_asset_kind(
@@ -6607,20 +6595,7 @@ async fn resolve_editor_pixel_art_source_for_owner(
project: &EditorProjectPayload,
source_resource: Option<&EditorProjectResourcePayload>,
requested_asset_kind: Option<&str>,
expected_result_resource_id: &str,
expected_result_task_id: &str,
) -> Result<EditorPixelArtSourceResolution, AppError> {
let existing_stable_resource = project
.resources
.iter()
.find(|resource| resource.resource_id.trim() == expected_result_resource_id);
let existing_result = existing_stable_resource.filter(|resource| {
resource.task_id.as_deref().map(str::trim) == Some(expected_result_task_id)
});
let existing_result_generation_inputs =
existing_result.map(|resource| resource.generation_inputs.clone());
let existing_result_object_key = existing_stable_resource
.map(|resource| normalize_optional_string(resource.object_key.clone()));
let resolved_without_lookup = match source_resource {
Some(source_resource) => resolve_editor_pixel_art_source_without_lookup(
owner_user_id,
@@ -6819,24 +6794,22 @@ async fn resolve_editor_pixel_art_source_for_owner(
object_key,
asset_kind,
generation_input_reference,
existing_result_generation_inputs,
existing_result_object_key,
})
}
fn ensure_editor_pixel_art_existing_result_matches_candidate_object_key(
existing_result_object_key: Option<Option<&str>>,
candidate_object_key: &str,
fn ensure_editor_pixel_art_stable_result_is_absent(
resources: &[EditorProjectResourcePayload],
expected_result_resource_id: &str,
) -> Result<(), AppError> {
let Some(existing_result_object_key) = existing_result_object_key else {
return Ok(());
};
if existing_result_object_key == Some(candidate_object_key) {
if !resources
.iter()
.any(|resource| resource.resource_id.trim() == expected_result_resource_id)
{
return Ok(());
}
Err(editor_pixel_art_snap_failure(
StatusCode::CONFLICT,
"同一完美像素操作已有其它权威结果,请先读取项目状态对账。",
"同一完美像素操作已有权威结果,请先读取项目状态对账。",
)
.with_detail_field(EDITOR_OPERATION_RESULT_ALREADY_EXISTS_DETAIL, json!(true)))
}
@@ -7030,7 +7003,6 @@ pub async fn snap_editor_image_to_pixel_art(
// 都在许可覆盖范围内,许可随 handler 返回自动释放。
let _snap_permit = acquire_editor_pixel_art_snap_permit(processing_deadline).await?;
let owner_user_id = current_owner_user_id(&authenticated);
let expected_result_task_id = format!("pixel-art-snap-{dialog_id}");
let expected_result_resource_id = format!(
"{EDITOR_RESOURCE_ID_PREFIX}{}",
editor_pixel_art_stable_record_suffix(
@@ -7066,6 +7038,14 @@ pub async fn snap_editor_image_to_pixel_art(
project_id.as_str(),
&payload.canvas_completion,
)?;
// 中文注释:稳定 result resource 是 owner-scoped 项目快照中的权威完成事实。
// 一旦它已存在,本请求不得再解析来源、读取 OSS 或重跑像素规整;旧响应丢失和
// 跨版本 exact retry 都统一交由客户端 GET 项目快照判定 applied / DialogMissing /
// conflict。这里不附 resultPersistenceStarted:本请求尚未进入任何持久化副作用。
ensure_editor_pixel_art_stable_result_is_absent(
project.resources.as_slice(),
expected_result_resource_id.as_str(),
)?;
let source_resource = if let Some(source_resource_id) = source_resource_id.as_deref() {
Some(
project
@@ -7090,8 +7070,6 @@ pub async fn snap_editor_image_to_pixel_art(
&project,
source_resource,
payload.asset_kind.as_deref(),
expected_result_resource_id.as_str(),
expected_result_task_id.as_str(),
)
.await
})
@@ -7104,19 +7082,14 @@ pub async fn snap_editor_image_to_pixel_art(
})??;
let source_object_key = source.object_key;
let asset_kind = source.asset_kind;
let existing_result_object_key = source.existing_result_object_key;
let authoritative_generation_inputs =
rebuild_editor_generation_inputs_with_authoritative_references(
payload.generation_inputs.take(),
source.generation_input_reference.into_iter().collect(),
);
// 旧结果已经落库时,重放必须携带原记录的 metadata 才能通过 SpacetimeDB 的精确
// compare-and-return;这只复用已由服务端持久化的 owner-scoped 记录。新操作始终使用
// 上面按已鉴权源重建的 references,不再接受客户端自报 provenance。
payload.generation_inputs = resolve_editor_pixel_art_persisted_generation_inputs(
authoritative_generation_inputs,
source.existing_result_generation_inputs,
);
// 新操作始终使用按已鉴权源重建的 references,不接受客户端自报 provenance。已有稳定
// operation 已在上面的 owner-scoped 项目快照阶段返回 GET-only 对账,不会走到这里。
payload.generation_inputs = authoritative_generation_inputs;
let source_image = download_editor_persisted_image_object_within_deadline(
&state,
source_object_key.as_str(),
@@ -7203,14 +7176,6 @@ pub async fn snap_editor_image_to_pixel_art(
"genarrative",
)?;
let prepared_object_key = prepared_upload.storage_paths.object_key.clone();
// 中文注释:算法版本变化会改变 fingerprint 与 object key,但 operation/dialog 和稳定记录
// ID 保持不变。若旧版本结果已经落库,必须在任何 preflight/OSS PUT 之前失败关闭并对账。
ensure_editor_pixel_art_existing_result_matches_candidate_object_key(
existing_result_object_key
.as_ref()
.map(|object_key| object_key.as_deref()),
prepared_object_key.as_str(),
)?;
let image_src = editor_media_src_from_object_key(prepared_object_key.as_str());
let mut project_resource = EditorProjectResourceCreateRecordInput {
resource_id: persistence_identity.resource_id.clone(),
@@ -14223,26 +14188,30 @@ mod tests {
}
#[test]
fn perfect_pixel_replay_uses_existing_server_metadata_for_exact_compare() {
let authoritative = Some(json!({
fn perfect_pixel_uses_authoritative_source_metadata_for_new_operations() {
let client_claimed = Some(json!({
"fields": [],
"references": [{"refType": "project-resource", "refId": "resource-owned"}]
"references": [{"refType": "asset", "refId": "asset-other-owner"}]
}));
let historical = Some(json!({
"fields": [],
"references": [{"refType": "asset", "refId": "legacy-client-value"}]
}));
assert_eq!(
resolve_editor_pixel_art_persisted_generation_inputs(
authoritative,
Some(historical.clone()),
rebuild_editor_generation_inputs_with_authoritative_references(
sanitize_editor_untrusted_generation_inputs(client_claimed),
vec![json!({
"title": "原图",
"label": "当前项目资源",
"refType": "project-resource",
"refId": "resource-owned"
})],
),
historical
);
assert_eq!(
resolve_editor_pixel_art_persisted_generation_inputs(None, Some(None)),
None
Some(json!({
"fields": [],
"references": [{
"title": "原图",
"label": "当前项目资源",
"refType": "project-resource",
"refId": "resource-owned"
}]
}))
);
}
@@ -16095,7 +16064,7 @@ mod tests {
&completion,
)
.is_ok(),
"DialogMissing 的同 operation 重放必须进入 procedure 做 exact compare"
"DialogMissing 的同 operation 重放必须进入稳定结果权威对账"
);
}
@@ -16130,13 +16099,19 @@ mod tests {
"tokio::time::timeout_at(",
".get_editor_project",
"validate_editor_pixel_art_snap_placeholder_exists",
// 中文注释:同一稳定 operation 已有资源时,必须在来源解析、OSS GET 和 CPU
// 规整前直接转入 GET-only 权威对账;旧资源损坏也由客户端 verdict 报告,
// 本请求不得尝试以新计算补写它。
"ensure_editor_pixel_art_stable_result_is_absent(",
"resolve_editor_pixel_art_source_for_owner",
"完美像素来源归属校验超出处理预算。",
"download_editor_persisted_image_object_within_deadline",
"validate_editor_pixel_art_static_raster",
"snap_editor_pixel_art_strict",
"Some(processing_deadline)",
// 中文注释:prepare 只计算精确 object key;只读 preflight 与后续
// 中文注释:prepare 只计算新操作的精确 object key;已有稳定 result 已在
// 前面的 owner-scoped 项目快照分支返回,不能再依赖 candidate key 判定。
// 只读 preflight 与后续
// PUT/HEAD/原子 persist 共用第二份 60 秒绝对 deadline。preflight 必须发生
// 在第一次外部写之前,避免已知的目录/布局拒绝留下 OSS 孤儿对象。
"prepare_editor_generated_image_object_data(",
@@ -16150,6 +16125,15 @@ mod tests {
".persist_editor_pixel_art_result(",
],
);
assert_function_not_contains(
source,
"pub async fn snap_editor_image_to_pixel_art(",
"async fn validate_editor_background_removal_source",
&[
"ensure_editor_pixel_art_existing_result_matches_candidate_object_key(",
"resolve_editor_pixel_art_persisted_generation_inputs(",
],
);
assert_function_contains_in_order(
source,
"pub async fn snap_editor_image_to_pixel_art(",
@@ -19596,25 +19580,40 @@ mod tests {
}
#[test]
fn explicit_pixel_art_snap_reconciles_existing_result_before_upload() {
let candidate = "pixel-art-snaps/v2.png";
fn explicit_pixel_art_snap_reconciles_existing_stable_result_before_source_processing() {
let mut stable_result = editor_project_resource_for_canvas_test(
"editor-resource-stable-result",
"image",
128,
128,
);
stable_result.resource_id = "editor-resource-stable-result".to_string();
stable_result.task_id = Some("pixel-art-snap-corrupted-task".to_string());
stable_result.object_key = None;
assert!(
ensure_editor_pixel_art_existing_result_matches_candidate_object_key(None, candidate,)
ensure_editor_pixel_art_stable_result_is_absent(&[], "editor-resource-stable-result")
.is_ok()
);
assert!(
ensure_editor_pixel_art_existing_result_matches_candidate_object_key(
Some(Some(candidate)),
candidate,
)
.is_ok()
);
let error = ensure_editor_pixel_art_existing_result_matches_candidate_object_key(
Some(Some("pixel-art-snaps/v1.png")),
candidate,
let error = ensure_editor_pixel_art_stable_result_is_absent(
&[stable_result],
"editor-resource-stable-result",
)
.expect_err("different stable result must fail before upload");
.expect_err("stable result must enter authority reconciliation before source processing");
assert_eq!(error.status_code(), StatusCode::CONFLICT);
assert_eq!(
error.details().and_then(|details| details
[EDITOR_OPERATION_RESULT_ALREADY_EXISTS_DETAIL]
.as_bool()),
Some(true)
);
assert_eq!(
error
.details()
.and_then(|details| details[EDITOR_RESULT_PERSISTENCE_STARTED_DETAIL].as_bool()),
None,
"this request has not entered result persistence"
);
}
#[test]