修复账号场景生成的幂等与结果返回协议
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m33s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m3s
Project CI / Backend tests (pull_request) Successful in 3m53s
Project CI / Frontend tests (pull_request) Successful in 2m2s
Project CI / Native shell tests (pull_request) Successful in 5m58s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m10s
Project CI / Repository checks (pull_request) Successful in 1m55s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m26s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m0s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m33s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m3s
Project CI / Backend tests (pull_request) Successful in 3m53s
Project CI / Frontend tests (pull_request) Successful in 2m2s
Project CI / Native shell tests (pull_request) Successful in 5m58s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m10s
Project CI / Repository checks (pull_request) Successful in 1m55s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m26s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m0s
站内场景入口读取并透传可选幂等键,复用既有队列去重逻辑 场景配方保留 AGC 来源标记,恢复可下载的队列完成结果 补充非法幂等键、场景结果序列化和幂等命名空间回归测试 同步 OpenAPI、技术方案、实施计划及排障记录
This commit is contained in:
@@ -2749,6 +2749,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn editor_scene_generation_rejects_invalid_idempotency_key_before_queueing() {
|
||||
let state = AppState::new(AppConfig {
|
||||
external_generation_mode: ExternalGenerationMode::Queue,
|
||||
..AppConfig::default()
|
||||
})
|
||||
.expect("state should build");
|
||||
let seed_user = seed_phone_user_with_password(&state, "13800138232", TEST_PASSWORD).await;
|
||||
let token = sign_test_user_token(&state, &seed_user, "sess_editor_scene_idempotency");
|
||||
state.fail_test_editor_generation_enqueue();
|
||||
let app = build_router(state.clone());
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/editor/scenes/generations")
|
||||
.header("authorization", format!("Bearer {token}"))
|
||||
.header("content-type", "application/json")
|
||||
.header("idempotency-key", "contains space")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"sceneContent": "雨夜小镇",
|
||||
"stylePreset": "anime",
|
||||
"generationInputs": { "source": "ai-game-creator-client" },
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("request should complete");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
assert_eq!(state.test_editor_generation_enqueue_attempts(), 0);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
assert!(String::from_utf8_lossy(&body).contains("Idempotency-Key"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn editor_scene_generation_rejects_inline_data_url_before_queueing() {
|
||||
let state = AppState::new(AppConfig {
|
||||
|
||||
@@ -826,6 +826,19 @@ mod tests {
|
||||
editor_generation_idempotency_namespace(&game_creator),
|
||||
GAME_CREATOR_CLIENT_GENERATION_DEDUPE_PREFIX
|
||||
);
|
||||
let scene = crate::editor_project::build_editor_scene_image_generation_payload(
|
||||
serde_json::from_value(json!({
|
||||
"sceneContent": "雨夜小镇",
|
||||
"stylePreset": "anime",
|
||||
"generationInputs": { "source": GAME_CREATOR_CLIENT_GENERATION_SOURCE },
|
||||
}))
|
||||
.expect("scene request should deserialize"),
|
||||
)
|
||||
.expect("scene payload should build");
|
||||
assert_eq!(
|
||||
editor_generation_idempotency_namespace(&scene),
|
||||
GAME_CREATOR_CLIENT_GENERATION_DEDUPE_PREFIX,
|
||||
);
|
||||
assert_eq!(
|
||||
editor_generation_idempotency_namespace(&ordinary),
|
||||
EXTERNAL_API_GENERATION_DEDUPE_PREFIX
|
||||
|
||||
@@ -2658,12 +2658,23 @@ fn build_editor_scene_generation_inputs(
|
||||
{
|
||||
fields.push(json!({ "id": "customStyle", "title": "自定义画风", "value": custom_style }));
|
||||
}
|
||||
json!({
|
||||
let mut inputs = json!({
|
||||
"version": 2,
|
||||
"action": "scene.generate",
|
||||
"fields": fields,
|
||||
"references": references,
|
||||
})
|
||||
});
|
||||
// AGC 账号任务依赖来源标记选择幂等命名空间和可下载的队列结果;其余字段仍由服务端重建。
|
||||
if payload
|
||||
.generation_inputs
|
||||
.as_ref()
|
||||
.and_then(|inputs| inputs.get("source"))
|
||||
.and_then(Value::as_str)
|
||||
== Some(GAME_CREATOR_CLIENT_GENERATION_SOURCE)
|
||||
{
|
||||
inputs["source"] = json!(GAME_CREATOR_CLIENT_GENERATION_SOURCE);
|
||||
}
|
||||
inputs
|
||||
}
|
||||
|
||||
fn normalize_editor_scene_optional_text<'a>(value: Option<&'a str>, default: &'a str) -> &'a str {
|
||||
@@ -2739,9 +2750,11 @@ pub async fn generate_editor_scene(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
headers: HeaderMap,
|
||||
payload: Result<Json<EditorSceneGenerateRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let Json(payload) = parse_editor_generation_json_payload(payload)?;
|
||||
let idempotency_key = optional_editor_idempotency_key(&headers)?;
|
||||
let image_payload = build_editor_scene_image_generation_payload(payload)?;
|
||||
let caller = EditorGenerationCaller::from_authenticated(&authenticated);
|
||||
if !state.config.external_generation_mode.is_inline() {
|
||||
@@ -2750,7 +2763,7 @@ pub async fn generate_editor_scene(
|
||||
&request_context,
|
||||
&caller,
|
||||
image_payload,
|
||||
None,
|
||||
idempotency_key,
|
||||
)
|
||||
.await?;
|
||||
return Ok(json_success_body(
|
||||
@@ -13316,6 +13329,57 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_generation_preserves_agc_downloadable_queue_result() {
|
||||
let request = serde_json::from_value::<EditorSceneGenerateRequest>(json!({
|
||||
"sceneContent": "雨夜小镇",
|
||||
"stylePreset": "anime",
|
||||
"referenceImageSrcs": ["art-spec-resource"],
|
||||
"generationInputs": {
|
||||
"source": GAME_CREATOR_CLIENT_GENERATION_SOURCE,
|
||||
"action": "client-supplied-action",
|
||||
"fields": [{ "id": "prompt", "value": "不可信配方" }],
|
||||
"references": [{ "refId": "untrusted-resource" }],
|
||||
},
|
||||
}))
|
||||
.expect("scene request should deserialize");
|
||||
let mut image_payload = build_editor_scene_image_generation_payload(request)
|
||||
.expect("scene payload should build");
|
||||
image_payload.generation_inputs =
|
||||
sanitize_editor_queued_generation_inputs(image_payload.generation_inputs);
|
||||
let inputs = image_payload.generation_inputs.as_ref().unwrap();
|
||||
assert_eq!(inputs["source"], GAME_CREATOR_CLIENT_GENERATION_SOURCE);
|
||||
assert_eq!(inputs["action"], "scene.generate");
|
||||
assert_eq!(inputs["fields"][0]["value"], "雨夜小镇");
|
||||
assert_eq!(inputs["references"], json!([{ "id": "reference" }]));
|
||||
|
||||
let mut job = atomic_editor_generation_job_fixture();
|
||||
job.request_payload_json = serde_json::to_string(&image_payload).unwrap();
|
||||
let context = EditorGenerationQueueResultContext::from_job(&job);
|
||||
assert_eq!(
|
||||
context.consumer,
|
||||
EditorGenerationQueueConsumer::GameCreatorResourceEditor
|
||||
);
|
||||
let result: Value = serde_json::from_str(
|
||||
&serialize_atomic_editor_generation_job_result(
|
||||
&context,
|
||||
&json!({
|
||||
"ok": true,
|
||||
"objectKey": "generated/scene.png",
|
||||
"resource": {
|
||||
"resourceId": "scene-resource",
|
||||
"objectKey": "generated/scene.png",
|
||||
"assetObjectId": "scene-object",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.expect("AGC scene result should serialize"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result["result"]["objectKey"], "generated/scene.png");
|
||||
assert_eq!(result["result"]["resource"]["resourceId"], "scene-resource");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_removal_options_preserve_queue_parameters_and_legacy_identity() {
|
||||
for (fields, mode, color) in [
|
||||
|
||||
Reference in New Issue
Block a user