From ee85d484c21b3e17a0bb3c4d548b455795e03407 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 21 Apr 2026 17:21:54 -0400 Subject: [PATCH] test refactor --- .claude/rules/code-style.md | 7 +- .claude/rules/testing.md | 5 +- CLAUDE.md | 2 +- crates/CLAUDE.md | 8 +- .../providers/kinovi/seedance_2p0/request.rs | 330 +++++++++--------- 5 files changed, 181 insertions(+), 171 deletions(-) diff --git a/.claude/rules/code-style.md b/.claude/rules/code-style.md index fee98b6edd..6ec4132c92 100644 --- a/.claude/rules/code-style.md +++ b/.claude/rules/code-style.md @@ -49,9 +49,14 @@ Put test cases first, helpers last. A reader opening a test file should immediat 1. **Imports** 2. **Constants** (test IDs, URLs, fixture data) — small context that tests reference -3. **Test functions** grouped by category +3. **Test functions** or **sub-modules** grouped by category 4. **Helper functions** (builders, pipeline runners, assertions) +Group related tests into sub-modules (`mod pricing_tests { ... }`) when a category has 2+ +tests. Don't create a sub-module for a single test. Keep nesting to two levels max +(`mod tests { mod category { ... } }`). Shared helpers go in the parent module so +sub-modules can reach them via `use super::*`. + ## Naming - Handler functions: `{verb}_{noun}_handler` (e.g. `get_health_check_handler`, `login_handler`) diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md index 56c0b120a6..9f1c6ee9db 100644 --- a/.claude/rules/testing.md +++ b/.claude/rules/testing.md @@ -10,7 +10,10 @@ ## Test Patterns - Tests live in `#[cfg(test)] mod tests { ... }` at the bottom of each file -- Group related tests in sub-modules: `mod explicit_checks`, `mod mechanical_checks` +- Group related tests into sub-modules by category (e.g. `mod pricing_tests`, `mod resolution_tests`) +- Only create a sub-module when it has 2+ tests — don't wrap a single test in its own module +- Keep nesting shallow: `mod tests { mod category { ... } }` is fine; a third level is rarely needed +- Shared helpers used by multiple sub-modules belong in the parent `mod tests`, not duplicated in each sub-module - Enum tests should cover: serialization, deserialization, `to_str`/`from_str` round-trip, variant count - Password/crypto tests: be mindful of computation cost, don't add too many bcrypt test cases diff --git a/CLAUDE.md b/CLAUDE.md index 37ab0b9157..cc777e9bf7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,4 +75,4 @@ Organize for top-to-bottom reading. Important things first, details later. - **In impl blocks**: constructors first, then public methods, then private helpers - Private helpers go *below* the methods that call them - Among helpers: meatier logic above leaf-level formatters -- **In test modules**: constants first, then test cases, then helper functions last +- **In test modules**: constants first, then test cases (grouped into sub-modules when 2+), then helper functions last diff --git a/crates/CLAUDE.md b/crates/CLAUDE.md index b1ce7dd2d4..c74de1c34c 100644 --- a/crates/CLAUDE.md +++ b/crates/CLAUDE.md @@ -29,11 +29,15 @@ Within `#[cfg(test)] mod tests { ... }`: 1. **Imports** 2. **Constants** (test IDs, URLs, fixture data) — small context that tests reference -3. **Test functions** (`#[test]` / `#[tokio::test]`) grouped by category +3. **Test functions or sub-modules** grouped by category 4. **Helper functions** (builders, pipeline runners, assertions) Constants provide small context up front. Test cases come next so engineers see *what* is -tested before *how* the helpers work. The same ordering applies within test sub-modules. +tested before *how* the helpers work. + +Group related tests into sub-modules (`mod pricing_tests { ... }`) when a category has 2+ +tests. Don't wrap a single test in its own module. Keep nesting to two levels max. Shared +helpers go in the parent `mod tests` so sub-modules can `use super::*`. ## Building diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/kinovi/seedance_2p0/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/kinovi/seedance_2p0/request.rs index f616f4ba74..b42a4cee05 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/kinovi/seedance_2p0/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/kinovi/seedance_2p0/request.rs @@ -56,186 +56,184 @@ mod tests { const STEAMPUNK_CLOWN_KINOVI_ID: &str = "char_1775176566518_sik0te"; const MOCHI_KINOVI_ID: &str = "char_1775177718294_g2pitx"; - // ── Aspect ratio tests ── + mod aspect_ratio_tests { + use super::*; - #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost - async fn test_text_to_video_landscape() { - let response = run_pipeline(GenerateVideoRequestBuilder { - prompt: Some("A corgi running through a field of wildflowers at sunset.".to_string()), - aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), - ..seedance2pro_builder() - }).await; - assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); - assert_eq!(1, 2, "Inspect output above"); + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn landscape() { + let response = run_pipeline(GenerateVideoRequestBuilder { + prompt: Some("A corgi running through a field of wildflowers at sunset.".to_string()), + aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + ..seedance2pro_builder() + }).await; + assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); + assert_eq!(1, 2, "Inspect output above"); + } + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn portrait() { + let response = run_pipeline(GenerateVideoRequestBuilder { + prompt: Some("A cat sitting on a windowsill watching rain.".to_string()), + aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen), + ..seedance2pro_builder() + }).await; + assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); + assert_eq!(1, 2, "Inspect output above"); + } + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn square() { + let response = run_pipeline(GenerateVideoRequestBuilder { + prompt: Some("A hummingbird hovering near a flower.".to_string()), + aspect_ratio: Some(CommonAspectRatio::Square), + ..seedance2pro_builder() + }).await; + assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); + assert_eq!(1, 2, "Inspect output above"); + } } - #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost - async fn test_text_to_video_portrait() { - let response = run_pipeline(GenerateVideoRequestBuilder { - prompt: Some("A cat sitting on a windowsill watching rain.".to_string()), - aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen), - ..seedance2pro_builder() - }).await; - assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); - assert_eq!(1, 2, "Inspect output above"); + mod resolution_tests { + use super::*; + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn res_480p() { + let response = run_pipeline(GenerateVideoRequestBuilder { + prompt: Some("A shiba inu playing in autumn leaves.".to_string()), + resolution: Some(CommonResolution::FourEightyP), + ..seedance2pro_builder() + }).await; + assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); + assert_eq!(1, 2, "Inspect output above"); + } + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn res_720p() { + let response = run_pipeline(GenerateVideoRequestBuilder { + prompt: Some("A fox walking through a snowy forest.".to_string()), + resolution: Some(CommonResolution::SevenTwentyP), + ..seedance2pro_builder() + }).await; + assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); + assert_eq!(1, 2, "Inspect output above"); + } + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn res_1080p() { + let response = run_pipeline(GenerateVideoRequestBuilder { + prompt: Some("A golden retriever catching a frisbee on the beach.".to_string()), + resolution: Some(CommonResolution::TenEightyP), + ..seedance2pro_builder() + }).await; + assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); + assert_eq!(1, 2, "Inspect output above"); + } } - #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost - async fn test_text_to_video_square() { - let response = run_pipeline(GenerateVideoRequestBuilder { - prompt: Some("A hummingbird hovering near a flower.".to_string()), - aspect_ratio: Some(CommonAspectRatio::Square), - ..seedance2pro_builder() - }).await; - assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); - assert_eq!(1, 2, "Inspect output above"); - } + mod modality_tests { + use super::*; - // ── Resolution tests ── + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn text_to_video() { + let response = run_pipeline(GenerateVideoRequestBuilder { + prompt: Some("A whale breaching in the open ocean at dawn, cinematic.".to_string()), + aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + ..seedance2pro_builder() + }).await; + assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); + assert_eq!(1, 2, "Inspect output above"); + } - #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost - async fn test_text_to_video_480p() { - let response = run_pipeline(GenerateVideoRequestBuilder { - prompt: Some("A shiba inu playing in autumn leaves.".to_string()), - resolution: Some(CommonResolution::FourEightyP), - ..seedance2pro_builder() - }).await; - assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); - assert_eq!(1, 2, "Inspect output above"); - } + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn keyframe_start_and_end_frame() { + let start_token = MediaFileToken::new("mf_start".to_string()); + let end_token = MediaFileToken::new("mf_end".to_string()); - #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost - async fn test_text_to_video_720p() { - let response = run_pipeline(GenerateVideoRequestBuilder { - prompt: Some("A fox walking through a snowy forest.".to_string()), - resolution: Some(CommonResolution::SevenTwentyP), - ..seedance2pro_builder() - }).await; - assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); - assert_eq!(1, 2, "Inspect output above"); - } + let mut media_map = HashMap::new(); + media_map.insert(start_token.clone(), test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL.to_string()); + media_map.insert(end_token.clone(), test_data::web::image_urls::FOREST_BACKDROP_IMAGE_URL.to_string()); - #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost - async fn test_text_to_video_1080p() { - let response = run_pipeline(GenerateVideoRequestBuilder { - prompt: Some("A golden retriever catching a frisbee on the beach.".to_string()), - resolution: Some(CommonResolution::TenEightyP), - ..seedance2pro_builder() - }).await; - assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); - assert_eq!(1, 2, "Inspect output above"); - } + let response = run_pipeline_with_media_map(GenerateVideoRequestBuilder { + prompt: Some("The dog walks from the lake to the forest.".to_string()), + start_frame: Some(ImageRef::MediaFileToken(start_token)), + end_frame: Some(ImageRef::MediaFileToken(end_token)), + aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + ..seedance2pro_builder() + }, media_map).await; + assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); + assert_eq!(1, 2, "Inspect output above"); + } - // ── Modality: text to video ── - - #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost - async fn test_text_to_video() { - let response = run_pipeline(GenerateVideoRequestBuilder { - prompt: Some("A whale breaching in the open ocean at dawn, cinematic.".to_string()), - aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), - ..seedance2pro_builder() - }).await; - assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); - assert_eq!(1, 2, "Inspect output above"); - } - - // ── Modality: keyframe (start + end frame) ── - - #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost - async fn test_keyframe_start_and_end_frame() { - let start_token = MediaFileToken::new("mf_start".to_string()); - let end_token = MediaFileToken::new("mf_end".to_string()); - - let mut media_map = HashMap::new(); - media_map.insert(start_token.clone(), test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL.to_string()); - media_map.insert(end_token.clone(), test_data::web::image_urls::FOREST_BACKDROP_IMAGE_URL.to_string()); - - let response = run_pipeline_with_media_map(GenerateVideoRequestBuilder { - prompt: Some("The dog walks from the lake to the forest.".to_string()), - start_frame: Some(ImageRef::MediaFileToken(start_token)), - end_frame: Some(ImageRef::MediaFileToken(end_token)), - aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), - ..seedance2pro_builder() - }, media_map).await; - assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); - assert_eq!(1, 2, "Inspect output above"); - } - - // ── Modality: image references ── - - #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost - async fn test_image_references() { - let response = run_pipeline(GenerateVideoRequestBuilder { - prompt: Some("The dog in @2 runs through the scenery in @1 towards the building in @3.".to_string()), - reference_images: Some(ImageListRef::Urls(vec![ - test_data::web::image_urls::FOREST_BACKDROP_IMAGE_URL.to_string(), - test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL.to_string(), - test_data::web::image_urls::WHITE_HOUSE_SUNSET_IMAGE_URL.to_string(), - ])), - aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), - ..seedance2pro_builder() - }).await; - assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); - assert_eq!(1, 2, "Inspect output above"); - } - - // ── Modality: video reference ── - - #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost - async fn test_video_reference() { - // NB: This URL is a pre-uploaded video on the Seedance2Pro CDN from prior tests. - let response = run_pipeline(GenerateVideoRequestBuilder { - prompt: Some("Change @video1 to a nighttime scene with moonlight.".to_string()), - reference_videos: Some(VideoListRef::Urls(vec![ - "https://static.seedance2-pro.com/materials/20260315/1773594284659-3a46d231.mp4".to_string(), - ])), - aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), - ..seedance2pro_builder() - }).await; - assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); - assert_eq!(1, 2, "Inspect output above"); - } - - // ── Modality: character references ── - - #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost - async fn test_character_references() { - let char_clown = CharacterToken::new("char_clown".to_string()); - let char_mochi = CharacterToken::new("char_mochi".to_string()); - - let mut character_map = HashMap::new(); - character_map.insert(char_clown.clone(), STEAMPUNK_CLOWN_KINOVI_ID.to_string()); - character_map.insert(char_mochi.clone(), MOCHI_KINOVI_ID.to_string()); - - let response = run_pipeline_with_maps( - GenerateVideoRequestBuilder { - prompt: Some("@Steampunk Clown and @Mochi are playing fetch in a sunny park.".to_string()), - reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![ - char_clown, - char_mochi, + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn image_references() { + let response = run_pipeline(GenerateVideoRequestBuilder { + prompt: Some("The dog in @2 runs through the scenery in @1 towards the building in @3.".to_string()), + reference_images: Some(ImageListRef::Urls(vec![ + test_data::web::image_urls::FOREST_BACKDROP_IMAGE_URL.to_string(), + test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL.to_string(), + test_data::web::image_urls::WHITE_HOUSE_SUNSET_IMAGE_URL.to_string(), ])), aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), ..seedance2pro_builder() - }, - HashMap::new(), - character_map, - ).await; - assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); - assert_eq!(1, 2, "Inspect output above"); - } + }).await; + assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); + assert_eq!(1, 2, "Inspect output above"); + } - // TODO: Add audio reference test once a web-accessible audio URL is available in test_data. + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn video_reference() { + // NB: This URL is a pre-uploaded video on the Seedance2Pro CDN from prior tests. + let response = run_pipeline(GenerateVideoRequestBuilder { + prompt: Some("Change @video1 to a nighttime scene with moonlight.".to_string()), + reference_videos: Some(VideoListRef::Urls(vec![ + "https://static.seedance2-pro.com/materials/20260315/1773594284659-3a46d231.mp4".to_string(), + ])), + aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + ..seedance2pro_builder() + }).await; + assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); + assert_eq!(1, 2, "Inspect output above"); + } + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn character_references() { + let char_clown = CharacterToken::new("char_clown".to_string()); + let char_mochi = CharacterToken::new("char_mochi".to_string()); + + let mut character_map = HashMap::new(); + character_map.insert(char_clown.clone(), STEAMPUNK_CLOWN_KINOVI_ID.to_string()); + character_map.insert(char_mochi.clone(), MOCHI_KINOVI_ID.to_string()); + + let response = run_pipeline_with_maps( + GenerateVideoRequestBuilder { + prompt: Some("@Steampunk Clown and @Mochi are playing fetch in a sunny park.".to_string()), + reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![ + char_clown, + char_mochi, + ])), + aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + ..seedance2pro_builder() + }, + HashMap::new(), + character_map, + ).await; + assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_))); + assert_eq!(1, 2, "Inspect output above"); + } + + // TODO: Add audio reference test once a web-accessible audio URL is available in test_data. + } // ── Helpers ──