恢复 image_provider 单元测试模块

image_provider/mod.rs 重新声明 #[cfg(test)] mod tests,tests.rs 去掉失效的模块包裹并补齐 helper 导入

提取器 extract_image_urls / extract_b64_images 放宽到 pub(crate),parse_reference_image_data_url 调用补 provider 参数

补回 5 个此前被静默跳过的单元测试
This commit is contained in:
2026-09-19 13:26:35 +08:00
parent 0817ea8158
commit c86ac04220
3 changed files with 224 additions and 221 deletions
@@ -3,6 +3,9 @@ pub(crate) mod protocol;
pub(crate) mod runtime;
pub(crate) mod transport;
#[cfg(test)]
mod tests;
pub(crate) use protocol::{payload, request, response};
pub(crate) use runtime::{audit, budget, error, image_source, types, util};
pub(crate) use transport::curl as curl_transport;
@@ -72,7 +72,7 @@ pub(super) fn extract_generation_id(payload: &Value) -> Option<String> {
.or_else(|| find_first_string_by_key(payload, "request_id"))
}
pub(super) fn extract_image_urls(payload: &Value) -> Vec<String> {
pub(crate) fn extract_image_urls(payload: &Value) -> Vec<String> {
let mut urls = Vec::new();
collect_strings_by_key(payload, "url", &mut urls);
collect_strings_by_key(payload, "image", &mut urls);
@@ -86,7 +86,7 @@ pub(super) fn extract_image_urls(payload: &Value) -> Vec<String> {
deduped
}
pub(super) fn extract_b64_images(payload: &Value) -> Vec<String> {
pub(crate) fn extract_b64_images(payload: &Value) -> Vec<String> {
let mut values = Vec::new();
collect_strings_by_key(payload, "b64_json", &mut values);
collect_inline_image_data(payload, &mut values);
@@ -1,223 +1,223 @@
#[cfg(test)]
mod tests {
use super::*;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use serde_json::json;
use super::*;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use serde_json::json;
#[test]
fn request_body_normalizes_size_prompt_and_candidate_count() {
let body = build_image_request_body(
" 风雨夜里的街道 ",
Some(" 低清,水印 "),
" 1:1 ",
10,
&["data:image/png;base64,AAAA".to_string()],
);
use super::image_source::{decode_generated_image_base64, parse_reference_image_data_url};
use super::payload::{extract_b64_images, extract_image_urls};
assert_eq!(body["model"], GPT_IMAGE_2_5_GENERATION_MODEL);
assert_eq!(body["size"], "1024x1024");
assert_eq!(body["n"], 4);
assert_eq!(body["prompt"], "风雨夜里的街道\n避免:低清,水印");
assert!(body.get("image").is_none());
}
#[test]
fn request_body_normalizes_size_prompt_and_candidate_count() {
let body = build_image_request_body(
" 风雨夜里的街道 ",
Some(" 低清,水印 "),
" 1:1 ",
10,
&["data:image/png;base64,AAAA".to_string()],
);
#[test]
fn provider_urls_normalize_root_and_v1_base_urls() {
let root_settings = ImageProviderSettings {
provider: ImageProvider::VectorEngine,
base_url: "https://vector.example".to_string(),
api_key: "test-key".to_string(),
request_timeout_ms: 1_000,
request_deadline: None,
};
let v1_settings = ImageProviderSettings {
provider: ImageProvider::VectorEngine,
base_url: "https://vector.example/v1".to_string(),
api_key: "test-key".to_string(),
request_timeout_ms: 1_000,
request_deadline: None,
};
assert_eq!(
images_generation_url(&root_settings),
"https://vector.example/v1/images/generations"
);
assert_eq!(
images_generation_url(&v1_settings),
"https://vector.example/v1/images/generations"
);
assert_eq!(
images_edit_url(&root_settings),
"https://vector.example/v1/images/edits"
);
assert_eq!(
images_edit_url(&v1_settings),
"https://vector.example/v1/images/edits"
);
}
#[test]
fn data_url_and_base64_image_decoding_preserves_image_metadata() {
let data_url = format!(
"data:image/png;base64,{}",
BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nrest")
);
let reference = parse_reference_image_data_url(&data_url, 2)
.expect("data url should parse")
.expect("image data url should be accepted");
assert_eq!(reference.file_name, "reference-2.png");
assert_eq!(reference.mime_type, "image/png");
assert_eq!(reference.bytes, b"\x89PNG\r\n\x1A\nrest");
let image = decode_generated_image_base64(
BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nrest").as_str(),
)
.expect("base64 image should decode");
assert_eq!(image.extension, "png");
assert_eq!(image.mime_type, "image/png");
assert_eq!(image.bytes, b"\x89PNG\r\n\x1A\nrest");
}
#[test]
fn error_status_hints_and_audit_fields_are_structured() {
let audit = PlatformImageFailureAudit {
provider: VECTOR_ENGINE_PROVIDER,
endpoint: "https://vector.example/v1/images/generations".to_string(),
operation: "图片生成失败".to_string(),
failure_stage: "upstream_status",
status_code: Some(504),
status_class: Some("5xx"),
timeout: true,
retryable: true,
error_message: "上游超时".to_string(),
error_source: Some("read timeout".to_string()),
raw_excerpt: Some("{\"error\":\"timeout\"}".to_string()),
latency_ms: Some(987),
prompt_chars: Some(64),
reference_image_count: Some(2),
image_model: Some(GPT_IMAGE_2_MODEL),
};
let request_error = PlatformImageError::Request {
provider: VECTOR_ENGINE_PROVIDER,
message: "请求发送失败".to_string(),
endpoint: Some("https://vector.example/v1/images/generations".to_string()),
timeout: true,
connect: false,
request: true,
body: false,
status_code: None,
source: None,
audit: None,
};
let invalid_config = PlatformImageError::InvalidConfig {
provider: VECTOR_ENGINE_PROVIDER,
message: "缺少配置".to_string(),
};
let invalid_request = PlatformImageError::InvalidRequest {
provider: VECTOR_ENGINE_PROVIDER,
message: "请求不合法".to_string(),
};
let upstream_timeout = PlatformImageError::Upstream {
provider: VECTOR_ENGINE_PROVIDER,
message: "upstream timeout".to_string(),
upstream_status: 502,
raw_excerpt: "deadline has elapsed".to_string(),
audit: Some(audit.clone()),
};
assert_eq!(
invalid_config.status_hint(),
PlatformImageStatusHint::ServiceUnavailable
);
assert_eq!(
invalid_request.status_hint(),
PlatformImageStatusHint::BadRequest
);
assert_eq!(
request_error.status_hint(),
PlatformImageStatusHint::GatewayTimeout
);
assert_eq!(
upstream_timeout.status_hint(),
PlatformImageStatusHint::GatewayTimeout
);
assert_eq!(
PlatformImageError::MissingImage {
provider: VECTOR_ENGINE_PROVIDER,
message: "缺图".to_string(),
audit: Some(audit.clone()),
}
.status_hint(),
PlatformImageStatusHint::BadGateway
);
let audit_ref = upstream_timeout.audit().expect("audit should be preserved");
assert_eq!(audit_ref.provider, VECTOR_ENGINE_PROVIDER);
assert_eq!(
audit_ref.endpoint,
"https://vector.example/v1/images/generations"
);
assert_eq!(audit_ref.status_code, Some(504));
assert_eq!(audit_ref.status_class, Some("5xx"));
assert!(audit_ref.timeout);
assert!(audit_ref.retryable);
assert_eq!(audit_ref.reference_image_count, Some(2));
assert_eq!(audit_ref.image_model, Some(GPT_IMAGE_2_MODEL));
assert!(invalid_config.audit().is_none());
assert!(invalid_request.audit().is_none());
}
#[test]
fn extract_image_urls_and_b64_values_are_deduped() {
let payload = json!({
"data": [
{"image": "https://example.com/a.png"},
{"url": "https://example.com/a.png"},
{"image_url": "ftp://example.com/b.png"},
{"url": "https://example.com/b.png"}
],
"nested": {
"b64_json": ["YWJj", "ZGVm"],
"parts": [
{
"inlineData": {
"mimeType": "image/png",
"data": "aW1hZ2UtMQ=="
}
},
{
"inline_data": {
"mime_type": "image/jpeg",
"data": "aW1hZ2UtMg=="
}
},
{
"inlineData": {
"mimeType": "text/plain",
"data": "bm90LWltYWdl"
}
}
]
}
});
assert_eq!(
extract_image_urls(&payload),
vec![
"https://example.com/a.png".to_string(),
"https://example.com/b.png".to_string()
]
);
assert_eq!(
extract_b64_images(&payload),
vec![
"YWJj".to_string(),
"ZGVm".to_string(),
"aW1hZ2UtMQ==".to_string(),
"aW1hZ2UtMg==".to_string(),
]
);
}
assert_eq!(body["model"], GPT_IMAGE_2_5_GENERATION_MODEL);
assert_eq!(body["size"], "1024x1024");
assert_eq!(body["n"], 4);
assert_eq!(body["prompt"], "风雨夜里的街道\n避免:低清,水印");
assert!(body.get("image").is_none());
}
#[test]
fn provider_urls_normalize_root_and_v1_base_urls() {
let root_settings = ImageProviderSettings {
provider: ImageProvider::VectorEngine,
base_url: "https://vector.example".to_string(),
api_key: "test-key".to_string(),
request_timeout_ms: 1_000,
request_deadline: None,
};
let v1_settings = ImageProviderSettings {
provider: ImageProvider::VectorEngine,
base_url: "https://vector.example/v1".to_string(),
api_key: "test-key".to_string(),
request_timeout_ms: 1_000,
request_deadline: None,
};
assert_eq!(
images_generation_url(&root_settings),
"https://vector.example/v1/images/generations"
);
assert_eq!(
images_generation_url(&v1_settings),
"https://vector.example/v1/images/generations"
);
assert_eq!(
images_edit_url(&root_settings),
"https://vector.example/v1/images/edits"
);
assert_eq!(
images_edit_url(&v1_settings),
"https://vector.example/v1/images/edits"
);
}
#[test]
fn data_url_and_base64_image_decoding_preserves_image_metadata() {
let data_url = format!(
"data:image/png;base64,{}",
BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nrest")
);
let reference = parse_reference_image_data_url(&data_url, 2, ImageProvider::VectorEngine)
.expect("data url should parse")
.expect("image data url should be accepted");
assert_eq!(reference.file_name, "reference-2.png");
assert_eq!(reference.mime_type, "image/png");
assert_eq!(reference.bytes, b"\x89PNG\r\n\x1A\nrest");
let image =
decode_generated_image_base64(BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nrest").as_str())
.expect("base64 image should decode");
assert_eq!(image.extension, "png");
assert_eq!(image.mime_type, "image/png");
assert_eq!(image.bytes, b"\x89PNG\r\n\x1A\nrest");
}
#[test]
fn error_status_hints_and_audit_fields_are_structured() {
let audit = PlatformImageFailureAudit {
provider: VECTOR_ENGINE_PROVIDER,
endpoint: "https://vector.example/v1/images/generations".to_string(),
operation: "图片生成失败".to_string(),
failure_stage: "upstream_status",
status_code: Some(504),
status_class: Some("5xx"),
timeout: true,
retryable: true,
error_message: "上游超时".to_string(),
error_source: Some("read timeout".to_string()),
raw_excerpt: Some("{\"error\":\"timeout\"}".to_string()),
latency_ms: Some(987),
prompt_chars: Some(64),
reference_image_count: Some(2),
image_model: Some(GPT_IMAGE_2_MODEL),
};
let request_error = PlatformImageError::Request {
provider: VECTOR_ENGINE_PROVIDER,
message: "请求发送失败".to_string(),
endpoint: Some("https://vector.example/v1/images/generations".to_string()),
timeout: true,
connect: false,
request: true,
body: false,
status_code: None,
source: None,
audit: None,
};
let invalid_config = PlatformImageError::InvalidConfig {
provider: VECTOR_ENGINE_PROVIDER,
message: "缺少配置".to_string(),
};
let invalid_request = PlatformImageError::InvalidRequest {
provider: VECTOR_ENGINE_PROVIDER,
message: "请求不合法".to_string(),
};
let upstream_timeout = PlatformImageError::Upstream {
provider: VECTOR_ENGINE_PROVIDER,
message: "upstream timeout".to_string(),
upstream_status: 502,
raw_excerpt: "deadline has elapsed".to_string(),
audit: Some(audit.clone()),
};
assert_eq!(
invalid_config.status_hint(),
PlatformImageStatusHint::ServiceUnavailable
);
assert_eq!(
invalid_request.status_hint(),
PlatformImageStatusHint::BadRequest
);
assert_eq!(
request_error.status_hint(),
PlatformImageStatusHint::GatewayTimeout
);
assert_eq!(
upstream_timeout.status_hint(),
PlatformImageStatusHint::GatewayTimeout
);
assert_eq!(
PlatformImageError::MissingImage {
provider: VECTOR_ENGINE_PROVIDER,
message: "缺图".to_string(),
audit: Some(audit.clone()),
}
.status_hint(),
PlatformImageStatusHint::BadGateway
);
let audit_ref = upstream_timeout.audit().expect("audit should be preserved");
assert_eq!(audit_ref.provider, VECTOR_ENGINE_PROVIDER);
assert_eq!(
audit_ref.endpoint,
"https://vector.example/v1/images/generations"
);
assert_eq!(audit_ref.status_code, Some(504));
assert_eq!(audit_ref.status_class, Some("5xx"));
assert!(audit_ref.timeout);
assert!(audit_ref.retryable);
assert_eq!(audit_ref.reference_image_count, Some(2));
assert_eq!(audit_ref.image_model, Some(GPT_IMAGE_2_MODEL));
assert!(invalid_config.audit().is_none());
assert!(invalid_request.audit().is_none());
}
#[test]
fn extract_image_urls_and_b64_values_are_deduped() {
let payload = json!({
"data": [
{"image": "https://example.com/a.png"},
{"url": "https://example.com/a.png"},
{"image_url": "ftp://example.com/b.png"},
{"url": "https://example.com/b.png"}
],
"nested": {
"b64_json": ["YWJj", "ZGVm"],
"parts": [
{
"inlineData": {
"mimeType": "image/png",
"data": "aW1hZ2UtMQ=="
}
},
{
"inline_data": {
"mime_type": "image/jpeg",
"data": "aW1hZ2UtMg=="
}
},
{
"inlineData": {
"mimeType": "text/plain",
"data": "bm90LWltYWdl"
}
}
]
}
});
assert_eq!(
extract_image_urls(&payload),
vec![
"https://example.com/a.png".to_string(),
"https://example.com/b.png".to_string()
]
);
assert_eq!(
extract_b64_images(&payload),
vec![
"YWJj".to_string(),
"ZGVm".to_string(),
"aW1hZ2UtMQ==".to_string(),
"aW1hZ2UtMg==".to_string(),
]
);
}