补Tripo worker纯逻辑定向用例
- checkpoint 只在 provider task id 非空且去空白后有效时才算存在,空白等同未提交 - 完成结果按端点严格区分,且序列化后不含 provider、taskId 与带签名的临时地址 - 预览图尺寸必须能真实解码,损坏字节直接失败而不是写占位宽高 - 预检用例去掉多余的通配导入,实施计划进展同步当前通过数
This commit is contained in:
@@ -74,7 +74,8 @@ Milestone Spec: `docs/project-memory/plans/【里程碑】Tripo生成Worker执
|
||||
- 提交路由已补应用级验收用例:走真实 `build_router` 断言两个端点未鉴权 401、缺 `Idempotency-Key` 400、组合校验与定价必填字段冲突在定价与 provider 之前 400,因此路由 merge、鉴权中间件与校验顺序都有可执行证据,不依赖 provider 与 SpacetimeDB 环境。
|
||||
|
||||
- 默认配置里的 `model3d` 泥点定价数值仍未确认,缺段时提交按 fail closed 拒绝。
|
||||
- 没有跑过端到端真实调用(提交 → worker → OSS → 资源登记),现有证据只到 `cargo test -p api-server` 全绿(1089 通过)与应用级路由验收用例。
|
||||
- worker 的纯逻辑部分已补定向用例:checkpoint 只在 provider task id 非空时才算存在、完成结果按端点严格区分且不含 provider 事实、预览图尺寸必须能真实解码;全量 `cargo test -p api-server` 1092 通过。
|
||||
- 没有跑过端到端真实调用(提交 → worker → OSS → 资源登记),现有证据只到上述单元与应用级用例,缺少真实 provider 与 SpacetimeDB / OSS 的联调证据。
|
||||
- 新增的 `tripo3d/` 文件里,`storage.rs` 的完整字节写入、`artifacts.rs` 的完整字节读取都留着流式 TODO。
|
||||
|
||||
## 验证命令
|
||||
|
||||
@@ -117,8 +117,6 @@ fn image_source_kind_mismatch(requested_kind: &str, resolved_kind: &str) -> AppE
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 预检必须是定点、只读元数据的;一旦退回“列工程 / 列素材库再筛”的老路,
|
||||
/// 或提前把图片正文读出来,这条断言会失败。
|
||||
#[test]
|
||||
|
||||
@@ -451,3 +451,148 @@ fn output_schema_error(message: String) -> AppError {
|
||||
"message": message,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tripo3d::job::Model3dJobKind;
|
||||
use module_assets::{AssetObjectAccessPolicy, AssetObjectUpsertInput};
|
||||
|
||||
/// at-most-once submit 完全依赖这条判定:checkpoint 有值就只能续跑查询。
|
||||
/// 空白值必须等同于“没有 checkpoint”,否则一次空写入会把 job 永久锁死。
|
||||
#[test]
|
||||
fn checkpoint_is_only_recognized_when_provider_task_id_is_present() {
|
||||
let mut job = job_fixture();
|
||||
assert_eq!(existing_checkpoint(&job), None);
|
||||
|
||||
job.provider_task_id = Some(" ".to_string());
|
||||
assert_eq!(existing_checkpoint(&job), None);
|
||||
|
||||
job.provider_task_id = Some(" task-123 ".to_string());
|
||||
assert_eq!(existing_checkpoint(&job).as_deref(), Some("task-123"));
|
||||
}
|
||||
|
||||
/// 完成结果按端点严格区分,且只带正式对象元数据:provider task ID、SDK 类型与
|
||||
/// 带签名的临时地址都不允许出现在返回给调用方的结果里。
|
||||
#[test]
|
||||
fn completed_result_is_strict_per_endpoint_and_free_of_provider_facts() {
|
||||
let model = stored_artifact("editor/model3d/task-1/model.glb", "model/gltf-binary", 4096);
|
||||
let preview = stored_artifact("editor/model3d/task-1/preview.webp", "image/webp", 2048);
|
||||
|
||||
for (kind, expected_kind) in [
|
||||
(Model3dJobKind::TextToModel, "textToModel"),
|
||||
(Model3dJobKind::ImageToModel, "imageToModel"),
|
||||
] {
|
||||
let result = build_result(
|
||||
kind,
|
||||
Model3dGenerationTargetRef::ProjectResource {
|
||||
resource_id: "resource-1".to_string(),
|
||||
},
|
||||
&model,
|
||||
&preview,
|
||||
);
|
||||
let payload = job_result_payload(result);
|
||||
let result = &payload["result"];
|
||||
|
||||
assert_eq!(result["kind"], json!(expected_kind));
|
||||
assert_eq!(result["target"]["kind"], json!("projectResource"));
|
||||
assert_eq!(result["target"]["resourceId"], json!("resource-1"));
|
||||
assert_eq!(
|
||||
result["model"]["objectKey"],
|
||||
json!("editor/model3d/task-1/model.glb")
|
||||
);
|
||||
assert_eq!(result["model"]["contentLength"], json!(4096));
|
||||
assert_eq!(result["preview"]["contentLength"], json!(2048));
|
||||
|
||||
let serialized = payload.to_string();
|
||||
for forbidden in ["provider", "taskId", "providerTaskId", "https://"] {
|
||||
assert!(
|
||||
!serialized.contains(forbidden),
|
||||
"完成结果不得携带 {forbidden}:{serialized}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 预览图尺寸是资源行的宽高来源,读不出来就宁可失败退款,也不写占位尺寸。
|
||||
#[test]
|
||||
fn preview_dimensions_require_a_real_decodable_image() {
|
||||
let image = image::RgbaImage::from_pixel(3, 2, image::Rgba([10, 20, 30, 255]));
|
||||
let mut bytes = Vec::new();
|
||||
image
|
||||
.write_to(
|
||||
&mut std::io::Cursor::new(&mut bytes),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.expect("测试用 PNG 应可编码");
|
||||
|
||||
assert_eq!(
|
||||
preview_dimensions(bytes.as_slice()).expect("应读出尺寸"),
|
||||
(3, 2)
|
||||
);
|
||||
assert!(preview_dimensions(b"not an image").is_err());
|
||||
}
|
||||
|
||||
fn stored_artifact(
|
||||
object_key: &str,
|
||||
content_type: &str,
|
||||
content_length: u64,
|
||||
) -> StoredModel3dArtifact {
|
||||
StoredModel3dArtifact {
|
||||
object_key: object_key.to_string(),
|
||||
content_type: content_type.to_string(),
|
||||
content_length,
|
||||
sha256: "0".repeat(64),
|
||||
asset_object: AssetObjectUpsertInput {
|
||||
asset_object_id: format!("asset-object-{object_key}"),
|
||||
bucket: "genarrative-test".to_string(),
|
||||
object_key: object_key.to_string(),
|
||||
access_policy: AssetObjectAccessPolicy::Private,
|
||||
content_type: Some(content_type.to_string()),
|
||||
content_length,
|
||||
content_hash: Some("0".repeat(64)),
|
||||
version: 1,
|
||||
source_job_id: Some("extgen-1".to_string()),
|
||||
owner_user_id: Some("user-1".to_string()),
|
||||
profile_id: None,
|
||||
entity_id: Some("extgen-1".to_string()),
|
||||
asset_kind: MODEL3D_ASSET_KIND.to_string(),
|
||||
updated_at_micros: 1_780_444_800_000_000,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn job_fixture() -> ExternalGenerationJobRecord {
|
||||
ExternalGenerationJobRecord {
|
||||
job_id: "extgen-1".to_string(),
|
||||
dedupe_key: "model3d-generation:user-1:model3d_text_to_model:issue-1".to_string(),
|
||||
job_kind: "model3d_text_to_model".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
source_module: "editor-canvas".to_string(),
|
||||
source_entity_id: "project-1".to_string(),
|
||||
request_label: "文生 3D 模型".to_string(),
|
||||
request_payload_json: "{}".to_string(),
|
||||
status: "running".to_string(),
|
||||
attempt: 1,
|
||||
max_attempts: 1,
|
||||
last_error_message: None,
|
||||
worker_id: Some("worker-a".to_string()),
|
||||
lease_expires_at: Some("2026-09-21T00:00:00Z".to_string()),
|
||||
available_at: "2026-09-21T00:00:00Z".to_string(),
|
||||
result_payload_json: None,
|
||||
created_at: "2026-09-21T00:00:00Z".to_string(),
|
||||
started_at: Some("2026-09-21T00:00:00Z".to_string()),
|
||||
completed_at: None,
|
||||
updated_at: "2026-09-21T00:00:00Z".to_string(),
|
||||
updated_at_micros: 1_780_444_800_000_000,
|
||||
lease_token: Some("lease-1".to_string()),
|
||||
price_mud_points: 20,
|
||||
refund_ledger_id: None,
|
||||
notification_acknowledged_at: None,
|
||||
notification_acknowledged_at_micros: None,
|
||||
phase: Some("generating".to_string()),
|
||||
provider_kind: None,
|
||||
provider_task_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user