use super::*; #[test] fn puzzle_generated_image_size_is_square_1_1() { assert_eq!(PUZZLE_GENERATED_IMAGE_SIZE, "1024*1024"); assert_eq!(PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE, "1024x1024"); } #[test] fn puzzle_vector_engine_request_uses_gpt_image_2_all_and_reference_images() { let body = build_puzzle_vector_engine_image_request_body( PuzzleImageModel::Gemini31FlashPreview, "一只猫在雨夜灯牌下回头。", PUZZLE_DEFAULT_NEGATIVE_PROMPT, PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE, 4, None, ); assert_eq!(body["model"], VECTOR_ENGINE_GPT_IMAGE_2_MODEL); assert_eq!(body["size"], PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE); assert_eq!(body["n"], 1); assert!(body.get("official_fallback").is_none()); assert!(body.get("image").is_none()); assert!( body["prompt"] .as_str() .unwrap_or_default() .contains("文字水印") ); } #[test] fn puzzle_vector_engine_generation_fallback_includes_reference_image() { let image = image::DynamicImage::ImageRgb8(image::RgbImage::new(4, 4)); let mut cursor = std::io::Cursor::new(Vec::new()); image .write_to(&mut cursor, ImageFormat::Png) .expect("test image should encode"); let reference_image = PuzzleResolvedReferenceImage { mime_type: "image/png".to_string(), bytes_len: cursor.get_ref().len(), bytes: cursor.into_inner(), }; let body = build_puzzle_vector_engine_image_request_body( PuzzleImageModel::GptImage2, "参考图里的小猫做成拼图主图。", PUZZLE_DEFAULT_NEGATIVE_PROMPT, PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE, 1, Some(&reference_image), ); let images = body["image"] .as_array() .expect("fallback generation should include reference image array"); assert_eq!(images.len(), 1); assert!( images[0] .as_str() .unwrap_or_default() .starts_with("data:image/png;base64,") ); } #[test] fn puzzle_vector_engine_edit_url_uses_images_edits_endpoint() { let settings = PuzzleVectorEngineSettings { base_url: "https://vector.example/v1".to_string(), api_key: "test-key".to_string(), }; assert_eq!( puzzle_vector_engine_images_edit_url(&settings), "https://vector.example/v1/images/edits" ); } #[test] fn puzzle_vector_engine_edit_response_decodes_b64_image() { let images = puzzle_images_from_base64( "edit-1".to_string(), vec![BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nrest")], 1, ); assert_eq!(images.images.len(), 1); assert_eq!(images.images[0].mime_type, "image/png"); assert_eq!(images.images[0].extension, "png"); } #[test] fn puzzle_vector_engine_prompt_strongly_uses_reference_image() { let prompt = build_puzzle_vector_engine_generation_prompt("请生成雨夜猫街。", true); assert!(prompt.contains("参考图作为第一优先级")); assert!(prompt.contains("严格保留参考图的主要主体、构图关系、视角、姿态、配色和光影氛围")); assert!(prompt.contains("请生成雨夜猫街。")); } #[test] fn puzzle_vector_engine_prompt_keeps_text_only_prompt_unchanged() { let prompt = build_puzzle_vector_engine_generation_prompt("请生成雨夜猫街。", false); assert_eq!(prompt, "请生成雨夜猫街。"); } #[test] fn puzzle_reference_image_edit_requires_ai_redraw() { assert!(!should_use_puzzle_reference_image_edit(None, true)); assert!(!should_use_puzzle_reference_image_edit( Some("data:image/png;base64,abcd"), false )); assert!(should_use_puzzle_reference_image_edit( Some("data:image/png;base64,abcd"), true )); } #[test] fn puzzle_reference_image_sources_are_deduped_and_limited() { let sources = collect_puzzle_reference_image_sources( Some("data:image/png;base64,a"), &[ "data:image/png;base64,a".to_string(), "data:image/png;base64,b".to_string(), "data:image/png;base64,c".to_string(), "data:image/png;base64,d".to_string(), "data:image/png;base64,e".to_string(), "data:image/png;base64,f".to_string(), ], ); assert_eq!(sources.len(), 5); assert_eq!(sources[0], "data:image/png;base64,a"); assert_eq!(sources[1], "data:image/png;base64,b"); assert!(!sources.contains(&"data:image/png;base64,f".to_string())); } #[test] fn puzzle_vector_engine_timeout_maps_to_gateway_timeout() { let error = map_puzzle_vector_engine_request_error( "创建拼图 VectorEngine 图片生成任务失败:operation timed out".to_string(), ); let response = error.into_response(); assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT); } #[test] fn puzzle_vector_engine_upstream_timeout_maps_to_gateway_timeout() { let error = map_puzzle_vector_engine_upstream_error( reqwest::StatusCode::GATEWAY_TIMEOUT, r#"{"error":{"message":"VectorEngine edit endpoint timeout"}}"#, "创建拼图 VectorEngine 图片编辑任务失败", ); let response = error.into_response(); assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT); } #[test] fn puzzle_reference_edit_fallback_only_accepts_timeout_errors() { let timeout_error = map_puzzle_vector_engine_upstream_error( reqwest::StatusCode::GATEWAY_TIMEOUT, r#"{"error":{"message":"VectorEngine edit endpoint timeout"}}"#, "创建拼图 VectorEngine 图片编辑任务失败", ); assert!(should_fallback_puzzle_reference_edit_to_generation( &timeout_error )); let auth_error = map_puzzle_vector_engine_upstream_error( reqwest::StatusCode::UNAUTHORIZED, r#"{"error":{"message":"invalid api key"}}"#, "创建拼图 VectorEngine 图片编辑任务失败", ); assert!(!should_fallback_puzzle_reference_edit_to_generation( &auth_error )); } #[test] fn puzzle_vector_engine_reqwest_error_maps_to_bad_gateway() { let error = match reqwest::Client::new().get("http://[::1").build() { Ok(_) => panic!("invalid url should fail request build"), Err(error) => error, }; let app_error = map_puzzle_vector_engine_reqwest_error( "创建拼图 VectorEngine 图片编辑任务失败", "https://api.vectorengine.ai/v1/images/edits", error, ); let response = app_error.into_response(); assert_eq!(response.status(), StatusCode::BAD_GATEWAY); } #[test] fn puzzle_compile_error_preserves_vector_engine_unavailable_status() { let error = map_puzzle_compile_error(SpacetimeClientError::Runtime( "VECTOR_ENGINE_API_KEY 未配置".to_string(), )); let response = error.into_response(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); } #[tokio::test] async fn puzzle_compile_error_normalizes_legacy_apimart_image_message() { let error = map_puzzle_compile_error(SpacetimeClientError::Runtime( "APIMart 图片生成密钥未配置".to_string(), )); let response = error.into_response(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); let body = response.into_body(); let bytes = axum::body::to_bytes(body, usize::MAX) .await .expect("body bytes should read"); let payload: Value = serde_json::from_slice(&bytes).expect("error response should be valid json"); assert_eq!( payload["error"]["details"]["provider"], Value::String(VECTOR_ENGINE_PROVIDER.to_string()) ); assert_eq!( payload["error"]["details"]["message"], Value::String("VectorEngine 图片生成密钥未配置".to_string()) ); } #[test] fn puzzle_image_generation_builds_fallback_session_from_levels_snapshot() { let levels_json = serde_json::to_string(&vec![json!({ "level_id": "puzzle-level-1", "level_name": "雨夜猫街", "picture_description": "一只猫在雨夜灯牌下回头。", "candidates": [], "selected_candidate_id": null, "cover_image_src": null, "cover_asset_id": null, "generation_status": "idle", })]) .expect("levels json"); let payload = ExecutePuzzleAgentActionRequest { action: "generate_puzzle_images".to_string(), prompt_text: None, reference_image_src: None, reference_image_srcs: Vec::new(), image_model: Some(PUZZLE_IMAGE_MODEL_GPT_IMAGE_2.to_string()), ai_redraw: None, candidate_count: Some(1), candidate_id: None, level_id: Some("puzzle-level-1".to_string()), work_title: Some("暖灯猫街作品".to_string()), work_description: Some("一套雨夜猫街主题拼图。".to_string()), picture_description: None, level_name: None, summary: Some("当前关卡画面。".to_string()), theme_tags: Some(vec!["猫咪".to_string(), "雨夜".to_string()]), levels_json: Some(levels_json.clone()), }; let session = build_puzzle_session_snapshot_from_action_payload( "puzzle-session-1", &payload, Some(levels_json.as_str()), 1_713_686_401_234_567, ) .expect("fallback session"); let draft = session.draft.expect("draft"); assert_eq!(session.stage, "ready_to_publish"); assert_eq!(draft.work_title, "暖灯猫街作品"); assert_eq!(draft.theme_tags, vec!["猫咪", "雨夜"]); assert_eq!(draft.levels[0].level_id, "puzzle-level-1"); assert_eq!( draft.levels[0].picture_description, "一只猫在雨夜灯牌下回头。" ); } #[test] fn puzzle_first_level_name_parser_accepts_json_and_normalizes_text() { assert_eq!( parse_puzzle_first_level_name_from_text(r#"{"levelName":"雨夜猫街"}"#), Some("雨夜猫街".to_string()) ); assert_eq!( parse_puzzle_first_level_name_from_text("1. 《暖灯猫街》"), Some("暖灯猫街".to_string()) ); assert_eq!( parse_puzzle_first_level_name_from_text(r#"{"levelName":"雨夜猫街画面"}"#), Some("雨夜猫街".to_string()) ); assert_eq!( parse_puzzle_first_level_name_from_text(r#"{"levelNam"#), None ); } #[test] fn puzzle_level_naming_parser_accepts_metadata_and_ui_background_prompt() { let naming = parse_puzzle_level_naming_from_text( r#"{"levelName":"雨夜猫街","workDescription":"在湿润灯牌与猫影之间完成一套雨夜街角拼图。","workTags":["雨夜","猫咪","灯牌","街角","暖色","插画"],"uiBackgroundPrompt":"雨夜老街延展成竖屏空间,湿润石板路倒映暖色灯牌,远处屋檐和薄雾形成柔和层次"}"#, ) .expect("naming should parse"); assert_eq!(naming.level_name, "雨夜猫街"); assert_eq!( naming.work_description.as_deref(), Some("在湿润灯牌与猫影之间完成一套雨夜街角拼图") ); assert_eq!(naming.work_tags.len(), module_puzzle::PUZZLE_MAX_TAG_COUNT); assert!(naming.work_tags.contains(&"雨夜".to_string())); assert!(naming.work_tags.contains(&"猫咪".to_string())); assert!(naming.work_tags.contains(&"灯牌".to_string())); assert_eq!( naming.ui_background_prompt.as_deref(), Some("雨夜老街延展成竖屏空间,湿润石板路倒映暖色灯牌,远处屋檐和薄雾形成柔和层次") ); } #[test] fn puzzle_level_naming_parser_filters_forbidden_ui_prompt_words() { let naming = parse_puzzle_level_naming_from_text( r#"{"levelName":"雨夜猫街","uiBackgroundPrompt":"雨夜老街背景,中央不要出现拼图槽、棋盘、HUD、按钮、文字或水印,保留暖色灯光"}"#, ) .expect("naming should parse"); let prompt = naming .ui_background_prompt .as_deref() .expect("prompt should parse"); assert!(!prompt.contains("拼图槽")); assert!(!prompt.contains("棋盘")); assert!(!prompt.contains("HUD")); assert!(!prompt.contains("按钮")); assert!(!prompt.contains("文字")); assert!(!prompt.contains("水印")); } #[test] fn puzzle_first_level_name_fallback_uses_picture_keywords() { assert_eq!( build_fallback_puzzle_first_level_name("一只猫在雨夜灯牌下回头。"), "雨夜猫街" ); assert_eq!( build_fallback_puzzle_first_level_name("看不出关键词的抽象色块。"), "奇境初见" ); } #[test] fn puzzle_level_name_image_data_url_downsizes_generated_image() { let image = image::DynamicImage::ImageRgb8(image::RgbImage::new(4, 4)); let mut cursor = std::io::Cursor::new(Vec::new()); image .write_to(&mut cursor, ImageFormat::Png) .expect("test image should encode"); let downloaded = PuzzleDownloadedImage { extension: "png".to_string(), mime_type: "image/png".to_string(), bytes: cursor.into_inner(), }; let data_url = build_puzzle_level_name_image_data_url(&downloaded).expect("data url should be generated"); assert!(data_url.starts_with("data:image/png;base64,")); assert!(data_url.len() > "data:image/png;base64,".len()); } #[test] fn puzzle_first_level_name_snapshot_defaults_work_title() { let levels_json = serde_json::to_string(&vec![json!({ "level_id": "puzzle-level-1", "level_name": "猫画面", "picture_description": "一只猫在雨夜灯牌下回头。", "candidates": [], "selected_candidate_id": null, "cover_image_src": null, "cover_asset_id": null, "generation_status": "idle", })]) .expect("levels json"); let payload = ExecutePuzzleAgentActionRequest { action: "generate_puzzle_images".to_string(), prompt_text: None, reference_image_src: None, reference_image_srcs: Vec::new(), image_model: Some(PUZZLE_IMAGE_MODEL_GPT_IMAGE_2.to_string()), ai_redraw: None, candidate_count: Some(1), candidate_id: None, level_id: Some("puzzle-level-1".to_string()), work_title: Some("猫画面".to_string()), work_description: None, picture_description: None, level_name: None, summary: None, theme_tags: Some(vec![]), levels_json: Some(levels_json.clone()), }; let session = build_puzzle_session_snapshot_from_action_payload( "puzzle-session-1", &payload, Some(levels_json.as_str()), 1_713_686_401_234_567, ) .expect("fallback session"); let renamed = apply_generated_puzzle_first_level_name_to_session_snapshot( session, "puzzle-level-1", "雨夜猫街", "猫画面", 1_713_686_401_234_568, ); let draft = renamed.draft.expect("draft"); assert_eq!(draft.level_name, "雨夜猫街"); assert_eq!(draft.work_title, "雨夜猫街"); assert_eq!(draft.levels[0].level_name, "雨夜猫街"); } #[test] fn puzzle_initial_metadata_defaults_empty_work_description_and_tags() { let mut session = PuzzleAgentSessionRecord { session_id: "puzzle-session-1".to_string(), seed_text: "画面描述:一只猫在雨夜灯牌下回头。".to_string(), current_turn: 1, progress_percent: 94, stage: "ready_to_publish".to_string(), anchor_pack: test_puzzle_anchor_pack_record(), draft: Some(test_puzzle_draft_record()), messages: Vec::new(), last_assistant_reply: None, published_profile_id: None, suggested_actions: Vec::new(), result_preview: None, updated_at: "2024-01-01T00:00:00Z".to_string(), }; { let draft = session.draft.as_mut().expect("draft"); draft.work_title = "猫画面".to_string(); draft.work_description = String::new(); draft.summary = String::new(); draft.theme_tags = Vec::new(); } let metadata = PuzzleLevelNaming { level_name: "雨夜猫街".to_string(), work_description: Some("在湿润灯牌与猫影之间完成一套雨夜街角拼图".to_string()), work_tags: vec![ "插画".to_string(), "灯牌".to_string(), "街角".to_string(), "猫咪".to_string(), "暖色".to_string(), "雨夜".to_string(), ], ui_background_prompt: None, }; let session = apply_generated_puzzle_initial_metadata_to_session_snapshot( session, &metadata, "猫画面", 1_713_686_401_234_568, ); let draft = session.draft.expect("draft"); assert_eq!(draft.work_title, "雨夜猫街"); assert_eq!( draft.work_description, "在湿润灯牌与猫影之间完成一套雨夜街角拼图" ); assert_eq!(draft.summary, draft.work_description); assert_eq!(draft.theme_tags, metadata.work_tags); } #[test] fn puzzle_level_audio_asset_roundtrips_between_response_and_module_json() { let level = PuzzleDraftLevelResponse { level_id: "puzzle-level-1".to_string(), level_name: "雨夜猫街".to_string(), picture_description: "一只猫在雨夜灯牌下回头。".to_string(), picture_reference: None, ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, background_music: Some(CreationAudioAsset { task_id: "suno-task-1".to_string(), provider: "vector-engine-suno".to_string(), asset_object_id: Some("assetobj_1".to_string()), asset_kind: Some("puzzle_background_music".to_string()), audio_src: "/generated-puzzle-assets/audio.mp3".to_string(), prompt: Some("轻快拼图音乐".to_string()), title: Some("雨夜猫街背景音乐".to_string()), updated_at: Some("2026-05-11T00:00:00Z".to_string()), }), candidates: vec![], selected_candidate_id: None, cover_image_src: None, cover_asset_id: None, generation_status: "ready".to_string(), }; let request_context = RequestContext::new( "test-request".to_string(), "PUT /api/runtime/puzzle/works/test".to_string(), Duration::ZERO, false, ); let levels_json = serialize_puzzle_levels_response(&request_context, &[level]) .expect("levels should serialize"); let payload: Value = serde_json::from_str(&levels_json).expect("levels json should parse"); assert_eq!( payload[0]["background_music"]["audio_src"], Value::String("/generated-puzzle-assets/audio.mp3".to_string()) ); assert!(payload[0]["background_music"].get("audioSrc").is_none()); let records = parse_puzzle_level_records_from_module_json(&levels_json) .expect("levels should map back into records"); let music = records[0] .background_music .as_ref() .expect("background music should exist"); assert_eq!(music.audio_src, "/generated-puzzle-assets/audio.mp3"); assert_eq!(music.asset_kind.as_deref(), Some("puzzle_background_music")); let response = map_puzzle_draft_level_response(records[0].clone()); assert_eq!( response .background_music .as_ref() .map(|asset| asset.audio_src.as_str()), Some("/generated-puzzle-assets/audio.mp3") ); } #[test] fn puzzle_ui_background_fields_roundtrip_between_response_and_module_json() { let level = PuzzleDraftLevelResponse { level_id: "puzzle-level-1".to_string(), level_name: "雨夜猫街".to_string(), picture_description: "一只猫在雨夜灯牌下回头。".to_string(), picture_reference: None, ui_background_prompt: Some("雨夜猫街竖屏拼图UI背景".to_string()), ui_background_image_src: Some( "/generated-puzzle-assets/session/ui/background.png".to_string(), ), ui_background_image_object_key: Some( "generated-puzzle-assets/session/ui/background.png".to_string(), ), background_music: None, candidates: vec![], selected_candidate_id: None, cover_image_src: Some("/generated-puzzle-assets/session/cover.png".to_string()), cover_asset_id: Some("asset-1".to_string()), generation_status: "ready".to_string(), }; let request_context = RequestContext::new( "test-request".to_string(), "PUT /api/runtime/puzzle/works/test".to_string(), Duration::ZERO, false, ); let levels_json = serialize_puzzle_levels_response(&request_context, &[level]) .expect("levels should serialize"); let payload: Value = serde_json::from_str(&levels_json).expect("levels json should parse"); assert_eq!( payload[0]["ui_background_prompt"], Value::String("雨夜猫街竖屏拼图UI背景".to_string()) ); assert!(payload[0].get("uiBackgroundPrompt").is_none()); let records = parse_puzzle_level_records_from_module_json(&levels_json) .expect("levels should map back into records"); assert_eq!( records[0].ui_background_image_src.as_deref(), Some("/generated-puzzle-assets/session/ui/background.png") ); let response = map_puzzle_draft_level_response(records[0].clone()); assert_eq!( response.ui_background_image_object_key.as_deref(), Some("generated-puzzle-assets/session/ui/background.png") ); } #[test] fn puzzle_work_summary_response_keeps_levels_for_shelf_cover() { let state = AppState::new(crate::config::AppConfig::default()).expect("state should build"); let level = PuzzleDraftLevelRecord { level_id: "puzzle-level-1".to_string(), level_name: "雨夜猫街".to_string(), picture_description: "一只猫在雨夜灯牌下回头。".to_string(), picture_reference: None, ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, background_music: None, candidates: vec![PuzzleGeneratedImageCandidateRecord { candidate_id: "candidate-1".to_string(), image_src: "/generated-puzzle-assets/session/candidate-1.png".to_string(), asset_id: "asset-1".to_string(), prompt: "雨夜猫街".to_string(), actual_prompt: None, source_type: "generated".to_string(), selected: true, }], selected_candidate_id: Some("candidate-1".to_string()), cover_image_src: Some("/generated-puzzle-assets/session/cover.png".to_string()), cover_asset_id: Some("asset-1".to_string()), generation_status: "ready".to_string(), }; let response = map_puzzle_work_summary_response( &state, PuzzleWorkProfileRecord { work_id: "puzzle-work-1".to_string(), profile_id: "puzzle-profile-1".to_string(), owner_user_id: "user-1".to_string(), source_session_id: Some("puzzle-session-1".to_string()), author_display_name: "玩家".to_string(), work_title: "雨夜猫街".to_string(), work_description: "一只猫在雨夜灯牌下回头。".to_string(), level_name: "雨夜猫街".to_string(), summary: "一只猫在雨夜灯牌下回头。".to_string(), theme_tags: vec!["猫".to_string()], cover_image_src: None, cover_asset_id: None, publication_status: "draft".to_string(), updated_at: "2026-05-08T00:00:00.000Z".to_string(), published_at: None, play_count: 0, remix_count: 0, like_count: 0, recent_play_count_7d: 0, point_incentive_total_half_points: 0, point_incentive_claimed_points: 0, publish_ready: false, anchor_pack: test_puzzle_anchor_pack_record(), levels: vec![level], }, ); assert_eq!(response.levels.len(), 1); assert_eq!(response.generation_status.as_deref(), Some("ready")); assert_eq!( response.levels[0].cover_image_src.as_deref(), Some("/generated-puzzle-assets/session/cover.png") ); assert_eq!( response.levels[0].candidates[0].image_src, "/generated-puzzle-assets/session/candidate-1.png" ); } #[test] fn puzzle_ui_background_prompt_keeps_generated_slots_out_of_background() { let prompt = build_puzzle_ui_background_request_prompt_for_test("雨夜猫街", "雨夜猫街主题背景"); assert!(prompt.contains("9:16")); assert!(prompt.contains("纯背景图")); assert!(prompt.contains("不得出现拼图槽")); assert!(prompt.contains("默认拼图槽")); assert!(prompt.contains("文字")); } #[test] fn puzzle_initial_ui_background_prompt_prefers_ai_generated_prompt() { let mut draft = test_puzzle_draft_record(); draft.work_title = "模板作品名".to_string(); draft.work_description = "模板作品描述".to_string(); let mut target_level = draft.levels[0].clone(); target_level.level_name = "雨夜猫街".to_string(); let ai_prompt = "雨夜老街延展成竖屏空间,湿润石板路倒映暖色灯牌,远处屋檐和薄雾形成柔和层次"; target_level.ui_background_prompt = Some(ai_prompt.to_string()); let prompt = resolve_puzzle_initial_ui_background_prompt(&draft, &target_level); assert_eq!(prompt, ai_prompt); assert!(!prompt.contains(PUZZLE_UI_BACKGROUND_PROMPT_FALLBACK_MARKER)); } #[test] fn puzzle_initial_ui_background_prompt_falls_back_to_context_template() { let draft = test_puzzle_draft_record(); let target_level = draft.levels[0].clone(); let prompt = resolve_puzzle_initial_ui_background_prompt(&draft, &target_level); assert!(prompt.contains("雨夜猫街")); assert!(prompt.contains(PUZZLE_UI_BACKGROUND_PROMPT_FALLBACK_MARKER)); } #[test] fn puzzle_ui_background_initial_attach_updates_first_level_fields() { let draft = test_puzzle_draft_record(); let generated = GeneratedPuzzleUiBackgroundResponse { image_src: "/generated-puzzle-assets/session/ui/background.png".to_string(), object_key: "generated-puzzle-assets/session/ui/background.png".to_string(), }; let mut levels = draft.levels.clone(); attach_puzzle_level_ui_background( &mut levels, "puzzle-level-1", "雨夜猫街移动端拼图UI背景".to_string(), generated, ); assert_eq!( levels[0].ui_background_prompt.as_deref(), Some("雨夜猫街移动端拼图UI背景") ); assert_eq!( levels[0].ui_background_image_src.as_deref(), Some("/generated-puzzle-assets/session/ui/background.png") ); assert_eq!( levels[0].ui_background_image_object_key.as_deref(), Some("generated-puzzle-assets/session/ui/background.png") ); } #[test] fn puzzle_initial_draft_assets_must_include_ui_background() { let mut draft = test_puzzle_draft_record(); let missing_all = ensure_puzzle_initial_level_assets_ready(&draft.levels[0]) .expect_err("缺少自动生成资产时不能把草稿标记为完成"); assert_eq!(missing_all.status_code(), StatusCode::BAD_GATEWAY); assert!(missing_all.body_text().contains("UI背景图")); draft.levels[0].ui_background_image_src = Some("/generated-puzzle-assets/session/ui/background.png".to_string()); ensure_puzzle_initial_level_assets_ready(&draft.levels[0]) .expect("UI 背景存在时即可完成自动草稿资源检查"); } fn test_puzzle_anchor_pack_record() -> PuzzleAnchorPackRecord { let item = PuzzleAnchorItemRecord { key: "visualSubject".to_string(), label: "画面".to_string(), value: "雨夜猫街".to_string(), status: "confirmed".to_string(), }; PuzzleAnchorPackRecord { theme_promise: item.clone(), visual_subject: item.clone(), visual_mood: item.clone(), composition_hooks: item.clone(), tags_and_forbidden: item, } } fn test_puzzle_draft_record() -> PuzzleResultDraftRecord { let anchor_pack = test_puzzle_anchor_pack_record(); PuzzleResultDraftRecord { work_title: "雨夜猫街".to_string(), work_description: "一只猫在雨夜灯牌下回头。".to_string(), level_name: "猫画面".to_string(), summary: "一只猫在雨夜灯牌下回头。".to_string(), theme_tags: vec![], forbidden_directives: vec![], creator_intent: None, anchor_pack, candidates: vec![], selected_candidate_id: None, cover_image_src: None, cover_asset_id: None, generation_status: "idle".to_string(), levels: vec![PuzzleDraftLevelRecord { level_id: "puzzle-level-1".to_string(), level_name: "猫画面".to_string(), picture_description: "一只猫在雨夜灯牌下回头。".to_string(), picture_reference: None, ui_background_prompt: None, ui_background_image_src: None, ui_background_image_object_key: None, background_music: None, candidates: vec![], selected_candidate_id: None, cover_image_src: None, cover_asset_id: None, generation_status: "idle".to_string(), }], form_draft: None, } } #[test] fn puzzle_primary_level_update_preserves_reference_for_regeneration() { let draft = test_puzzle_draft_record(); let mut target_level = draft.levels[0].clone(); target_level.level_name = "雨夜猫街".to_string(); let levels = build_puzzle_levels_with_primary_update( &draft, &target_level, Some("data:image/png;base64,abcd"), ); assert_eq!(levels[0].level_name, "雨夜猫街"); assert_eq!( levels[0].picture_reference.as_deref(), Some("data:image/png;base64,abcd") ); } #[test] fn puzzle_generated_fallback_snapshot_preserves_picture_reference() { let anchor_pack = test_puzzle_anchor_pack_record(); let session = PuzzleAgentSessionRecord { session_id: "puzzle-session-1".to_string(), seed_text: "雨夜猫街".to_string(), current_turn: 1, progress_percent: 0, stage: "draft_ready".to_string(), anchor_pack: anchor_pack.clone(), draft: Some(test_puzzle_draft_record()), messages: Vec::new(), last_assistant_reply: None, published_profile_id: None, suggested_actions: Vec::new(), result_preview: None, updated_at: "2024-01-01T00:00:00Z".to_string(), }; let candidate = PuzzleGeneratedImageCandidateRecord { candidate_id: "puzzle-session-1-candidate-1".to_string(), image_src: "/generated-puzzle-assets/puzzle-session-1/1/cover.png".to_string(), asset_id: "puzzle-cover-1".to_string(), prompt: "雨夜猫街".to_string(), actual_prompt: Some("雨夜猫街".to_string()), source_type: "generated:gpt-image-2".to_string(), selected: true, }; let session = apply_generated_puzzle_candidates_to_session_snapshot( session, "puzzle-level-1", vec![candidate], Some("data:image/png;base64,abcd"), 1_713_686_401_234_568, ); let draft = session.draft.expect("draft"); assert_eq!( draft.levels[0].picture_reference.as_deref(), Some("data:image/png;base64,abcd") ); } #[test] fn freeze_boundary_sync_only_matches_freeze_invalid_operation() { let invalid_operation = AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ "provider": "spacetimedb", "message": "操作不合法", })); let other_error = AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ "provider": "spacetimedb", "message": "泥点余额不足", })); assert!(should_sync_puzzle_freeze_boundary(&invalid_operation, true)); assert!(!should_sync_puzzle_freeze_boundary( &invalid_operation, false )); assert!(!should_sync_puzzle_freeze_boundary(&other_error, true)); }