修复issue160
This commit is contained in:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user