fd48829c24
- shared-contracts 的 text-to-model / image-to-model 请求用平坦可选 projectId / canvasCompletion / assetFolderId / assetLabel 取代 tagged enum target,删除 Model3dGenerationTarget 及其契约测试。 - 同步 ts-rs 生成绑定:新增四个可选字段并移除 Model3dGenerationTarget.ts 与 barrel 导出。 - api-server 校验层改按「项目资源 / 素材库二选一」校验,缺失、冲突与空白值都返回 400 并点名具体字段。 - 落点预检改用其它付费编辑器生成共用的 preflight_editor_generation_target_and_return,素材库落点不再读整库。 - worker 按平坦字段落库,素材名缺省回落到 MODEL3D_DEFAULT_ASSET_LABEL。 - 定向验证:cargo test -p shared-contracts 与 cargo test -p api-server tripo3d:: 全绿(54 passed)。
208 lines
7.6 KiB
Rust
208 lines
7.6 KiB
Rust
use serde_json::json;
|
|
use shared_contracts::model3d::Model3dGenerationResult;
|
|
use shared_contracts::model3d::common::{
|
|
Model3dGeneratedArtifact, Model3dGenerationSource, Model3dGenerationTargetRef,
|
|
};
|
|
use shared_contracts::model3d::image_to_model::Model3dImageToModelRequest;
|
|
use shared_contracts::model3d::text_to_model::Model3dTextToModelRequest;
|
|
|
|
#[test]
|
|
fn text_to_model_request_accepts_generation_and_flat_target() {
|
|
let request: Model3dTextToModelRequest = serde_json::from_value(json!({
|
|
"generation": { "prompt": "一把木椅", "model": "v3.1-20260211", "texture": true },
|
|
"assetFolderId": "folder-1",
|
|
"assetLabel": "木椅"
|
|
}))
|
|
.expect("合法请求应可反序列化");
|
|
|
|
assert_eq!(request.generation.prompt, "一把木椅");
|
|
assert_eq!(request.asset_folder_id.as_deref(), Some("folder-1"));
|
|
assert_eq!(request.asset_label.as_deref(), Some("木椅"));
|
|
assert!(request.project_id.is_none());
|
|
assert!(request.canvas_completion.is_none());
|
|
}
|
|
|
|
/// 落点是平坦可选字段:契约本身不强制「二选一」,也没给落点时必须照原样透传,
|
|
/// 由服务端在扣费前拒绝(见 api-server 的 `validate_target`)。
|
|
#[test]
|
|
fn target_fields_are_flat_and_optional() {
|
|
let project: Model3dTextToModelRequest = serde_json::from_value(json!({
|
|
"generation": { "prompt": "一把木椅", "model": "v3.1-20260211" },
|
|
"projectId": "project-1",
|
|
"canvasCompletion": {
|
|
"dialogId": "dialog-1",
|
|
"title": "木椅",
|
|
"placeholder": {
|
|
"x": 1, "y": 2, "width": 3, "height": 4,
|
|
"originalWidth": 3, "originalHeight": 4
|
|
}
|
|
}
|
|
}))
|
|
.expect("项目落点应可反序列化");
|
|
assert_eq!(project.project_id.as_deref(), Some("project-1"));
|
|
assert!(project.canvas_completion.is_some());
|
|
assert!(project.asset_folder_id.is_none());
|
|
|
|
let without_target: Model3dTextToModelRequest = serde_json::from_value(json!({
|
|
"generation": { "prompt": "一把木椅", "model": "v3.1-20260211" }
|
|
}))
|
|
.expect("缺少落点字段在契约层仍可反序列化");
|
|
assert!(without_target.project_id.is_none());
|
|
assert!(without_target.asset_folder_id.is_none());
|
|
|
|
// 落点是平坦字段:旧的 tagged enum 形状不再是合法输入。
|
|
let error = serde_json::from_value::<Model3dTextToModelRequest>(json!({
|
|
"generation": { "prompt": "一把木椅", "model": "v3.1-20260211" },
|
|
"target": { "kind": "assetLibrary", "folderId": "folder-1", "label": "木椅" }
|
|
}))
|
|
.expect_err("target 已不是请求字段,应被拒绝");
|
|
assert!(
|
|
error.to_string().contains("unknown field"),
|
|
"旧 target 形状应报 unknown field,实际为:{error}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn image_source_rejects_ambiguous_and_raw_input() {
|
|
let both_ids = json!({
|
|
"source": { "kind": "resource", "resourceId": "r1", "assetId": "a1" },
|
|
"generation": { "model": "v3.1-20260211" },
|
|
"assetFolderId": "folder-1"
|
|
});
|
|
let error = serde_json::from_value::<Model3dImageToModelRequest>(both_ids)
|
|
.expect_err("同时给出资源与素材 ID 应被拒绝");
|
|
assert!(
|
|
error.to_string().contains("unknown field"),
|
|
"同时给出两个 ID 应报 unknown field,实际为:{error}"
|
|
);
|
|
|
|
let raw_input = json!({
|
|
"source": "https://example.com/a.png",
|
|
"generation": { "model": "v3.1-20260211" },
|
|
"assetFolderId": "folder-1"
|
|
});
|
|
let error = serde_json::from_value::<Model3dImageToModelRequest>(raw_input)
|
|
.expect_err("裸字符串图片输入应被拒绝");
|
|
// 拒绝必须发生在 source 本身:错误里点名 Model3dGenerationSource,避免将来
|
|
// generation / 落点字段的变化把这条负例“顶替”成通过。
|
|
assert!(
|
|
error.to_string().contains("Model3dGenerationSource"),
|
|
"裸字符串 source 的报错应点名 Model3dGenerationSource,实际为:{error}"
|
|
);
|
|
|
|
// 同样把 source 单独反序列化一遍,锁死被拒的是图片输入的形状本身。
|
|
serde_json::from_value::<Model3dGenerationSource>(json!("https://example.com/a.png"))
|
|
.expect_err("裸字符串 source 值应被拒绝");
|
|
}
|
|
|
|
#[test]
|
|
fn api_request_rejects_unknown_top_level_fields() {
|
|
let error = serde_json::from_value::<Model3dTextToModelRequest>(json!({
|
|
"generation": { "prompt": "一把木椅", "model": "v3.1-20260211" },
|
|
"assetFolderId": "folder-1",
|
|
"unexpected": true
|
|
}))
|
|
.expect_err("顶层未知字段应被拒绝");
|
|
|
|
assert!(
|
|
error.to_string().contains("unknown field"),
|
|
"顶层未知字段应报 unknown field,实际为:{error}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn generation_result_is_tagged_per_endpoint() {
|
|
let result: Model3dGenerationResult = serde_json::from_value(json!({
|
|
"kind": "textToModel",
|
|
"target": { "kind": "projectResource", "resourceId": "res-1" },
|
|
"model": {
|
|
"objectKey": "models/res-1.glb",
|
|
"contentType": "model/gltf-binary",
|
|
"contentLength": 41_636_076_u64,
|
|
"sha256": "0f1e"
|
|
},
|
|
"preview": {
|
|
"objectKey": "models/res-1.webp",
|
|
"contentType": "image/webp",
|
|
"contentLength": 20_480_u64,
|
|
"sha256": "2a3b"
|
|
}
|
|
}))
|
|
.expect("按端点的结果应可反序列化");
|
|
|
|
let Model3dGenerationResult::TextToModel(text) = result else {
|
|
panic!("kind=textToModel 不应解成 imageToModel 分支");
|
|
};
|
|
assert_eq!(
|
|
text.target,
|
|
Model3dGenerationTargetRef::ProjectResource {
|
|
resource_id: "res-1".into()
|
|
}
|
|
);
|
|
assert_eq!(
|
|
text.model,
|
|
Model3dGeneratedArtifact {
|
|
object_key: "models/res-1.glb".into(),
|
|
content_type: "model/gltf-binary".into(),
|
|
content_length: 41_636_076,
|
|
sha256: "0f1e".into(),
|
|
}
|
|
);
|
|
assert_eq!(
|
|
text.preview,
|
|
Model3dGeneratedArtifact {
|
|
object_key: "models/res-1.webp".into(),
|
|
content_type: "image/webp".into(),
|
|
content_length: 20_480,
|
|
sha256: "2a3b".into(),
|
|
}
|
|
);
|
|
|
|
// 对称覆盖 imageToModel 分支:tag 路由与分支字段都要单独守住。
|
|
let image_result: Model3dGenerationResult = serde_json::from_value(json!({
|
|
"kind": "imageToModel",
|
|
"target": { "kind": "projectResource", "resourceId": "res-2" },
|
|
"model": {
|
|
"objectKey": "models/res-2.glb",
|
|
"contentType": "model/gltf-binary",
|
|
"contentLength": 41_636_077_u64,
|
|
"sha256": "3c4d"
|
|
},
|
|
"preview": {
|
|
"objectKey": "models/res-2.webp",
|
|
"contentType": "image/webp",
|
|
"contentLength": 20_481_u64,
|
|
"sha256": "4e5f"
|
|
}
|
|
}))
|
|
.expect("按端点的结果应可反序列化");
|
|
|
|
let Model3dGenerationResult::ImageToModel(image) = image_result else {
|
|
panic!("kind=imageToModel 不应解成 textToModel 分支");
|
|
};
|
|
assert_eq!(
|
|
image.target,
|
|
Model3dGenerationTargetRef::ProjectResource {
|
|
resource_id: "res-2".into()
|
|
}
|
|
);
|
|
assert_eq!(
|
|
image.model,
|
|
Model3dGeneratedArtifact {
|
|
object_key: "models/res-2.glb".into(),
|
|
content_type: "model/gltf-binary".into(),
|
|
content_length: 41_636_077,
|
|
sha256: "3c4d".into(),
|
|
}
|
|
);
|
|
assert_eq!(
|
|
image.preview,
|
|
Model3dGeneratedArtifact {
|
|
object_key: "models/res-2.webp".into(),
|
|
content_type: "image/webp".into(),
|
|
content_length: 20_481,
|
|
sha256: "4e5f".into(),
|
|
}
|
|
);
|
|
}
|