修正 icon 全局放行后的两处判据:删掉死分支并更新旧断言

- 删除 `ensure_editor_image_edit_source_kind_allowed_for_request` 里 `AGC && assetKind=="icon" → Ok` 的例外分支:`icon` 已进静态图类型表被全局放行,该分支恒不改变判定结果
- 连带删除因此不再被引用的 `is_game_creator_resource_editor_generation` 与其定向用例(同一 scope 无人调用);AGC 来源别名口径仍由 `EditorGenerationQueueResultContext::from_job` 的 queue consumer 判定承担并有既有用例覆盖
- 两处调用点改为直接调 `ensure_editor_image_edit_source_kind_allowed`,删掉没有行为的 `_for_request` 包装层与 `generation_inputs` 形参
- 旧用例 `game_creator_resource_editor_bypasses_quick_edit_whitelist_only_for_its_source` 改名 `quick_edit_source_kind_policy_ignores_generation_source`,改为断言来源类型判据与 generation source 无关
- 修正 master 侧旧断言 `editor_image_edit_target_binding_prefers_object_id_and_fails_closed`:反例 effective_asset_kind 从 `icon` 换成仍被拒的 `character-animation`,断言保持 Err,守卫意图(目标图层不能借壳成允许来源)不变
This commit is contained in:
2026-09-11 12:11:37 +08:00
parent a5aba86493
commit 49a30d51ff
+31 -139
View File
@@ -4163,37 +4163,6 @@ pub(crate) fn ensure_editor_image_edit_source_kind_allowed(
)
}
fn is_game_creator_resource_editor_generation(generation_inputs: Option<&Value>) -> bool {
generation_inputs
.and_then(|value| value.pointer("/source"))
.and_then(Value::as_str)
.is_some_and(|source| {
// 中文注释:AGC 客户端在发送前会把 source 改写成 `ai-game-creator-client`
// 两个 source 是同一个 consumer 的别名(见 `from_job` 的 queue consumer 判定),
// 这里必须同口径,否则客户端实际发出的请求永远走不到 AGC 分支。
matches!(
source.trim(),
GAME_CREATOR_RESOURCE_EDITOR_SOURCE | GAME_CREATOR_CLIENT_GENERATION_SOURCE
)
})
}
fn ensure_editor_image_edit_source_kind_allowed_for_request(
asset_kind: Option<&str>,
media_type: Option<&str>,
generation_inputs: Option<&Value>,
) -> Result<(), AppError> {
let asset_kind = asset_kind.map(str::trim).filter(|value| !value.is_empty());
let media_type = media_type.map(str::trim).filter(|value| !value.is_empty());
if is_game_creator_resource_editor_generation(generation_inputs)
&& asset_kind == Some("icon")
&& matches!(media_type, None | Some("image"))
{
return Ok(());
}
ensure_editor_image_edit_source_kind_allowed(asset_kind, media_type)
}
pub(crate) const EDITOR_IMAGE_EDIT_QUEUE_PAYLOAD_VERSION: u32 = 1;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
@@ -4379,10 +4348,9 @@ fn ensure_editor_image_edit_target_matches_source(
})),
);
}
ensure_editor_image_edit_source_kind_allowed_for_request(
ensure_editor_image_edit_source_kind_allowed(
target.effective_asset_kind.as_deref(),
Some(target.media_type.as_str()),
generation_inputs,
)
}
@@ -4524,11 +4492,7 @@ async fn resolve_editor_image_edit_source(
.map(str::trim)
.filter(|value| !value.is_empty())
else {
ensure_editor_image_edit_source_kind_allowed_for_request(
source_asset_kind.as_deref(),
Some("image"),
payload.generation_inputs.as_ref(),
)?;
ensure_editor_image_edit_source_kind_allowed(source_asset_kind.as_deref(), Some("image"))?;
return Ok(EditorImageEditResolvedSource {
reference_kind,
source_reference_id: source_reference_id.to_string(),
@@ -20759,96 +20723,29 @@ mod tests {
}
#[test]
fn game_creator_resource_editor_bypasses_quick_edit_whitelist_only_for_its_source() {
let generation_inputs = json!({ "source": GAME_CREATOR_RESOURCE_EDITOR_SOURCE });
fn quick_edit_source_kind_policy_ignores_generation_source() {
assert!(
ensure_editor_image_edit_source_kind_allowed_for_request(
Some("icon"),
Some("image"),
Some(&generation_inputs),
)
.is_ok()
ensure_editor_image_edit_source_kind_allowed(Some("icon"), Some("image")).is_ok(),
"icon 是共享契约里的静态图 canonical 类型,与 generationInputs.source 无关"
);
assert!(
ensure_editor_image_edit_source_kind_allowed_for_request(
Some("icon"),
Some("image"),
Some(&json!({ "source": "editor-agent" })),
)
.is_ok(),
"icon 已是共享契约里的静态图 canonical 类型,与 generationInputs.source 无关"
ensure_editor_image_edit_source_kind_allowed(Some("art-spritesheet"), Some("image"))
.is_ok()
);
assert!(ensure_editor_image_edit_source_kind_allowed(Some("icon"), Some("video")).is_err());
assert!(
ensure_editor_image_edit_source_kind_allowed_for_request(
Some("icon"),
Some("video"),
Some(&generation_inputs),
)
.is_err()
);
assert!(
ensure_editor_image_edit_source_kind_allowed_for_request(
Some("future-kind"),
Some("image"),
Some(&generation_inputs),
)
.is_err()
ensure_editor_image_edit_source_kind_allowed(Some("future-kind"), Some("image"))
.is_err()
);
}
/// AGC 客户端发送前把 `generationInputs.source` 改写成
/// `ai-game-creator-client``resource_editor.rs` `submit_resource_edit_remote`),
/// 服务端必须按队列 consumer 的同口径把它认成同一个 AGC consumer,否则 AGC 分支是死代码。
#[test]
fn game_creator_client_source_is_recognized_as_game_creator_resource_editor() {
assert!(is_game_creator_resource_editor_generation(Some(&json!({
"source": GAME_CREATOR_CLIENT_GENERATION_SOURCE
}))));
assert!(is_game_creator_resource_editor_generation(Some(&json!({
"source": GAME_CREATOR_RESOURCE_EDITOR_SOURCE
}))));
assert!(
is_game_creator_resource_editor_generation(Some(&json!({
"source": " game-creator-resource-editor "
}))),
"别名判定沿用 trim 口径"
);
assert!(!is_game_creator_resource_editor_generation(Some(&json!({
"source": "editor-agent"
}))));
assert!(!is_game_creator_resource_editor_generation(Some(
&json!({})
)));
assert!(!is_game_creator_resource_editor_generation(None));
// 端到端同口径:AGC 客户端实际发出的 source 必须与 `from_job` 的 consumer 判定一致。
// 快速编辑端点把 `generationInputs` 对象本身当判定输入,所以这里按 `/generationInputs` 取。
let mut queued_job = atomic_editor_generation_job_fixture();
queued_job.dedupe_key =
"game-creator-client-generation:editor_image_generation:fingerprint".to_string();
queued_job.request_payload_json = json!({
"generationInputs": { "source": GAME_CREATOR_CLIENT_GENERATION_SOURCE }
})
.to_string();
assert_eq!(
EditorGenerationQueueResultContext::from_job(&queued_job).consumer,
EditorGenerationQueueConsumer::GameCreatorResourceEditor
);
let queued_payload: Value = serde_json::from_str(queued_job.request_payload_json.as_str())
.expect("fixture payload should be JSON");
assert!(is_game_creator_resource_editor_generation(
queued_payload.pointer("/generationInputs")
));
}
/// 实测 400×5 的回归用例:AGC「图片快速编辑」发出的请求,source 已被改写成
/// `ai-game-creator-client`,来源资源的 assetKind 是本地 manifest 原始类型
/// `art-spritesheet` / `ui`mediaType=image)。这条路径必须放行;
/// 实测 400×5 的回归用例:AGC「图片快速编辑」发出的请求,来源资源的 assetKind 是本地
/// manifest 原始类型 `art-spritesheet` / `ui`mediaType=image)。这条路径必须放行;
/// 非静态媒体(video/audio/image-sequence)必须继续拒绝。
#[test]
fn game_creator_client_quick_edit_accepts_local_manifest_static_image_kinds() {
// 与客户端 `submit_resource_edit_remote` 发送的 JSON 同形:请求体
// `generationInputs` 就是这里传入的判定输入
// 与客户端 `submit_resource_edit_remote` 发送的 JSON 同形;放行判据只看来源资源
// 权威 assetKind 与 mediaType,请求体里的 `generationInputs.source` 不参与
let agc_payload: Value = serde_json::from_str(
r#"{
"prompt": "把这张图改成夜间配色",
@@ -20860,9 +20757,13 @@ mod tests {
}"#,
)
.expect("AGC quick edit payload should be JSON");
let generation_inputs = agc_payload
.pointer("/generationInputs")
.expect("AGC payload should carry generationInputs");
assert_eq!(
agc_payload
.pointer("/generationInputs/assetKind")
.and_then(Value::as_str),
Some("art-spritesheet"),
"回归夹具必须保持 AGC 客户端实际发送的请求形状"
);
for (asset_kind, media_type) in [
("art-spritesheet", "image"),
@@ -20873,12 +20774,8 @@ mod tests {
("icon", "image"),
] {
assert!(
ensure_editor_image_edit_source_kind_allowed_for_request(
Some(asset_kind),
Some(media_type),
Some(generation_inputs),
)
.is_ok(),
ensure_editor_image_edit_source_kind_allowed(Some(asset_kind), Some(media_type))
.is_ok(),
"AGC 快速编辑必须放行 {asset_kind}/{media_type}"
);
}
@@ -20893,24 +20790,17 @@ mod tests {
("future-kind", "image"),
] {
assert!(
ensure_editor_image_edit_source_kind_allowed_for_request(
Some(asset_kind),
Some(media_type),
Some(generation_inputs),
)
.is_err(),
ensure_editor_image_edit_source_kind_allowed(Some(asset_kind), Some(media_type))
.is_err(),
"非静态来源必须继续拒绝:{asset_kind}/{media_type}"
);
}
// 400 错误体必须带上真实原因,客户端才能诊断(对应 AGC 的
// `editor_api_rejection_reason` 取值顺序修正)。
let error = ensure_editor_image_edit_source_kind_allowed_for_request(
Some("future-kind"),
Some("image"),
Some(generation_inputs),
)
.expect_err("未知类型必须拒绝");
let error =
ensure_editor_image_edit_source_kind_allowed(Some("future-kind"), Some("image"))
.expect_err("未知类型必须拒绝");
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
assert_eq!(
error.details().and_then(|details| details.get("provider")),
@@ -21035,7 +20925,9 @@ mod tests {
}
let mut forbidden_override = target.clone();
forbidden_override.effective_asset_kind = Some("icon".to_string());
// 用仍然被静态图表拒绝的类型,守卫意图不变:目标图层不能借壳成允许来源。
// `icon` 已是共享契约里的静态图 canonical 类型,不能再当反例。
forbidden_override.effective_asset_kind = Some("character-animation".to_string());
assert!(
ensure_editor_image_edit_target_matches_source(
"resource-source",