diff --git a/crates/api_clients/fal_client/src/requests/api/video/image/kling_2p6_pro_image_to_video/api.rs b/crates/api_clients/fal_client/src/requests/api/video/image/kling_2p6_pro_image_to_video/api.rs index f768e78a26..6233462583 100644 --- a/crates/api_clients/fal_client/src/requests/api/video/image/kling_2p6_pro_image_to_video/api.rs +++ b/crates/api_clients/fal_client/src/requests/api/video/image/kling_2p6_pro_image_to_video/api.rs @@ -210,5 +210,102 @@ mod tests { ); } + /// Parity vs the legacy + /// `requests_old/http/video/image/http_kling_v2p6_pro_image_to_video.rs`. + /// + /// The legacy has no cost calculator, so cost parity is N/A. The wire + /// shape *intentionally diverges*: fal renamed the image field from + /// `image_url` (legacy) to `start_image_url` (current docs), and the + /// legacy gained no concept of `voice_ids` or `end_image_url`. These + /// tests document the differences and pin the endpoint URL, so we'll + /// notice if fal ever flips back or further changes the schema. + mod legacy_parity { + use super::*; + use crate::requests_old::http::video::image::http_kling_v2p6_pro_image_to_video::{ + kling_v2p6_pro_image_to_video, KlingV2p6ProImageToVideoInput, + }; + + /// Same `fal-ai/kling-video/v2.6/pro/image-to-video` path. + #[test] + fn endpoint_path_matches_legacy() { + let legacy = kling_v2p6_pro_image_to_video(KlingV2p6ProImageToVideoInput { + prompt: "p".to_string(), + image_url: "https://example.com/x.png".to_string(), + ..Default::default() + }); + assert_eq!( + Kling2p6ProImageToVideoRequest::ENDPOINT, + legacy.endpoint, + ); + } + + /// The image-URL field was renamed by fal: legacy `image_url` → + /// current `start_image_url`. This test pins the new module to the + /// current name and confirms the legacy field name is *not* emitted. + #[test] + fn new_module_emits_start_image_url_not_image_url() { + let new = Kling2p6ProImageToVideoRequest { + prompt: "p".to_string(), + start_image_url: "https://example.com/x.png".to_string(), + end_image_url: None, + duration: None, + negative_prompt: None, + generate_audio: None, + voice_ids: None, + }; + let json = serde_json::to_value(new.to_raw_request().unwrap()).unwrap(); + assert!(json.get("start_image_url").is_some(), "missing start_image_url: {json}"); + assert!(json.get("image_url").is_none(), "unexpected legacy image_url: {json}"); + } + + /// Legacy struct still emits the *old* `image_url` field name — + /// asserting this here flags any future schema drift in the legacy + /// archive (it shouldn't change; if it does we want to know). + #[test] + fn legacy_module_still_emits_image_url() { + let legacy = KlingV2p6ProImageToVideoInput { + prompt: "p".to_string(), + image_url: "https://example.com/x.png".to_string(), + ..Default::default() + }; + let json = serde_json::to_value(&legacy).unwrap(); + assert!(json.get("image_url").is_some(), "missing image_url: {json}"); + assert!(json.get("start_image_url").is_none(), "unexpected start_image_url: {json}"); + } + + /// On fields that *did* survive the rename (prompt, duration, + /// negative_prompt, generate_audio), wire JSON must match between + /// new and legacy when populated with the same values. + #[test] + fn surviving_fields_match_legacy_wire_shape() { + let new = Kling2p6ProImageToVideoRequest { + prompt: "shared prompt".to_string(), + start_image_url: "https://example.com/x.png".to_string(), + end_image_url: None, + duration: Some(Kling2p6ProImageToVideoDuration::TenSeconds), + negative_prompt: Some("blurry".to_string()), + generate_audio: Some(false), + voice_ids: None, + }; + let mut new_json = serde_json::to_value(new.to_raw_request().unwrap()).unwrap(); + + let legacy = KlingV2p6ProImageToVideoInput { + prompt: "shared prompt".to_string(), + image_url: "https://example.com/x.png".to_string(), + duration: Some("10".to_string()), + negative_prompt: Some("blurry".to_string()), + generate_audio: Some(false), + }; + let mut legacy_json = serde_json::to_value(&legacy).unwrap(); + + // Strip the renamed image-URL field from each side, then compare. + // (`voice_ids` and `end_image_url` are new in 2.6 — neither is set + // in this test, so they don't appear in JSON.) + new_json.as_object_mut().unwrap().remove("start_image_url"); + legacy_json.as_object_mut().unwrap().remove("image_url"); + assert_eq!(new_json, legacy_json); + } + } + // NB: Pricing tests are in cost.rs } diff --git a/crates/api_clients/fal_client/src/requests/api/video/text/kling_2p6_pro_text_to_video/api.rs b/crates/api_clients/fal_client/src/requests/api/video/text/kling_2p6_pro_text_to_video/api.rs index 9944be59ee..e42cacba13 100644 --- a/crates/api_clients/fal_client/src/requests/api/video/text/kling_2p6_pro_text_to_video/api.rs +++ b/crates/api_clients/fal_client/src/requests/api/video/text/kling_2p6_pro_text_to_video/api.rs @@ -233,5 +233,126 @@ mod tests { ); } + /// Wire-shape and endpoint parity vs the legacy + /// `requests_old/http/video/text/http_kling_v2p6_pro_text_to_video.rs`. + /// The legacy module has no cost calculator, so cost parity is N/A; + /// these tests verify the *shape sent on the wire* hasn't drifted. + mod legacy_parity { + use super::*; + use crate::requests_old::http::video::text::http_kling_v2p6_pro_text_to_video::{ + kling_v2p6_pro_text_to_video, KlingV2p6ProTextToVideoInput, + }; + + /// Same `fal-ai/kling-video/v2.6/pro/text-to-video` path. + #[test] + fn endpoint_path_matches_legacy() { + let legacy = kling_v2p6_pro_text_to_video(KlingV2p6ProTextToVideoInput::default()); + assert_eq!( + Kling2p6ProTextToVideoRequest::ENDPOINT, + legacy.endpoint, + ); + } + + /// At a representative populated case, the new module's serialized + /// `RawRequest` must equal the legacy `Input`'s JSON byte-for-byte + /// (modulo serde field ordering, which is stable within each struct). + #[test] + fn wire_json_matches_legacy_fully_populated() { + let new = Kling2p6ProTextToVideoRequest { + prompt: "a wave at dawn".to_string(), + generate_audio: Some(true), + negative_prompt: Some("blurry".to_string()), + duration: Some(Kling2p6ProTextToVideoDuration::TenSeconds), + aspect_ratio: Some(Kling2p6ProTextToVideoAspectRatio::NineBySixteen), + cfg_scale: Some(0.5), + }; + let new_json = serde_json::to_value(new.to_raw_request().unwrap()).unwrap(); + + let legacy = KlingV2p6ProTextToVideoInput { + prompt: "a wave at dawn".to_string(), + aspect_ratio: Some("9:16".to_string()), + generate_audio: Some(true), + negative_prompt: Some("blurry".to_string()), + duration: Some("10".to_string()), + cfg_scale: Some(0.5), + }; + let legacy_json = serde_json::to_value(&legacy).unwrap(); + + assert_eq!(new_json, legacy_json); + } + + /// At minimum (prompt only), unset optionals must be omitted on the + /// wire just like the legacy struct. + #[test] + fn wire_json_matches_legacy_minimal() { + let new = Kling2p6ProTextToVideoRequest { + prompt: "minimal".to_string(), + generate_audio: None, + negative_prompt: None, + duration: None, + aspect_ratio: None, + cfg_scale: None, + }; + let new_json = serde_json::to_value(new.to_raw_request().unwrap()).unwrap(); + + let legacy = KlingV2p6ProTextToVideoInput { + prompt: "minimal".to_string(), + ..Default::default() + }; + let legacy_json = serde_json::to_value(&legacy).unwrap(); + + assert_eq!(new_json, legacy_json); + // And the minimal payload should literally be `{"prompt": "minimal"}`. + assert_eq!(new_json, serde_json::json!({ "prompt": "minimal" })); + } + + /// Cross product over duration × aspect_ratio × audio: every cell's wire + /// JSON must match the equivalent legacy `Input`. + #[test] + fn wire_json_matches_legacy_at_every_combo() { + let durations = [ + (None, None), + (Some(Kling2p6ProTextToVideoDuration::FiveSeconds), Some("5")), + (Some(Kling2p6ProTextToVideoDuration::TenSeconds), Some("10")), + ]; + let aspect_ratios = [ + (None, None), + (Some(Kling2p6ProTextToVideoAspectRatio::Square), Some("1:1")), + (Some(Kling2p6ProTextToVideoAspectRatio::SixteenByNine), Some("16:9")), + (Some(Kling2p6ProTextToVideoAspectRatio::NineBySixteen), Some("9:16")), + ]; + let audio_options = [None, Some(false), Some(true)]; + + for (d_new, d_legacy) in durations { + for (ar_new, ar_legacy) in aspect_ratios { + for audio in audio_options { + let new = Kling2p6ProTextToVideoRequest { + prompt: "p".to_string(), + generate_audio: audio, + negative_prompt: None, + duration: d_new, + aspect_ratio: ar_new, + cfg_scale: None, + }; + let legacy = KlingV2p6ProTextToVideoInput { + prompt: "p".to_string(), + aspect_ratio: ar_legacy.map(String::from), + generate_audio: audio, + negative_prompt: None, + duration: d_legacy.map(String::from), + cfg_scale: None, + }; + let new_json = serde_json::to_value(new.to_raw_request().unwrap()).unwrap(); + let legacy_json = serde_json::to_value(&legacy).unwrap(); + assert_eq!( + new_json, legacy_json, + "duration={d_new:?} aspect_ratio={ar_new:?} audio={audio:?}", + ); + } + } + } + } + } + // NB: Pricing tests are in cost.rs }