#[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()] ); } }