178 lines
6.7 KiB
Rust
178 lines
6.7 KiB
Rust
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
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_vector_engine_image_request_body(
|
|
" 风雨夜里的街道 ",
|
|
Some(" 低清,水印 "),
|
|
" 1:1 ",
|
|
10,
|
|
&["data:image/png;base64,AAAA".to_string()],
|
|
);
|
|
|
|
assert_eq!(body["model"], GPT_IMAGE_2_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 = VectorEngineImageSettings {
|
|
base_url: "https://vector.example".to_string(),
|
|
api_key: "test-key".to_string(),
|
|
request_timeout_ms: 1_000,
|
|
};
|
|
let v1_settings = VectorEngineImageSettings {
|
|
base_url: "https://vector.example/v1".to_string(),
|
|
api_key: "test-key".to_string(),
|
|
request_timeout_ms: 1_000,
|
|
};
|
|
|
|
assert_eq!(
|
|
vector_engine_images_generation_url(&root_settings),
|
|
"https://vector.example/v1/images/generations"
|
|
);
|
|
assert_eq!(
|
|
vector_engine_images_generation_url(&v1_settings),
|
|
"https://vector.example/v1/images/generations"
|
|
);
|
|
assert_eq!(
|
|
vector_engine_images_edit_url(&root_settings),
|
|
"https://vector.example/v1/images/edits"
|
|
);
|
|
assert_eq!(
|
|
vector_engine_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(VECTOR_ENGINE_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(VECTOR_ENGINE_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"]
|
|
}
|
|
});
|
|
|
|
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()]
|
|
);
|
|
}
|
|
}
|