From 83913785aeaa9888dd2e793b2c2c9390d75e8d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 23 Sep 2026 12:12:26 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=20Tripo=203D=20=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=98=A0=E5=B0=84=E7=9A=84=E5=89=A9=E4=BD=99=E5=88=86?= =?UTF-8?q?=E6=94=AF=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增补 SDK 参数错误、无状态码 Api、Api 5xx、Sdk 兜底与传输错误的 502 用例,断言只回固定文案、不回显上游报文与 task id - 增补 map_request_error 的字段口径用例:平台字段路径、provider 字段名与字段未知时不写 field - 增补 map_pricing_error / map_pricing_store_error / oss_unavailable 的状态码、provider 与 reason 断言 --- .../crates/api-server/src/tripo3d/errors.rs | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/server-rs/crates/api-server/src/tripo3d/errors.rs b/server-rs/crates/api-server/src/tripo3d/errors.rs index dde87177e..6b5b5e871 100644 --- a/server-rs/crates/api-server/src/tripo3d/errors.rs +++ b/server-rs/crates/api-server/src/tripo3d/errors.rs @@ -130,6 +130,13 @@ pub(crate) fn oss_unavailable(message: &str) -> AppError { mod tests { use super::*; + fn detail(error: &AppError, key: &str) -> Option { + error + .details() + .and_then(|details| details.get(key)) + .cloned() + } + #[test] fn provider_parameter_errors_map_to_bad_request() { let error = map_provider_error(TripoError::InvalidParameters { @@ -202,4 +209,138 @@ mod tests { } } } + + /// SDK 参数错误与 provider 4xx 同档:都是「调用方参数不对」,不该报 5xx。 + #[test] + fn provider_sdk_invalid_argument_maps_to_bad_request() { + let error = map_provider_error(TripoError::SdkInvalidArgument( + "unknown argument: secret-sdk-argument".to_string(), + )); + assert_eq!(error.status_code(), StatusCode::BAD_REQUEST); + assert_eq!( + detail(&error, "reason"), + Some(json!("tripo-invalid-parameters")) + ); + } + + /// 没有可用状态码的 Api 错误、SDK 兜底错误与传输错误一律 502,且只回固定文案: + /// 上游报文与 task id 都不允许出现在响应里。 + #[test] + fn provider_unknown_upstream_errors_use_fixed_text() { + let cases = [ + ( + "api-without-status", + TripoError::Api { + code: 1001, + message: Some("secret-api-message".to_string()), + suggestion: None, + status: None, + }, + ), + ( + "api-5xx", + TripoError::Api { + code: 1001, + message: Some("secret-api-message".to_string()), + suggestion: None, + status: Some(500), + }, + ), + ( + "sdk-fallback", + TripoError::Sdk { + message: "secret-sdk-message".to_string(), + }, + ), + ( + "request", + TripoError::Request { + message: "secret-request-message".to_string(), + status: None, + }, + ), + ]; + for (name, case) in cases { + let error = map_provider_error(case); + assert_eq!(error.status_code(), StatusCode::BAD_GATEWAY, "{name}"); + assert_eq!( + detail(&error, "reason"), + Some(json!("tripo-upstream-error")), + "{name}" + ); + let serialized = serde_json::to_string(error.details().expect("details 必须存在")) + .expect("details 必须可序列化"); + for leaked in ["secret-", "task-"] { + assert!(!serialized.contains(leaked), "{name}: {serialized}"); + } + } + } + + /// 平台规则直接给字段路径;provider 预检失败沿用 provider 字段名;字段未知时不写 field 键。 + #[test] + fn request_errors_report_field_only_when_known() { + let platform = map_request_error(Model3dRequestError::InvalidRequest { + field: "prompt", + message: "prompt is required".to_string(), + }); + assert_eq!(platform.status_code(), StatusCode::BAD_REQUEST); + assert_eq!( + detail(&platform, "reason"), + Some(json!("invalid-model3d-request")) + ); + assert_eq!(detail(&platform, "field"), Some(json!("prompt"))); + + let provider = map_request_error(Model3dRequestError::Provider( + TripoError::InvalidParameters { + field: Some(platform_tripo::TripoField::GeometryQuality), + reason: platform_tripo::TripoValidationReason::InvalidCombination, + message: "geometry_quality is not supported".to_string(), + }, + )); + assert_eq!(provider.status_code(), StatusCode::BAD_REQUEST); + assert_eq!(detail(&provider, "field"), Some(json!("geometry_quality"))); + + let unknown = map_request_error(Model3dRequestError::Provider(TripoError::Request { + message: "transport".to_string(), + status: None, + })); + assert_eq!(unknown.status_code(), StatusCode::BAD_REQUEST); + assert_eq!(detail(&unknown, "field"), None); + } + + /// 定价未配置属部署口径:503 且原因可被后台区分,不能混成 500。 + #[test] + fn pricing_not_configured_is_service_unavailable() { + let error = map_pricing_error(Model3dPricingError::NotConfigured); + assert_eq!(error.status_code(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!( + detail(&error, "reason"), + Some(json!("model3d-pricing-unavailable")) + ); + } + + /// 定价快照读取失败属服务端问题:500,且沿用全仓库定价错误的 provider 口径。 + #[test] + fn pricing_store_failures_are_internal_errors() { + let error = map_pricing_store_error(EditorGenerationPricingError::Model3d( + Model3dPricingError::NotConfigured, + )); + assert_eq!(error.status_code(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!( + detail(&error, "provider"), + Some(json!("editor-generation-pricing")) + ); + assert_eq!( + detail(&error, "reason"), + Some(json!("model3d-pricing-store-error")) + ); + } + + #[test] + fn oss_unavailable_is_service_unavailable() { + let error = oss_unavailable("OSS 未完成环境变量配置,无法写入 3D 产物。"); + assert_eq!(error.status_code(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!(detail(&error, "provider"), Some(json!("aliyun-oss"))); + assert_eq!(detail(&error, "reason"), Some(json!("oss-unavailable"))); + } }