3D 图生输入解析的测试改为断言可观察行为

- 删掉 include_str! 嵌源码、按 token 断言「没出现列工程 / 列素材库」的守卫:它测的是实现文本,改名、换格式、调顺序都会误伤
- 定点查询用例改成覆盖 resource / asset 两个分支,断言每个分支只发起一次按 ID 的查询、且对象键原样来自记录
- 模块文档写明定点查询由注入签名收口(一个 ID → 一条记录),失败语义与测试口径一并写实
This commit is contained in:
2026-09-23 14:58:44 +08:00
parent 1be77d2f99
commit 11d1324857
@@ -2,12 +2,17 @@
//!
//! 请求只给站内的画布资源 ID 或账号素材 ID,分两步处理:
//! 1. 提交时只做元数据预检:按 ID 定点确认归属与记录类型,不读字节、不调用 provider,
//! 因此跨 owner、未登记与类型不符都在扣费与入队之前返回 400
//! 因此跨 owner、未登记、已删除与类型不符都在扣费与入队之前失败
//! 2. worker 执行时重新确认同一事实,再从私有 OSS 读出字节,最后上传 provider 换
//! `file_token`。**不把带签名的临时地址交给第三方**,也不接受 URL / data URL。
//!
//! 预检只按主键定点查引用,不拉取当前用户的工程列表或素材库;跨 owner、已删除与不存在
//! 收敛成同一句 400,避免把别的账号是否存在该 ID 变成可探测信息。
//! 收敛成同一句 400,避免把别的账号是否存在该 ID 变成可探测信息。只有这类「引用不可用」
//! 才是 400:403(服务身份校验失败)、409(版本冲突)与 5xx 都是基础设施 / 并发问题,
//! 按原状态码上报;「记录已解析但缺对象键」是行本身不完整,按 502 上报。
//!
//! 定点查询是注入到 `resolve_source_object_key_with` 的参数,解析每分支只发一次;测试按可观察
//! 行为断言(查询次数、查询 ID、带出的对象键、各分支状态码),不依赖源码文本。
//!
//! TODO(oss-direct-read): 「让 Tripo 侧直接读站内对象」这条替代路径(交签名 URL,或对端直读
//! OSS)能省掉一次字节中转与一份上行带宽,但等于把私有对象和带时效的读取凭据交给第三方,
@@ -189,23 +194,40 @@ mod tests {
}
}
/// 解析只发起一次按 ID 的定点查询:查询到的 ID 必须与请求里的引用 ID 一致。
/// 两个 source 分支都只发起一次按 ID 的定点查询,并把查到的那条记录的对象键原样带出:
/// 解析不需要「列工程 / 列素材库再筛」这类宽查询,注入的查询签名本身就是「一个 ID → 一条记录」。
#[tokio::test]
async fn image_source_resolution_queries_only_the_requested_reference_id() {
let source = Model3dGenerationSource::Resource {
resource_id: "res-1".to_string(),
};
let mut queried: Vec<String> = Vec::new();
for (source, requested_id, kind, object_key) in [
(
Model3dGenerationSource::Resource {
resource_id: "res-1".to_string(),
},
"res-1",
"resource",
"objects/res-1.png",
),
(
Model3dGenerationSource::Asset {
asset_id: "asset-9".to_string(),
},
"asset-9",
"asset",
"objects/asset-9.png",
),
] {
let mut queried: Vec<String> = Vec::new();
let object_key = resolve_source_object_key_with(&source, |reference_id| {
queried.push(reference_id);
async { Ok(resolved("resource", Some("objects/res-1.png"))) }
})
.await
.expect("同类型的已登记引用应解析出对象键");
let resolved_key = resolve_source_object_key_with(&source, |reference_id| {
queried.push(reference_id);
async move { Ok(resolved(kind, Some(object_key))) }
})
.await
.expect("同类型的已登记引用应解析出对象键");
assert_eq!(object_key, "objects/res-1.png");
assert_eq!(queried, vec!["res-1".to_string()]);
assert_eq!(resolved_key, object_key);
assert_eq!(queried, vec![requested_id.to_string()]);
}
}
/// 记录类型与 `source` 分支不一致时报类型不符,而不是把对象键照用。
@@ -287,26 +309,4 @@ mod tests {
assert_eq!(reason_of(&kept), Some("probe"));
}
}
/// 本模块只做定点解析:一旦退回「列工程 / 列素材库再筛」的老路,
/// 这两个 token 会出现在源码里,断言立刻失败。
///
/// 预检不读字节、不调用 provider 由类型保证(`preflight_image_source` 不接
/// provider client,也不碰 OSS),解析规则本身由上面的注入式用例覆盖。
#[test]
fn image_source_module_never_lists_projects_or_asset_libraries() {
let source = include_str!("image_source.rs");
assert!(
source.contains("resolve_editor_reference_record_by_id_for_owner("),
"定点解析必须复用既有的按 ID 窄查询"
);
// 用 concat! 拼出待检查的字面量,否则断言里的字面量会被 include_str! 自己命中。
for forbidden in [
concat!("list_editor_", "projects"),
concat!("get_editor_", "asset_library"),
] {
assert!(!source.contains(forbidden), "本模块不得包含 {forbidden}");
}
}
}