diff --git a/server-rs/crates/platform-tripo/src/common/validation.rs b/server-rs/crates/platform-tripo/src/common/validation.rs index b91972bc8..9630ee3ec 100644 --- a/server-rs/crates/platform-tripo/src/common/validation.rs +++ b/server-rs/crates/platform-tripo/src/common/validation.rs @@ -311,3 +311,249 @@ pub(crate) fn submitted_task_handle(task_id: String) -> Result TripoGenerationOptions { + TripoGenerationOptions { + model, + texture: None, + pbr: None, + texture_quality: None, + texture_version: None, + geometry_quality: None, + face_limit: None, + auto_size: None, + quad: None, + smart_low_poly: None, + generate_parts: None, + compress: None, + } + } + + /// 取出「被拒字段 + 拒绝原因」,用于断言错误确实来自预期的规则。 + fn rejection(error: TripoError) -> Option<(Option, TripoValidationReason)> { + match error { + TripoError::InvalidParameters { field, reason, .. } => Some((field, reason)), + _ => None, + } + } + + #[test] + fn minimal_options_pass_for_every_model_version() { + for model in Model3dModelVersion::ALL { + let options = minimal_options(*model); + if let Err(error) = validate_generation_options(&options) { + panic!("{model:?} 的最简选项应放行,实际被拒:{error}"); + } + } + } + + /// `texture_quality=fast` 必须显式配 `texture_version=v3.5-20260815`。 + #[test] + fn fast_texture_quality_requires_v35_texture_version() { + for texture_version in [ + None, + Some(Model3dTextureVersion::V30), + Some(Model3dTextureVersion::V25), + ] { + let mut options = minimal_options(Model3dModelVersion::H31); + options.texture = Some(true); + options.texture_quality = Some(Model3dTextureQuality::Fast); + options.texture_version = texture_version; + + let error = + validate_generation_options(&options).expect_err("fast 未配 v3.5 贴图版本应被拒"); + assert_eq!( + rejection(error), + Some(( + Some(TripoField::TextureQuality), + TripoValidationReason::InvalidCombination + )), + "texture_version={texture_version:?}" + ); + } + + let mut options = minimal_options(Model3dModelVersion::H31); + options.texture = Some(true); + options.texture_quality = Some(Model3dTextureQuality::Fast); + options.texture_version = Some(Model3dTextureVersion::V35); + validate_generation_options(&options).expect("fast + v3.5 应放行"); + } + + /// `generate_parts=true` 只在「白模 + 不叠几何模式」下成立。 + #[test] + fn generate_parts_requires_plain_geometry_options() { + let cases = [ + ( + "白模且无几何模式", + Some(false), + Some(false), + None, + None, + true, + ), + ("未显式 texture=false", None, Some(false), None, None, false), + ("texture=true", Some(true), Some(false), None, None, false), + ("未显式 pbr=false", Some(false), None, None, None, false), + ("pbr=true", Some(false), Some(true), None, None, false), + ("叠 quad", Some(false), Some(false), Some(true), None, false), + ( + "叠 smart_low_poly", + Some(false), + Some(false), + None, + Some(true), + false, + ), + ]; + + for (label, texture, pbr, quad, smart_low_poly, should_pass) in cases { + let mut options = minimal_options(Model3dModelVersion::H31); + options.texture = texture; + options.pbr = pbr; + options.quad = quad; + options.smart_low_poly = smart_low_poly; + options.generate_parts = Some(true); + + let result = validate_generation_options(&options); + assert_eq!( + result.is_ok(), + should_pass, + "{label} 的期望结果不符:{result:?}" + ); + if let Err(error) = result { + assert_eq!( + rejection(error), + Some(( + Some(TripoField::GenerateParts), + TripoValidationReason::InvalidCombination + )), + "{label}" + ); + } + } + } + + /// `quad=true` 只对 v3.x 家族与 P2 放行,其余家族按 Quad 字段拒绝。 + #[test] + fn quad_is_limited_to_v3_families_and_p2() { + for model in Model3dModelVersion::ALL { + let allowed = matches!( + model, + Model3dModelVersion::H31 | Model3dModelVersion::H30 | Model3dModelVersion::P2 + ); + let mut options = minimal_options(*model); + options.quad = Some(true); + + match validate_generation_options(&options) { + Ok(()) => assert!(allowed, "{model:?} 不应允许 quad=true"), + Err(error) => { + assert!(!allowed, "{model:?} 应允许 quad=true,实际被拒:{error}"); + assert_eq!( + rejection(error), + Some(( + Some(TripoField::Quad), + TripoValidationReason::InvalidCombination + )), + "{model:?}" + ); + } + } + } + } + + /// `face_limit` 边界:min-1 拒绝、min / max 放行、max+1 拒绝。 + #[test] + fn face_limit_uses_the_bounds_of_the_selected_mode() { + let cases = [ + ( + "H31 默认", + Model3dModelVersion::H31, + None, + None, + 1, + 1_500_000, + ), + ("H25 默认", Model3dModelVersion::H25, None, None, 1, 500_000), + ("P1 默认", Model3dModelVersion::P1, None, None, 50, 20_000), + ( + "P2 quad", + Model3dModelVersion::P2, + Some(true), + None, + 48, + 25_000, + ), + ( + "H31 智能低模", + Model3dModelVersion::H31, + None, + Some(true), + 500, + 20_000, + ), + ( + "H31 智能低模 + quad", + Model3dModelVersion::H31, + Some(true), + Some(true), + 500, + 10_000, + ), + ]; + + for (label, model, quad, smart_low_poly, minimum, maximum) in cases { + for (face_limit, should_pass) in [ + (minimum - 1, false), + (minimum, true), + (maximum, true), + (maximum + 1, false), + ] { + let mut options = minimal_options(model); + options.quad = quad; + options.smart_low_poly = smart_low_poly; + options.face_limit = Some(face_limit); + + let result = validate_generation_options(&options); + assert_eq!( + result.is_ok(), + should_pass, + "{label} face_limit={face_limit} 期望通过={should_pass},实际 {result:?}" + ); + if let Err(error) = result { + assert_eq!( + rejection(error), + Some(( + Some(TripoField::FaceLimit), + TripoValidationReason::OutOfRange + )), + "{label} face_limit={face_limit}" + ); + } + } + } + } + + /// 空白 task id 是 provider 违约,按 OutputSchema 失败而不是参数错误。 + #[test] + fn blank_submitted_task_id_is_an_output_schema_error() { + for blank in ["", " ", "\t\n"] { + let error = submitted_task_handle(blank.to_string()).expect_err("空白 task id 应被拒"); + assert!( + matches!(error, TripoError::OutputSchema { .. }), + "空白 task id 应按 OutputSchema 失败,实际:{error}" + ); + } + + assert_eq!( + submitted_task_handle(" task-1 ".to_string()) + .expect("非空 task id 应放行") + .task_id, + "task-1" + ); + } +}