diff --git a/docs/openapi/genarrative-external-v1.openapi.json b/docs/openapi/genarrative-external-v1.openapi.json index df02f1edd..92732c25a 100644 --- a/docs/openapi/genarrative-external-v1.openapi.json +++ b/docs/openapi/genarrative-external-v1.openapi.json @@ -2064,11 +2064,31 @@ ] }, "then": { + "required": [ + "imageSequenceFrames", + "imageSequenceDurationMs" + ], "properties": { "generationInputs": { "$ref": "#/components/schemas/ExternalCharacterAnimationGenerationInputs" } } + }, + "else": { + "not": { + "anyOf": [ + { + "required": [ + "imageSequenceFrames" + ] + }, + { + "required": [ + "imageSequenceDurationMs" + ] + } + ] + } } } ], @@ -2158,6 +2178,7 @@ }, "imageSequenceFrames": { "type": "array", + "minItems": 2, "items": { "$ref": "#/components/schemas/EditorImageSequenceFrame" } @@ -2189,11 +2210,31 @@ ] }, "then": { + "required": [ + "imageSequenceFrames", + "imageSequenceDurationMs" + ], "properties": { "generationInputs": { "$ref": "#/components/schemas/ExternalCharacterAnimationGenerationInputs" } } + }, + "else": { + "not": { + "anyOf": [ + { + "required": [ + "imageSequenceFrames" + ] + }, + { + "required": [ + "imageSequenceDurationMs" + ] + } + ] + } } } ], @@ -2275,6 +2316,7 @@ }, "imageSequenceFrames": { "type": "array", + "minItems": 2, "items": { "$ref": "#/components/schemas/EditorImageSequenceFrame" } diff --git a/server-rs/crates/api-server/src/external_editor_api.rs b/server-rs/crates/api-server/src/external_editor_api.rs index c0076d7d4..10baecb1f 100644 --- a/server-rs/crates/api-server/src/external_editor_api.rs +++ b/server-rs/crates/api-server/src/external_editor_api.rs @@ -1021,6 +1021,11 @@ fn serialize_external_editor_generation_inputs( mod tests { use super::*; + const EXTERNAL_MEDIA_CREATE_REQUEST_SCHEMAS: [&str; 2] = [ + "ExternalEditorAssetCreateRequest", + "ExternalEditorProjectResourceCreateRequest", + ]; + fn external_generation_job_fixture(status: &str) -> ExternalGenerationJobRecord { ExternalGenerationJobRecord { job_id: "task-external-test".to_string(), @@ -1246,6 +1251,62 @@ mod tests { assert!(!unavailable.body_text().contains("ConnectDropped")); } + #[test] + fn external_openapi_character_animation_create_requires_complete_sequence_metadata() { + let parsed: Value = serde_json::from_str(OPENAPI_JSON).expect("openapi json should parse"); + + for request_schema_name in EXTERNAL_MEDIA_CREATE_REQUEST_SCHEMAS { + let request_schema = &parsed["components"]["schemas"][request_schema_name]; + let conditional = &request_schema["allOf"][0]; + assert_eq!( + conditional["then"]["required"], + json!(["imageSequenceFrames", "imageSequenceDurationMs"]), + "{request_schema_name}" + ); + assert_eq!( + request_schema["properties"]["imageSequenceFrames"]["minItems"], + json!(2), + "{request_schema_name}" + ); + assert_eq!( + request_schema["properties"]["imageSequenceDurationMs"]["minimum"], + json!(1), + "{request_schema_name}" + ); + assert_eq!( + conditional["then"]["properties"]["generationInputs"]["$ref"], + "#/components/schemas/ExternalCharacterAnimationGenerationInputs", + "{request_schema_name}" + ); + } + } + + #[test] + fn external_openapi_non_animation_create_forbids_sequence_metadata() { + let parsed: Value = serde_json::from_str(OPENAPI_JSON).expect("openapi json should parse"); + + for request_schema_name in EXTERNAL_MEDIA_CREATE_REQUEST_SCHEMAS { + let request_schema = &parsed["components"]["schemas"][request_schema_name]; + let conditional = &request_schema["allOf"][0]; + assert_eq!( + conditional["if"]["required"], + json!(["assetKind"]), + "{request_schema_name}" + ); + let forbidden_fields = conditional["else"]["not"]["anyOf"] + .as_array() + .expect("非角色动作分支必须逐项禁止序列字段") + .iter() + .filter_map(|schema| schema["required"][0].as_str()) + .collect::>(); + assert_eq!( + forbidden_fields, + ["imageSequenceFrames", "imageSequenceDurationMs"], + "{request_schema_name}" + ); + } + } + #[test] fn exported_openapi_json_contains_external_editor_routes_and_security() { let parsed: Value = serde_json::from_str(OPENAPI_JSON).expect("openapi json should parse"); @@ -1270,19 +1331,12 @@ mod tests { .collect::>(), legacy_runtime_fields ); - for request_schema in [ - "ExternalEditorAssetCreateRequest", - "ExternalEditorProjectResourceCreateRequest", - ] { - let conditional = &parsed["components"]["schemas"][request_schema]["allOf"][0]; + for request_schema in EXTERNAL_MEDIA_CREATE_REQUEST_SCHEMAS { assert_eq!( - conditional["if"]["properties"]["assetKind"]["const"], + parsed["components"]["schemas"][request_schema]["allOf"][0]["if"]["properties"]["assetKind"] + ["const"], "character-animation" ); - assert_eq!( - conditional["then"]["properties"]["generationInputs"]["$ref"], - "#/components/schemas/ExternalCharacterAnimationGenerationInputs" - ); } assert!( parsed["paths"]