Files
Genarrative/server-rs/crates/shared-contracts/tests/model3d_api_request_contract.rs
T
k88936 2348315dba 3D 结果打标契约测试补齐 preview 与 imageToModel 分支
- textToModel 用例补 preview 产物的相等断言
- 新增对称的 imageToModel 用例,守住 tag 路由与分支字段
- 裸字符串 source 的负例改为点名 Model3dGenerationSource 并单独反序列化 source 值
2026-09-22 16:46:19 +08:00

184 lines
6.6 KiB
Rust

use serde_json::json;
use shared_contracts::model3d::Model3dGenerationResult;
use shared_contracts::model3d::common::{
Model3dGeneratedArtifact, Model3dGenerationSource, Model3dGenerationTarget,
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_target() {
let request: Model3dTextToModelRequest = serde_json::from_value(json!({
"generation": { "prompt": "一把木椅", "model": "v3.1-20260211", "texture": true },
"target": { "kind": "assetLibrary", "folderId": "folder-1", "label": "木椅" }
}))
.expect("合法请求应可反序列化");
assert_eq!(request.generation.prompt, "一把木椅");
assert!(matches!(
request.target,
Model3dGenerationTarget::AssetLibrary { .. }
));
}
#[test]
fn target_requires_branch_local_fields() {
for invalid in [
json!({ "kind": "assetLibrary", "label": "木椅" }),
json!({ "kind": "assetLibrary", "folderId": "folder-1" }),
json!({ "kind": "projectResource" }),
] {
let error = serde_json::from_value::<Model3dGenerationTarget>(invalid.clone())
.expect_err("缺少分支必填字段应被拒绝");
assert!(
error.to_string().contains("missing field"),
"缺少字段的报错应指出 missing field,输入 {invalid},实际为:{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" },
"target": { "kind": "assetLibrary", "folderId": "folder-1", "label": "木椅" }
});
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" },
"target": { "kind": "assetLibrary", "folderId": "folder-1", "label": "木椅" }
});
let error = serde_json::from_value::<Model3dImageToModelRequest>(raw_input)
.expect_err("裸字符串图片输入应被拒绝");
// 拒绝必须发生在 source 本身:错误里点名 Model3dGenerationSource,避免将来
// generation / target 的必填变化把这条负例“顶替”成通过。
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" },
"target": { "kind": "assetLibrary", "folderId": "folder-1", "label": "木椅" },
"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(),
}
);
}