f198b7a6d0
- 契约新增 Model3dViewInput(url / fileToken 的 tagged enum),views 的四个视图都改用它,裸字符串在契约层即被拒绝
- platform-tripo 不再走 SDK 的位置数组形态:按 Tripo 文档推荐的 view-key 构造 inputs,值写成显式 {url} / {file_token},种类由契约字段决定、链路上不再猜前缀
- 视图校验(front 必填、至少两张、空白按未提供)与 inputs 构造合并成一条路径,不再两处重复规则
- 新增 4 条定向用例钉住发出的 JSON 形状、空白视图、视图数量与 taskId 复用;契约用例补齐裸字符串与视图内未知字段被拒
- 重新生成 ts-rs 绑定与导出索引,技术方案同步 multiview 输入口径
276 lines
9.2 KiB
Rust
276 lines
9.2 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, Model3dViewInput,
|
|
};
|
|
|
|
#[test]
|
|
fn multiview_inputs_accepts_declared_fields_and_camel_case_tag() {
|
|
let inputs: Model3dMultiviewInputs = serde_json::from_value(json!({
|
|
"kind": "views",
|
|
"front": { "kind": "url", "url": "front.png" },
|
|
"left": { "kind": "fileToken", "fileToken": "left-token" }
|
|
}))
|
|
.expect("已声明字段应可反序列化");
|
|
|
|
match inputs {
|
|
Model3dMultiviewInputs::Views { front, left, .. } => {
|
|
assert_eq!(
|
|
front,
|
|
Model3dViewInput::Url {
|
|
url: "front.png".to_string()
|
|
}
|
|
);
|
|
assert_eq!(
|
|
left,
|
|
Some(Model3dViewInput::FileToken {
|
|
file_token: "left-token".to_string()
|
|
})
|
|
);
|
|
}
|
|
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": { "kind": "url", "url": "front.png" },
|
|
"left": { "kind": "url", "url": "left.png" },
|
|
"back": { "kind": "url", "url": "back.png" },
|
|
"right": { "kind": "url", "url": "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": { "kind": "url", "url": "front.png" },
|
|
"left": { "kind": "fileToken", "fileToken": "left-token" },
|
|
"back": { "kind": "url", "url": "back.png" },
|
|
"right": { "kind": "fileToken", "fileToken": "right-token" }
|
|
},
|
|
"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,
|
|
&Model3dViewInput::Url {
|
|
url: "front.png".to_string()
|
|
}
|
|
);
|
|
assert_eq!(
|
|
left,
|
|
&Some(Model3dViewInput::FileToken {
|
|
file_token: "left-token".to_string()
|
|
})
|
|
);
|
|
assert_eq!(
|
|
back,
|
|
&Some(Model3dViewInput::Url {
|
|
url: "back.png".to_string()
|
|
})
|
|
);
|
|
assert_eq!(
|
|
right,
|
|
&Some(Model3dViewInput::FileToken {
|
|
file_token: "right-token".to_string()
|
|
})
|
|
);
|
|
}
|
|
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
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn multiview_views_require_typed_inputs_without_unknown_fields() {
|
|
// 裸字符串只能靠前缀猜种类(URL / file_token / task_id),契约层直接拒绝,
|
|
// 逼调用方写明是 `url` 还是 `fileToken`。
|
|
let error = serde_json::from_value::<Model3dMultiviewInputs>(json!({
|
|
"kind": "views",
|
|
"front": "front.png",
|
|
"left": "left.png"
|
|
}))
|
|
.expect_err("裸字符串视图应被拒绝");
|
|
|
|
assert!(
|
|
error.to_string().contains("invalid type"),
|
|
"裸字符串应报类型错误,实际为:{error}"
|
|
);
|
|
|
|
// 每个视图只允许一种取值:kind=url 时再给 fileToken 会被拒绝。
|
|
let error = serde_json::from_value::<Model3dMultiviewInputs>(json!({
|
|
"kind": "views",
|
|
"front": { "kind": "url", "url": "front.png", "fileToken": "front-token" },
|
|
"left": { "kind": "fileToken", "fileToken": "left-token" }
|
|
}))
|
|
.expect_err("视图取值里出现未知字段应被拒绝");
|
|
|
|
assert!(
|
|
error.to_string().contains("unknown field"),
|
|
"未知字段错误应指出 unknown field,实际为:{error}"
|
|
);
|
|
}
|