修正 AGC 图片快速编辑的 400 判据测试:补 art-spritesheet / ui 正向用例并钉住请求级链路
- 服务端白名单用例改为按静态图集合断言,新增 `art-spritesheet` / `ui` / `ui-prototype` / `game-art` / `game-background` / `art-spritesheet-slice` 正向用例 - 保留 `future-kind` / `video` / `audio` / `image-sequence` / `character-animation` / `document` / `code` 负向用例,并补 mediaType AND 门用例 - 新增请求级回归用例 `game_creator_client_quick_edit_accepts_local_manifest_static_image_kinds`,按 AGC 客户端真实请求体(source 已改写为 ai-game-creator-client)覆盖放行与拒绝两侧 - 新增 `game_creator_client_source_is_recognized_as_game_creator_resource_editor`,把 source 别名与队列 consumer 判定钉在同口径 - 修正 icon 相关旧断言:`icon` 已是共享契约静态图类型,改断言 non-static 媒体仍被拒
This commit is contained in:
@@ -5475,6 +5475,102 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// 平台把 4xx 兜底成 `请求参数不合法`,具体原因写在 `details.message`。
|
||||
/// 只读 `error.message` 会让用户永远看不到真实原因,因此这里把取值顺序钉在测试里。
|
||||
#[test]
|
||||
fn editor_api_rejection_reason_prefers_details_message_and_keeps_context() {
|
||||
assert_eq!(
|
||||
editor_api_rejection_reason_from_body(
|
||||
serde_json::json!({
|
||||
"error": {
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "请求参数不合法",
|
||||
"details": {
|
||||
"provider": "editor-image-edit",
|
||||
"message": "当前素材类型不支持图片快速编辑",
|
||||
"assetKind": "art-spritesheet",
|
||||
"mediaType": "image",
|
||||
},
|
||||
}
|
||||
})
|
||||
.to_string()
|
||||
.as_str(),
|
||||
),
|
||||
Some(
|
||||
"当前素材类型不支持图片快速编辑|provider editor-image-edit|素材类型 art-spritesheet|媒体类型 image"
|
||||
.to_string()
|
||||
),
|
||||
"details.message 必须优先于通用 error.message,并附上 provider 与类型上下文"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
editor_api_rejection_reason_from_body(
|
||||
serde_json::json!({
|
||||
"error": {
|
||||
"code": "UPSTREAM_FAILED",
|
||||
"message": "上游服务请求失败",
|
||||
"details": { "message": "Provider 返回 429", "provider": "editor-image-edit" },
|
||||
}
|
||||
})
|
||||
.to_string()
|
||||
.as_str(),
|
||||
),
|
||||
Some("Provider 返回 429|provider editor-image-edit|错误码 UPSTREAM_FAILED".to_string()),
|
||||
"非 BAD_REQUEST 的稳定码照旧透出"
|
||||
);
|
||||
|
||||
// 顶层 details(没有 error.details 的错误体)同样优先。
|
||||
assert_eq!(
|
||||
editor_api_rejection_reason_from_body(
|
||||
serde_json::json!({ "details": { "message": "具体原因" }, "message": "通用文案" })
|
||||
.to_string()
|
||||
.as_str(),
|
||||
),
|
||||
Some("具体原因".to_string())
|
||||
);
|
||||
|
||||
// 只有 error.message 时退回它;error 是字符串时按文本兜底。
|
||||
assert_eq!(
|
||||
editor_api_rejection_reason_from_body(
|
||||
serde_json::json!({ "error": { "code": "BAD_REQUEST", "message": "请求参数不合法" } })
|
||||
.to_string()
|
||||
.as_str(),
|
||||
),
|
||||
Some("请求参数不合法".to_string()),
|
||||
"BAD_REQUEST 不重复透出错误码"
|
||||
);
|
||||
assert_eq!(
|
||||
editor_api_rejection_reason_from_body(serde_json::json!({ "error": "直白文本" }).to_string().as_str()),
|
||||
Some("直白文本".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
editor_api_rejection_reason_from_body(
|
||||
serde_json::json!({ "message": "顶层文案" }).to_string().as_str()
|
||||
),
|
||||
Some("顶层文案".to_string())
|
||||
);
|
||||
|
||||
// 空值与非 JSON 正文不给用户编造原因。
|
||||
assert_eq!(
|
||||
editor_api_rejection_reason_from_body(
|
||||
serde_json::json!({ "error": { "message": " " } }).to_string().as_str()
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(editor_api_rejection_reason_from_body("<html>502</html>"), None);
|
||||
assert_eq!(editor_api_rejection_reason_from_body(""), None);
|
||||
|
||||
// 超长正文仍按 200 字符截断,避免把整段响应体塞进用户可见文案。
|
||||
let long = "长".repeat(400);
|
||||
let reason = editor_api_rejection_reason_from_body(
|
||||
serde_json::json!({ "error": { "details": { "message": long.clone() } } })
|
||||
.to_string()
|
||||
.as_str(),
|
||||
)
|
||||
.expect("超长正文仍应给出原因");
|
||||
assert_eq!(reason, "长".repeat(200));
|
||||
}
|
||||
|
||||
async fn with_test_external_editor_credentials<T>(
|
||||
api_base_url: &str,
|
||||
api_key: &str,
|
||||
|
||||
@@ -20602,24 +20602,44 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_image_edit_source_kind_whitelist_covers_current_and_unknown_types() {
|
||||
fn editor_image_edit_source_kind_whitelist_covers_static_image_kinds_only() {
|
||||
let cases = [
|
||||
(None, None, true),
|
||||
(None, Some("image"), true),
|
||||
(Some("image"), Some("image"), true),
|
||||
(Some("spec"), Some("image"), true),
|
||||
(Some("character"), Some("image"), true),
|
||||
(Some("icon"), Some("image"), true),
|
||||
(Some("icon-spritesheet"), Some("image"), true),
|
||||
(Some("icon-spec"), Some("image"), true),
|
||||
(Some("publication-material"), Some("image"), true),
|
||||
(Some("ui-design"), Some("image"), true),
|
||||
(Some("scene"), Some("image"), true),
|
||||
(Some("icon"), Some("image"), false),
|
||||
// `canonicalGameCreationAppAssetKind()` 的合法输入:平台按本地 manifest 原始类型
|
||||
// 登记来源资源时会出现这些 legacy 值,实测 `art-spritesheet` / `ui` 曾整片 400。
|
||||
(Some("art-spritesheet"), Some("image"), true),
|
||||
(Some("art-spritesheet-slice"), Some("image"), true),
|
||||
(Some("ui"), Some("image"), true),
|
||||
(Some("ui-prototype"), Some("image"), true),
|
||||
(Some("game-art"), Some("image"), true),
|
||||
(Some("game-background"), Some("image"), true),
|
||||
(Some("character-art"), Some("image"), true),
|
||||
(Some("illustration"), Some("image"), true),
|
||||
// 非静态媒体与非静态类型必须继续拒绝。
|
||||
(Some("character-animation"), Some("image-sequence"), false),
|
||||
(Some("sound-effect"), Some("audio"), false),
|
||||
(Some("background-music"), Some("audio"), false),
|
||||
(Some("video"), Some("video"), false),
|
||||
(Some("audio"), Some("audio"), false),
|
||||
(Some("document"), Some("application/pdf"), false),
|
||||
(Some("code"), Some("text/plain"), false),
|
||||
(Some("future-asset-kind"), Some("image"), false),
|
||||
(None, Some("future-media-type"), false),
|
||||
// mediaType 是 AND 门:图片类 assetKind 挂非静态媒体照旧拒绝。
|
||||
(Some("art-spritesheet"), Some("video"), false),
|
||||
(Some("ui"), Some("audio"), false),
|
||||
(Some("icon"), Some("image-sequence"), false),
|
||||
(Some("scene"), Some("image-sequence"), false),
|
||||
];
|
||||
for (asset_kind, media_type, expected_allowed) in cases {
|
||||
let result = ensure_editor_image_edit_source_kind_allowed(asset_kind, media_type);
|
||||
@@ -20630,8 +20650,8 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
let error = ensure_editor_image_edit_source_kind_allowed(Some("icon"), Some("image"))
|
||||
.expect_err("individual icon assets should not support quick edit");
|
||||
let error = ensure_editor_image_edit_source_kind_allowed(Some("art-spritesheet"), Some("video"))
|
||||
.expect_err("非静态媒体即使挂着图片类 assetKind 也不得支持快速编辑");
|
||||
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("message")),
|
||||
@@ -20677,7 +20697,15 @@ mod tests {
|
||||
icon_source.effective_asset_kind.as_deref(),
|
||||
Some(icon_source.media_type.as_str()),
|
||||
)
|
||||
.is_err()
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
ensure_editor_image_edit_source_kind_allowed(
|
||||
icon_source.effective_asset_kind.as_deref(),
|
||||
Some("image-sequence"),
|
||||
)
|
||||
.is_err(),
|
||||
"单张 icon 可快速编辑,但同一类型挂非静态媒体仍必须拒绝"
|
||||
);
|
||||
let icon_spec_source = resolve_editor_image_edit_target_layer_source(
|
||||
"bucket",
|
||||
@@ -20747,7 +20775,8 @@ mod tests {
|
||||
Some("image"),
|
||||
Some(&json!({ "source": "editor-agent" })),
|
||||
)
|
||||
.is_err()
|
||||
.is_ok(),
|
||||
"icon 已是共享契约里的静态图 canonical 类型,与 generationInputs.source 无关"
|
||||
);
|
||||
assert!(
|
||||
ensure_editor_image_edit_source_kind_allowed_for_request(
|
||||
@@ -20767,6 +20796,139 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// 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)。这条路径必须放行;
|
||||
/// 非静态媒体(video/audio/image-sequence)必须继续拒绝。
|
||||
#[test]
|
||||
fn game_creator_client_quick_edit_accepts_local_manifest_static_image_kinds() {
|
||||
// 与客户端 `submit_resource_edit_remote` 发送的 JSON 同形:请求体的
|
||||
// `generationInputs` 就是这里传入的判定输入。
|
||||
let agc_payload: Value = serde_json::from_str(
|
||||
r#"{
|
||||
"prompt": "把这张图改成夜间配色",
|
||||
"sourceReferenceId": "resource-art-spritesheet",
|
||||
"generationInputs": {
|
||||
"source": "ai-game-creator-client",
|
||||
"assetKind": "art-spritesheet"
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("AGC quick edit payload should be JSON");
|
||||
let generation_inputs = agc_payload
|
||||
.pointer("/generationInputs")
|
||||
.expect("AGC payload should carry generationInputs");
|
||||
|
||||
for (asset_kind, media_type) in [
|
||||
("art-spritesheet", "image"),
|
||||
("ui", "image"),
|
||||
("ui-prototype", "image"),
|
||||
("game-art", "image"),
|
||||
("game-background", "image"),
|
||||
("icon", "image"),
|
||||
] {
|
||||
assert!(
|
||||
ensure_editor_image_edit_source_kind_allowed_for_request(
|
||||
Some(asset_kind),
|
||||
Some(media_type),
|
||||
Some(generation_inputs),
|
||||
)
|
||||
.is_ok(),
|
||||
"AGC 快速编辑必须放行 {asset_kind}/{media_type}"
|
||||
);
|
||||
}
|
||||
|
||||
for (asset_kind, media_type) in [
|
||||
("art-spritesheet", "video"),
|
||||
("ui", "audio"),
|
||||
("video", "video"),
|
||||
("sound-effect", "audio"),
|
||||
("background-music", "audio"),
|
||||
("character-animation", "image-sequence"),
|
||||
("future-kind", "image"),
|
||||
] {
|
||||
assert!(
|
||||
ensure_editor_image_edit_source_kind_allowed_for_request(
|
||||
Some(asset_kind),
|
||||
Some(media_type),
|
||||
Some(generation_inputs),
|
||||
)
|
||||
.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("未知类型必须拒绝");
|
||||
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("provider")),
|
||||
Some(&json!("editor-image-edit"))
|
||||
);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("message")),
|
||||
Some(&json!("当前素材类型不支持图片快速编辑"))
|
||||
);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("assetKind")),
|
||||
Some(&json!("future-kind"))
|
||||
);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("mediaType")),
|
||||
Some(&json!("image"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_image_edit_target_binding_prefers_object_id_and_fails_closed() {
|
||||
let target = EditorImageEditTargetLayerSource {
|
||||
|
||||
Reference in New Issue
Block a user