Files
Genarrative/server-rs/crates/shared-contracts/tests/model3d_multiview_request_contract.rs
T
k88936 3af0d3d90b 契约枚举的线上取值只由serde定义
- shared-contracts 新增 wire_str 与 wire_str_lossy:线上取值只从 serde rename / rename_all 取,不再维护第二份字面量
- 删除模型版本、纹理质量、纹理版本、纹理对齐、几何质量、输入方向、导出方向、压缩共 8 个枚举上手工维护的 as_str
- platform-tripo 新增 common/wire.rs 负责契约枚举到 provider 请求字符串的转换,三个入口的 to_sdk_params 改走它并把失败归一成 provider 错误
- api-server 的定价文案与 job 模型版本取值改用 serde 取值,job 的 model_version 由 &'static str 改为 String
- 取值来源只剩 serde 一处,改 rename 不会再静默分叉
2026-09-21 19:46:43 +08:00

216 lines
7.1 KiB
Rust

use serde_json::json;
use shared_contracts::model3d::common::{
Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dInputOrientation,
Model3dModelVersion, Model3dTextureAlignment, Model3dTextureQuality, Model3dTextureVersion,
};
use shared_contracts::model3d::multiview_to_model::{
Model3dMultiviewInputs, Model3dMultiviewToModelRequest,
};
#[test]
fn multiview_inputs_accepts_declared_fields_and_camel_case_tag() {
let inputs: Model3dMultiviewInputs = serde_json::from_value(json!({
"kind": "views",
"front": "front.png",
"left": "left.png"
}))
.expect("已声明字段应可反序列化");
match inputs {
Model3dMultiviewInputs::Views { front, left, .. } => {
assert_eq!(front, "front.png");
assert_eq!(left.as_deref(), Some("left.png"));
}
Model3dMultiviewInputs::TaskId { .. } => panic!("kind=views 不应解成 taskId 变体"),
}
let task: Model3dMultiviewInputs = serde_json::from_value(json!({
"kind": "taskId",
"taskId": "tripo-task-1"
}))
.expect("taskId 变体使用 camelCase 字段名");
assert!(matches!(task, Model3dMultiviewInputs::TaskId { .. }));
}
#[test]
fn multiview_inputs_reject_unknown_fields_inside_variants() {
// enum 级 `deny_unknown_fields` 由 serde 生效,ts-rs 12 无法在 TS 绑定里表达;
// 这条用例钉住该严格性,防止为消除 ts-rs 告警而把它改成非严格模式。
let error = serde_json::from_value::<Model3dMultiviewInputs>(json!({
"kind": "views",
"front": "front.png",
"left": "left.png",
"back": "back.png",
"right": "right.png",
"unexpected": true
}))
.expect_err("变体里出现未知字段应被拒绝");
assert!(
error.to_string().contains("unknown field"),
"未知字段错误应指出 unknown field,实际为:{error}"
);
let error = serde_json::from_value::<Model3dMultiviewInputs>(json!({
"kind": "taskId",
"taskId": "tripo-task-1",
"unexpected": true
}))
.expect_err("taskId 变体里出现未知字段应被拒绝");
assert!(
error.to_string().contains("unknown field"),
"未知字段错误应指出 unknown field,实际为:{error}"
);
}
#[test]
fn multiview_request_rejects_unknown_top_level_fields() {
let error = serde_json::from_value::<Model3dMultiviewToModelRequest>(json!({
"inputs": { "kind": "taskId", "taskId": "tripo-task-1" },
"model": "v3.1-20260211",
"unexpected": true
}))
.expect_err("请求体顶层出现未知字段应被拒绝");
assert!(
error.to_string().contains("unknown field"),
"未知字段错误应指出 unknown field,实际为:{error}"
);
}
#[test]
fn multiview_request_round_trips_every_optional_field() {
// 全字段往返:钉住 camelCase 字段名、枚举线上取值与 Option 的可省略写法,
// 与生成的 TypeScript 绑定保持一致。
let payload = json!({
"inputs": {
"kind": "views",
"front": "front.png",
"left": "left.png",
"back": "back.png",
"right": "right.png"
},
"model": "v3.1-20260211",
"modelSeed": 7,
"textureSeed": 8,
"texture": true,
"pbr": true,
"textureQuality": "standard",
"textureVersion": "v3.0-20250812",
"delight": false,
"geometryQuality": "detailed",
"textureAlignment": "original_image",
"faceLimit": 20_000,
"autoSize": false,
"orientation": "align_image",
"quad": false,
"smartLowPoly": true,
"generateParts": false,
"compress": "geometry",
"exportUv": true,
"exportOrientation": "-y"
});
let request: Model3dMultiviewToModelRequest =
serde_json::from_value(payload.clone()).expect("全字段请求应可反序列化");
match &request.inputs {
Model3dMultiviewInputs::Views {
front,
left,
back,
right,
} => {
assert_eq!(front, "front.png");
assert_eq!(left.as_deref(), Some("left.png"));
assert_eq!(back.as_deref(), Some("back.png"));
assert_eq!(right.as_deref(), Some("right.png"));
}
Model3dMultiviewInputs::TaskId { .. } => panic!("kind=views 不应解成 taskId 变体"),
}
assert_eq!(request.model, Model3dModelVersion::H31);
assert_eq!(request.model_seed, Some(7));
assert_eq!(request.texture_seed, Some(8));
assert_eq!(request.texture, Some(true));
assert_eq!(request.pbr, Some(true));
assert_eq!(
request.texture_quality,
Some(Model3dTextureQuality::Standard)
);
assert_eq!(request.texture_version, Some(Model3dTextureVersion::V30));
assert_eq!(request.delight, Some(false));
assert_eq!(
request.geometry_quality,
Some(Model3dGeometryQuality::Detailed)
);
assert_eq!(
request.texture_alignment,
Some(Model3dTextureAlignment::OriginalImage)
);
assert_eq!(request.face_limit, Some(20_000));
assert_eq!(request.auto_size, Some(false));
assert_eq!(
request.orientation,
Some(Model3dInputOrientation::AlignImage)
);
assert_eq!(request.quad, Some(false));
assert_eq!(request.smart_low_poly, Some(true));
assert_eq!(request.generate_parts, Some(false));
assert_eq!(request.compress, Some(Model3dCompression::Geometry));
assert_eq!(request.export_uv, Some(true));
assert_eq!(
request.export_orientation,
Some(Model3dExportOrientation::MinusY)
);
assert_eq!(
serde_json::to_value(&request).expect("请求应可回写为 JSON"),
payload
);
}
#[test]
fn multiview_request_round_trips_explicit_nulls() {
// 显式 null 是可省略写法之外的另一种合法形态,回写时必须仍是 null 而不是缺字段。
let payload = json!({
"inputs": { "kind": "taskId", "taskId": "tripo-task-1" },
"model": "P2-20260801",
"modelSeed": null,
"textureSeed": null,
"texture": null,
"pbr": null,
"textureQuality": null,
"textureVersion": null,
"delight": null,
"geometryQuality": null,
"textureAlignment": null,
"faceLimit": null,
"autoSize": null,
"orientation": null,
"quad": null,
"smartLowPoly": null,
"generateParts": null,
"compress": null,
"exportUv": null,
"exportOrientation": null
});
let request: Model3dMultiviewToModelRequest =
serde_json::from_value(payload.clone()).expect("显式 null 的请求应可反序列化");
assert!(matches!(
request.inputs,
Model3dMultiviewInputs::TaskId { .. }
));
assert_eq!(request.model, Model3dModelVersion::P2);
assert_eq!(request.model_seed, None);
assert_eq!(request.export_orientation, None);
assert_eq!(
serde_json::to_value(&request).expect("请求应可回写为 JSON"),
payload
);
}