From d2fc71cfe98c51fda4ef8bd3868a9eb43b68b5d4 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 9 Apr 2026 21:23:13 -0400 Subject: [PATCH] seedream test --- ...plan_generate_image_artcraft_seedream_4.rs | 17 +- .../omni_gen/generate/image/tests/mod.rs | 1 + .../generate/image/tests/test_seedream_4.rs | 613 ++++++++++++++++++ 3 files changed, 623 insertions(+), 8 deletions(-) create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/tests/test_seedream_4.rs diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_4.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_4.rs index ebde9c7339..1fe3f1d5d0 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_4.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_4.rs @@ -46,9 +46,10 @@ fn resolve_image_list_ref<'a>( match image_list_ref { None => Ok(None), Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)), - Some(ImageListRef::Urls(_)) => { - Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens)) - } + // Omni-gen distillation hydrates media tokens to URLs before running the + // Artcraft cost path. Cost only depends on num_images, so URL-form inputs + // are accepted and dropped. + Some(ImageListRef::Urls(_)) => Ok(None), } } @@ -351,16 +352,16 @@ mod tests { // ── Image inputs ────────────────────────────────────────────────────────── #[test] - fn url_image_inputs_returns_error() { + fn url_image_inputs_are_accepted_for_cost_path() { let urls = vec!["https://example.com/image.jpg".to_string()]; let request = GenerateImageRequest { image_inputs: Some(ImageListRef::Urls(&urls)), ..base_seedream_4_image_request() }; - assert!(matches!( - request.build(), - Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens)) - )); + let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { + panic!("expected ArtcraftSeedream4") + }; + assert!(plan.image_inputs.is_none()); } #[test] diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/tests/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/tests/mod.rs index 3eb77153f4..b0df61b121 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/tests/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/tests/mod.rs @@ -7,3 +7,4 @@ mod test_gpt_image_1p5; mod test_nano_banana; mod test_nano_banana_2; mod test_nano_banana_pro; +mod test_seedream_4; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/tests/test_seedream_4.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/tests/test_seedream_4.rs new file mode 100644 index 0000000000..f7da203358 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/tests/test_seedream_4.rs @@ -0,0 +1,613 @@ +//! Tests for omni-gen against the Seedream 4 model. +//! +//! Seedream 4 is multi-function: text-to-image and image-to-image (edit). +//! +//! Artcraft-tier pricing: flat 3¢/image regardless of size, quality, or mode. +//! +//! The Fal execution plan maps aspect ratios to Seedream-native image sizes: +//! Square, SquareHd, LandscapeFourThree, LandscapeSixteenNine, +//! PortraitFourThree, PortraitSixteenNine, Auto, Auto2k, Auto4k. +//! Auto in t2i mode falls back to Square; in edit mode it passes through. +//! Unsupported ratios (5:4, 3:2, 21:9, 4:5, 2:3, 9:21) fall back under +//! PayMoreUpgrade to the nearest 16:9 or 9:16 variant. + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest; + use enums::common::generation::common_aspect_ratio::CommonAspectRatio; + use enums::common::generation::common_image_model::CommonImageModel; + use tokens::tokens::media_files::MediaFileToken; + use url::Url; + + use crate::http_server::endpoints::omni_gen::generate::image::distill_image_request::{ + distill_image_request, DistilledImageRequest, + }; + + // ── Helpers ──────────────────────────────────────────────────────────────── + + fn make_request( + prompt: Option<&str>, + aspect_ratio: Option, + image_batch_count: Option, + image_media_tokens: Option>, + ) -> OmniGenImageCostAndGenerateRequest { + OmniGenImageCostAndGenerateRequest { + idempotency_token: Some("11111111-1111-1111-1111-111111111111".to_string()), + model: Some(CommonImageModel::Seedream4), + prompt: prompt.map(|s| s.to_string()), + image_media_tokens, + resolution: None, + aspect_ratio, + quality: None, + image_batch_count, + horizontal_angle: None, + vertical_angle: None, + zoom: None, + } + } + + fn fake_image_refs(n: usize) -> (Vec, HashMap) { + let mut tokens = Vec::with_capacity(n); + let mut map = HashMap::with_capacity(n); + for i in 0..n { + let token = MediaFileToken::new_from_str(&format!("mf_test{:028}", i)); + let url = Url::parse(&format!("https://fake.example.com/img{}.png", i)).unwrap(); + map.insert(token.clone(), url); + tokens.push(token); + } + (tokens, map) + } + + fn distill_text(request: &OmniGenImageCostAndGenerateRequest) -> DistilledImageRequest { + let empty: HashMap = HashMap::new(); + distill_image_request(request, Some(&empty)) + .expect("distill_image_request should succeed for Seedream 4 (text)") + } + + fn distill_edit( + request: &OmniGenImageCostAndGenerateRequest, + hydration: &HashMap, + ) -> DistilledImageRequest { + distill_image_request(request, Some(hydration)) + .expect("distill_image_request should succeed for Seedream 4 (edit)") + } + + // ──────────────────────────────────────────────────────────────────────────── + // COST — flat 3¢/image + // ──────────────────────────────────────────────────────────────────────────── + mod cost { + use super::*; + + mod text { + use super::*; + + fn cost_for_batch(batch: Option) -> u64 { + let request = make_request(Some("p"), None, batch, None); + distill_text(&request).cost.cost_in_usd_cents.unwrap() + } + + #[test] + fn default_batch_costs_3_cents() { + assert_eq!(cost_for_batch(None), 3); + } + + #[test] + fn batch_of_one_costs_3() { assert_eq!(cost_for_batch(Some(1)), 3); } + + #[test] + fn batch_of_two_costs_6() { assert_eq!(cost_for_batch(Some(2)), 6); } + + #[test] + fn batch_of_three_costs_9() { assert_eq!(cost_for_batch(Some(3)), 9); } + + #[test] + fn batch_of_four_costs_12() { assert_eq!(cost_for_batch(Some(4)), 12); } + + #[test] + fn batch_above_max_clamps_to_12() { assert_eq!(cost_for_batch(Some(7)), 12); } + + #[test] + fn cost_is_independent_of_aspect_ratio() { + let ars = [ + None, + Some(CommonAspectRatio::Square), + Some(CommonAspectRatio::SquareHd), + Some(CommonAspectRatio::WideSixteenByNine), + Some(CommonAspectRatio::WideFourByThree), + Some(CommonAspectRatio::TallNineBySixteen), + Some(CommonAspectRatio::TallThreeByFour), + Some(CommonAspectRatio::Auto), + Some(CommonAspectRatio::Auto2k), + Some(CommonAspectRatio::Auto4k), + ]; + for ar in ars { + let request = make_request(Some("p"), ar, Some(2), None); + assert_eq!( + distill_text(&request).cost.cost_in_usd_cents.unwrap(), 6, + "expected 6¢ regardless of aspect ratio {:?}", ar, + ); + } + } + + #[test] + fn cost_is_independent_of_prompt() { + let a = distill_text(&make_request(Some("a cat"), None, Some(1), None)); + let b = distill_text(&make_request(None, None, Some(1), None)); + assert_eq!(a.cost.cost_in_usd_cents, b.cost.cost_in_usd_cents); + } + + #[test] + fn cost_metadata_flags_are_default() { + let d = distill_text(&make_request(Some("p"), None, Some(1), None)); + assert!(!d.cost.is_free); + assert!(!d.cost.is_unlimited); + assert!(!d.cost.is_rate_limited); + assert!(!d.cost.has_watermark); + } + } + + mod edit { + use super::*; + + fn cost_edit(batch: Option, num_refs: usize) -> u64 { + let (tokens, hydration) = fake_image_refs(num_refs); + let request = make_request(Some("p"), None, batch, Some(tokens)); + distill_edit(&request, &hydration).cost.cost_in_usd_cents.unwrap() + } + + #[test] + fn default_batch_costs_3() { assert_eq!(cost_edit(None, 1), 3); } + + #[test] + fn batch_of_one_costs_3() { assert_eq!(cost_edit(Some(1), 1), 3); } + + #[test] + fn batch_of_two_costs_6() { assert_eq!(cost_edit(Some(2), 1), 6); } + + #[test] + fn batch_of_four_costs_12() { assert_eq!(cost_edit(Some(4), 1), 12); } + + #[test] + fn batch_above_max_clamps_to_12() { assert_eq!(cost_edit(Some(9), 1), 12); } + + #[test] + fn cost_independent_of_input_image_count() { + for num_refs in [1usize, 2, 3, 5] { + assert_eq!(cost_edit(Some(2), num_refs), 6, "expected 6¢ regardless of {} input images", num_refs); + } + } + + #[test] + fn edit_and_text_cost_match() { + for batch in [1u16, 2, 3, 4] { + let text = distill_text(&make_request(Some("p"), None, Some(batch), None)) + .cost.cost_in_usd_cents.unwrap(); + let (tokens, hydration) = fake_image_refs(1); + let edit = distill_edit( + &make_request(Some("p"), None, Some(batch), Some(tokens)), + &hydration, + ).cost.cost_in_usd_cents.unwrap(); + assert_eq!(text, edit, "text/edit cost diverged at batch={}", batch); + } + } + + #[test] + fn cost_metadata_flags_are_default() { + let (tokens, hydration) = fake_image_refs(1); + let d = distill_edit(&make_request(Some("p"), None, Some(1), Some(tokens)), &hydration); + assert!(!d.cost.is_free); + assert!(!d.cost.is_unlimited); + assert!(!d.cost.is_rate_limited); + assert!(!d.cost.has_watermark); + } + } + } + + // ──────────────────────────────────────────────────────────────────────────── + // PLAN + // ──────────────────────────────────────────────────────────────────────────── + mod plan { + use super::*; + + use artcraft_router::generate::generate_image::image_generation_plan::ImageGenerationPlan; + use artcraft_router::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::{ + FalSeedream4ImageSize, FalSeedream4NumImages, PlanFalSeedream4, + }; + + fn with_text_plan)>( + request: &OmniGenImageCostAndGenerateRequest, + assertion: F, + ) { + let distilled = distill_text(request); + match distilled.plan() { + ImageGenerationPlan::FalSeedream4(plan) => assertion(plan), + other => panic!("expected FalSeedream4, got {:?}", other), + } + } + + fn with_edit_plan)>( + request: &OmniGenImageCostAndGenerateRequest, + hydration: &HashMap, + assertion: F, + ) { + let distilled = distill_edit(request, hydration); + match distilled.plan() { + ImageGenerationPlan::FalSeedream4(plan) => assertion(plan), + other => panic!("expected FalSeedream4, got {:?}", other), + } + } + + mod text { + use super::*; + + // ── Mode detection ──────────────────────────────────────────────────── + + #[test] + fn text_mode_has_empty_image_urls() { + with_text_plan(&make_request(Some("p"), None, Some(1), None), |plan| { + assert!(plan.image_urls.is_empty()); + }); + } + + // ── Image size: direct mappings ─────────────────────────────────────── + + #[test] + fn default_image_size_is_none() { + with_text_plan(&make_request(Some("p"), None, Some(1), None), |plan| { + assert!(plan.image_size.is_none()); + }); + } + + #[test] + fn square_yields_square() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::Square), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::Square))); + }); + } + + #[test] + fn square_hd_yields_square_hd() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::SquareHd), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::SquareHd))); + }); + } + + #[test] + fn wide_16x9_yields_landscape_sixteen_nine() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::WideSixteenByNine), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::LandscapeSixteenNine))); + }); + } + + #[test] + fn wide_alias_yields_landscape_sixteen_nine() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::Wide), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::LandscapeSixteenNine))); + }); + } + + #[test] + fn wide_4x3_yields_landscape_four_three() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::WideFourByThree), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::LandscapeFourThree))); + }); + } + + #[test] + fn tall_9x16_yields_portrait_sixteen_nine() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::TallNineBySixteen), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::PortraitSixteenNine))); + }); + } + + #[test] + fn tall_alias_yields_portrait_sixteen_nine() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::Tall), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::PortraitSixteenNine))); + }); + } + + #[test] + fn tall_3x4_yields_portrait_four_three() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::TallThreeByFour), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::PortraitFourThree))); + }); + } + + // ── Image size: Auto variants ───────────────────────────────────────── + + #[test] + fn auto_in_text_mode_yields_square() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::Auto), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::Square))); + }); + } + + #[test] + fn auto_2k_yields_auto_2k() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::Auto2k), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::Auto2k))); + }); + } + + #[test] + fn auto_4k_yields_auto_4k() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::Auto4k), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::Auto4k))); + }); + } + + // ── Image size: fallbacks (unsupported → nearest under PayMoreUpgrade) + + #[test] + fn wide_5x4_falls_back_to_landscape_sixteen_nine() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::WideFiveByFour), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::LandscapeSixteenNine))); + }); + } + + #[test] + fn wide_3x2_falls_back_to_landscape_sixteen_nine() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::WideThreeByTwo), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::LandscapeSixteenNine))); + }); + } + + #[test] + fn wide_21x9_falls_back_to_landscape_sixteen_nine() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::WideTwentyOneByNine), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::LandscapeSixteenNine))); + }); + } + + #[test] + fn tall_4x5_falls_back_to_portrait_sixteen_nine() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::TallFourByFive), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::PortraitSixteenNine))); + }); + } + + #[test] + fn tall_2x3_falls_back_to_portrait_sixteen_nine() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::TallTwoByThree), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::PortraitSixteenNine))); + }); + } + + #[test] + fn tall_9x21_falls_back_to_portrait_sixteen_nine() { + with_text_plan(&make_request(Some("p"), Some(CommonAspectRatio::TallNineByTwentyOne), Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::PortraitSixteenNine))); + }); + } + + // ── Num images ──────────────────────────────────────────────────────── + + #[test] + fn default_batch_count_is_one() { + with_text_plan(&make_request(Some("p"), None, None, None), |plan| { + assert!(matches!(plan.num_images, FalSeedream4NumImages::One)); + }); + } + + #[test] + fn batch_direct_mapping() { + for (count, expected) in [ + (1u16, FalSeedream4NumImages::One), (2, FalSeedream4NumImages::Two), + (3, FalSeedream4NumImages::Three), (4, FalSeedream4NumImages::Four), + ] { + with_text_plan(&make_request(Some("p"), None, Some(count), None), |plan| { + assert!(std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected)); + }); + } + } + + #[test] + fn batch_above_four_clamps_to_four() { + with_text_plan(&make_request(Some("p"), None, Some(9), None), |plan| { + assert!(matches!(plan.num_images, FalSeedream4NumImages::Four)); + }); + } + + // ── Prompt passthrough ──────────────────────────────────────────────── + + #[test] + fn prompt_is_passed_through() { + with_text_plan(&make_request(Some("a corgi in a hat"), None, Some(1), None), |plan| { + assert_eq!(plan.prompt, Some("a corgi in a hat")); + }); + } + + #[test] + fn missing_prompt_is_none() { + with_text_plan(&make_request(None, None, Some(1), None), |plan| { + assert_eq!(plan.prompt, None); + }); + } + } + + mod edit { + use super::*; + + // ── Mode detection / image-url passthrough ──────────────────────────── + + #[test] + fn edit_mode_populates_image_urls() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), None, Some(1), Some(tokens)), + &hydration, + |plan| { + assert_eq!(plan.image_urls.len(), 1); + assert!(plan.image_urls[0].starts_with("https://fake.example.com/")); + }, + ); + } + + #[test] + fn edit_mode_with_three_image_refs() { + let (tokens, hydration) = fake_image_refs(3); + with_edit_plan( + &make_request(Some("p"), None, Some(1), Some(tokens)), + &hydration, + |plan| { assert_eq!(plan.image_urls.len(), 3); }, + ); + } + + #[test] + fn edit_mode_with_five_image_refs() { + let (tokens, hydration) = fake_image_refs(5); + with_edit_plan( + &make_request(Some("p"), None, Some(1), Some(tokens)), + &hydration, + |plan| { assert_eq!(plan.image_urls.len(), 5); }, + ); + } + + #[test] + fn edit_image_urls_are_hydrated_from_map() { + let (tokens, hydration) = fake_image_refs(3); + let expected: Vec = tokens.iter() + .map(|t| hydration.get(t).unwrap().to_string()) + .collect(); + with_edit_plan( + &make_request(Some("p"), None, Some(1), Some(tokens.clone())), + &hydration, + |plan| { assert_eq!(plan.image_urls, expected); }, + ); + } + + // ── Auto in edit mode yields Auto (not Square) ──────────────────────── + + #[test] + fn auto_in_edit_mode_yields_auto() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), Some(CommonAspectRatio::Auto), Some(1), Some(tokens)), + &hydration, + |plan| { + assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::Auto))); + }, + ); + } + + // ── Image size direct mappings in edit mode ─────────────────────────── + + #[test] + fn edit_default_image_size_is_none() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), None, Some(1), Some(tokens)), + &hydration, + |plan| { assert!(plan.image_size.is_none()); }, + ); + } + + #[test] + fn edit_square_yields_square() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), Some(CommonAspectRatio::Square), Some(1), Some(tokens)), + &hydration, + |plan| { assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::Square))); }, + ); + } + + #[test] + fn edit_wide_16x9_yields_landscape_sixteen_nine() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), Some(CommonAspectRatio::WideSixteenByNine), Some(1), Some(tokens)), + &hydration, + |plan| { assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::LandscapeSixteenNine))); }, + ); + } + + #[test] + fn edit_tall_9x16_yields_portrait_sixteen_nine() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), Some(CommonAspectRatio::TallNineBySixteen), Some(1), Some(tokens)), + &hydration, + |plan| { assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::PortraitSixteenNine))); }, + ); + } + + #[test] + fn edit_auto_2k() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), Some(CommonAspectRatio::Auto2k), Some(1), Some(tokens)), + &hydration, + |plan| { assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::Auto2k))); }, + ); + } + + #[test] + fn edit_auto_4k() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), Some(CommonAspectRatio::Auto4k), Some(1), Some(tokens)), + &hydration, + |plan| { assert!(matches!(plan.image_size, Some(FalSeedream4ImageSize::Auto4k))); }, + ); + } + + // ── Num images in edit mode ─────────────────────────────────────────── + + #[test] + fn edit_default_batch_count_is_one() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), None, None, Some(tokens)), + &hydration, + |plan| { assert!(matches!(plan.num_images, FalSeedream4NumImages::One)); }, + ); + } + + #[test] + fn edit_batch_of_four_yields_four() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), None, Some(4), Some(tokens)), + &hydration, + |plan| { assert!(matches!(plan.num_images, FalSeedream4NumImages::Four)); }, + ); + } + + #[test] + fn edit_batch_above_four_clamps_to_four() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), None, Some(9), Some(tokens)), + &hydration, + |plan| { assert!(matches!(plan.num_images, FalSeedream4NumImages::Four)); }, + ); + } + + // ── Prompt passthrough ──────────────────────────────────────────────── + + #[test] + fn edit_prompt_is_passed_through() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("make it shiny"), None, Some(1), Some(tokens)), + &hydration, + |plan| { assert_eq!(plan.prompt, Some("make it shiny")); }, + ); + } + + #[test] + fn edit_missing_prompt_is_none() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(None, None, Some(1), Some(tokens)), + &hydration, + |plan| { assert_eq!(plan.prompt, None); }, + ); + } + } + } +}