mod application; mod commands; mod domain; mod errors; mod events; pub use application::*; pub use commands::*; pub use domain::*; pub use errors::*; use shared_kernel::format_rfc3339 as format_shared_rfc3339; use time::OffsetDateTime; pub fn format_utc_micros(micros: i64) -> String { let timestamp = OffsetDateTime::from_unix_timestamp_nanos(i128::from(micros) * 1_000) .unwrap_or(OffsetDateTime::UNIX_EPOCH); format_shared_rfc3339(timestamp).unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string()) } pub fn runtime_profile_recharge_point_products() -> Vec { vec![ build_points_recharge_product( "points_60", "60泥点", 600, 60, 60, "首充双倍", "首充送60泥点", ), build_points_recharge_product( "points_180", "180泥点", 1800, 180, 180, "首充双倍", "首充送180泥点", ), build_points_recharge_product( "points_300", "300泥点", 3000, 300, 300, "首充双倍", "首充送300泥点", ), build_points_recharge_product( "points_680", "680泥点", 6800, 680, 680, "首充双倍", "首充送680泥点", ), build_points_recharge_product( "points_1280", "1280泥点", 12800, 1280, 1280, "首充双倍", "首充送1280泥点", ), build_points_recharge_product( "points_3280", "3280泥点", 32800, 3280, 3280, "首充双倍", "首充送3280泥点", ), ] } /// 中文注释:充值中心展示当前账号本次实际可生效的首充赠送状态。 pub fn resolve_runtime_profile_recharge_point_products( has_points_recharged: bool, ) -> Vec { let mut products = runtime_profile_recharge_point_products(); if has_points_recharged { for product in &mut products { product.bonus_points = 0; product.badge_label.clear(); product.description = product.title.clone(); } } products } pub fn runtime_profile_recharge_membership_products() -> Vec { vec![ build_membership_recharge_product( "member_month", "月卡", 2800, 30, RuntimeProfileMembershipTier::Month, ), build_membership_recharge_product( "member_season", "季卡", 7800, 90, RuntimeProfileMembershipTier::Season, ), build_membership_recharge_product( "member_year", "年卡", 24800, 365, RuntimeProfileMembershipTier::Year, ), ] } pub fn runtime_profile_membership_benefits() -> Vec { vec![ RuntimeProfileMembershipBenefitSnapshot { benefit_name: "特权名称".to_string(), normal_value: "普通".to_string(), month_value: "月卡".to_string(), season_value: "季卡".to_string(), year_value: "年卡".to_string(), }, RuntimeProfileMembershipBenefitSnapshot { benefit_name: "免费".to_string(), normal_value: "免费".to_string(), month_value: "¥28".to_string(), season_value: "¥78".to_string(), year_value: "¥248".to_string(), }, RuntimeProfileMembershipBenefitSnapshot { benefit_name: "免泥点回合数".to_string(), normal_value: "30".to_string(), month_value: "100".to_string(), season_value: "100".to_string(), year_value: "100".to_string(), }, RuntimeProfileMembershipBenefitSnapshot { benefit_name: "每日签到加成".to_string(), normal_value: "0%".to_string(), month_value: "0%".to_string(), season_value: "+100%".to_string(), year_value: "+210%".to_string(), }, ] } pub fn runtime_profile_recharge_product_by_id( product_id: &str, ) -> Option { runtime_profile_recharge_point_products() .into_iter() .chain(runtime_profile_recharge_membership_products()) .find(|product| product.product_id == product_id) } pub fn visible_runtime_profile_user_tags(tags: &[String]) -> Vec { tags.iter() .filter(|tag| tag.as_str() == "北科") .cloned() .collect() } fn build_points_recharge_product( product_id: &str, title: &str, price_cents: u64, points_amount: u64, bonus_points: u64, badge_label: &str, description: &str, ) -> RuntimeProfileRechargeProductSnapshot { RuntimeProfileRechargeProductSnapshot { product_id: product_id.to_string(), title: title.to_string(), price_cents, kind: RuntimeProfileRechargeProductKind::Points, points_amount, bonus_points, duration_days: 0, badge_label: badge_label.to_string(), description: description.to_string(), tier: RuntimeProfileMembershipTier::Normal, } } fn build_membership_recharge_product( product_id: &str, title: &str, price_cents: u64, duration_days: u32, tier: RuntimeProfileMembershipTier, ) -> RuntimeProfileRechargeProductSnapshot { RuntimeProfileRechargeProductSnapshot { product_id: product_id.to_string(), title: title.to_string(), price_cents, kind: RuntimeProfileRechargeProductKind::Membership, points_amount: 0, bonus_points: 0, duration_days, badge_label: String::new(), description: format!("{}天会员", duration_days), tier, } } #[cfg(test)] mod tests { use super::*; #[test] fn defaults_match_shared_contract_baseline() { let settings = RuntimeSettings::defaults(); assert!((settings.music_volume - DEFAULT_MUSIC_VOLUME).abs() < f32::EPSILON); assert_eq!(settings.platform_theme, RuntimePlatformTheme::Light); } #[test] fn default_creation_entry_types_include_baby_object_match() { let configs = default_creation_entry_type_snapshots(1); let baby_object_match = configs .iter() .find(|item| item.id == "baby-object-match") .expect("baby-object-match creation entry should be seeded"); assert_eq!(baby_object_match.title, "宝贝识物"); assert_eq!(baby_object_match.subtitle, "亲子识物分类"); assert!(baby_object_match.visible); assert!(baby_object_match.open); assert_eq!(baby_object_match.sort_order, 90); assert_eq!( baby_object_match.image_src, "/child-motion-demo/picture-book-grass-stage.png" ); } #[test] fn default_creation_entry_types_include_bark_battle() { let configs = default_creation_entry_type_snapshots(1); let bark_battle = configs .iter() .find(|item| item.id == "bark-battle") .expect("bark-battle creation entry should be seeded"); assert_eq!(bark_battle.title, "汪汪声浪"); assert!(bark_battle.visible); assert!(bark_battle.open); assert_eq!(bark_battle.sort_order, 85); } #[test] fn normalized_clamps_music_volume_into_valid_range() { let low = RuntimeSettings::normalized(-1.0, RuntimePlatformTheme::Light); let high = RuntimeSettings::normalized(3.5, RuntimePlatformTheme::Dark); assert_eq!(low.music_volume, 0.0); assert_eq!(high.music_volume, 1.0); assert_eq!(high.platform_theme, RuntimePlatformTheme::Dark); } #[test] fn theme_from_client_string_falls_back_to_light() { assert_eq!( RuntimePlatformTheme::from_client_str("dark"), RuntimePlatformTheme::Dark ); assert_eq!( RuntimePlatformTheme::from_client_str("LIGHT"), RuntimePlatformTheme::Light ); assert_eq!( RuntimePlatformTheme::from_client_str("mythic"), RuntimePlatformTheme::Light ); } #[test] fn build_upsert_input_rejects_blank_user_id() { let error = build_runtime_setting_upsert_input( " ".to_string(), DEFAULT_MUSIC_VOLUME, RuntimePlatformTheme::Light, 1, ) .expect_err("blank user id should fail"); assert_eq!(error, RuntimeSettingsFieldError::MissingUserId); } #[test] fn browse_history_theme_from_client_string_falls_back_to_mythic() { assert_eq!( RuntimeBrowseHistoryThemeMode::from_client_str("martial"), RuntimeBrowseHistoryThemeMode::Martial ); assert_eq!( RuntimeBrowseHistoryThemeMode::from_client_str("RIFT"), RuntimeBrowseHistoryThemeMode::Rift ); assert_eq!( RuntimeBrowseHistoryThemeMode::from_client_str("unknown"), RuntimeBrowseHistoryThemeMode::Mythic ); } #[test] fn build_browse_history_sync_input_normalizes_optionals_and_visited_at() { let input = build_runtime_browse_history_sync_input( " user-1 ".to_string(), vec![RuntimeBrowseHistoryWriteInput { owner_user_id: " owner-a ".to_string(), profile_id: " profile-a ".to_string(), world_name: " 世界A ".to_string(), subtitle: Some(" ".to_string()), summary_text: Some(" 简介 ".to_string()), cover_image_src: Some(" /cover.png ".to_string()), theme_mode: Some(" arcane ".to_string()), author_display_name: Some(" ".to_string()), visited_at: None, }], 1_713_680_000_000_000, ) .expect("sync input should build"); assert_eq!(input.user_id, "user-1"); assert_eq!(input.entries.len(), 1); assert_eq!(input.entries[0].owner_user_id, "owner-a"); assert_eq!(input.entries[0].profile_id, "profile-a"); assert_eq!(input.entries[0].world_name, "世界A"); assert_eq!(input.entries[0].subtitle, None); assert_eq!(input.entries[0].summary_text, Some("简介".to_string())); assert_eq!( input.entries[0].cover_image_src, Some("/cover.png".to_string()) ); assert_eq!(input.entries[0].theme_mode, Some("arcane".to_string())); assert_eq!(input.entries[0].author_display_name, None); assert_eq!( input.entries[0].visited_at, Some("2024-04-21T06:13:20Z".to_string()) ); } #[test] fn prepare_browse_history_entries_sorts_desc_and_dedups_by_owner_profile() { let entries = prepare_runtime_browse_history_entries(RuntimeBrowseHistorySyncInput { user_id: "user-1".to_string(), entries: vec![ RuntimeBrowseHistoryWriteInput { owner_user_id: "owner-a".to_string(), profile_id: "profile-a".to_string(), world_name: "世界旧".to_string(), subtitle: None, summary_text: None, cover_image_src: None, theme_mode: Some("martial".to_string()), author_display_name: None, visited_at: Some("2026-04-20T10:00:00Z".to_string()), }, RuntimeBrowseHistoryWriteInput { owner_user_id: "owner-b".to_string(), profile_id: "profile-b".to_string(), world_name: "世界B".to_string(), subtitle: None, summary_text: None, cover_image_src: None, theme_mode: Some("rift".to_string()), author_display_name: Some("作者B".to_string()), visited_at: Some("2026-04-21T10:00:00Z".to_string()), }, RuntimeBrowseHistoryWriteInput { owner_user_id: "owner-a".to_string(), profile_id: "profile-a".to_string(), world_name: "世界新".to_string(), subtitle: None, summary_text: None, cover_image_src: None, theme_mode: Some("unknown".to_string()), author_display_name: Some("".to_string()), visited_at: Some("2026-04-21T11:00:00Z".to_string()), }, ], updated_at_micros: 1_776_000_000_000_000, }) .expect("entries should prepare"); assert_eq!(entries.len(), 2); assert_eq!(entries[0].world_name, "世界新"); assert_eq!(entries[0].theme_mode, RuntimeBrowseHistoryThemeMode::Mythic); assert_eq!( entries[0].author_display_name, DEFAULT_BROWSE_HISTORY_AUTHOR_DISPLAY_NAME ); assert_eq!(entries[1].world_name, "世界B"); assert!(entries[0].visited_at_micros > entries[1].visited_at_micros); } #[test] fn build_browse_history_sync_input_silently_filters_invalid_entries() { let input = build_runtime_browse_history_sync_input( "user-1".to_string(), vec![ RuntimeBrowseHistoryWriteInput { owner_user_id: " ".to_string(), profile_id: "profile-a".to_string(), world_name: "世界A".to_string(), subtitle: None, summary_text: None, cover_image_src: None, theme_mode: None, author_display_name: None, visited_at: None, }, RuntimeBrowseHistoryWriteInput { owner_user_id: "owner-b".to_string(), profile_id: "profile-b".to_string(), world_name: " 世界B ".to_string(), subtitle: None, summary_text: None, cover_image_src: None, theme_mode: None, author_display_name: None, visited_at: None, }, RuntimeBrowseHistoryWriteInput { owner_user_id: "owner-c".to_string(), profile_id: "".to_string(), world_name: "世界C".to_string(), subtitle: None, summary_text: None, cover_image_src: None, theme_mode: None, author_display_name: None, visited_at: None, }, ], 1_776_000_000_000_000, ) .expect("sync input should build"); assert_eq!(input.entries.len(), 1); assert_eq!(input.entries[0].owner_user_id, "owner-b"); assert_eq!(input.entries[0].profile_id, "profile-b"); assert_eq!(input.entries[0].world_name, "世界B"); } #[test] fn build_profile_inputs_reject_blank_user_id() { assert_eq!( build_runtime_profile_dashboard_get_input(" ".to_string()) .expect_err("dashboard input should fail"), RuntimeProfileFieldError::MissingUserId ); assert_eq!( build_runtime_profile_wallet_ledger_list_input(" ".to_string()) .expect_err("wallet ledger input should fail"), RuntimeProfileFieldError::MissingUserId ); assert_eq!( build_runtime_profile_play_stats_get_input(" ".to_string()) .expect_err("play stats input should fail"), RuntimeProfileFieldError::MissingUserId ); } #[test] fn profile_dashboard_record_formats_optional_timestamp() { let record = build_runtime_profile_dashboard_record(RuntimeProfileDashboardSnapshot { user_id: "user-1".to_string(), wallet_balance: 8, total_play_time_ms: 12, played_world_count: 2, updated_at_micros: Some(1_713_680_000_000_000), }); assert_eq!(record.updated_at, Some("2024-04-21T06:13:20Z".to_string())); } #[test] fn profile_wallet_ledger_source_type_formats_to_snapshot_sync() { assert_eq!( RuntimeProfileWalletLedgerSourceType::SnapshotSync.as_str(), "snapshot_sync" ); assert_eq!( RuntimeProfileWalletLedgerSourceType::NewUserRegistrationReward.as_str(), "new_user_registration_reward" ); assert_eq!( RuntimeProfileWalletLedgerSourceType::PointsRecharge.as_str(), "points_recharge" ); assert_eq!( RuntimeProfileWalletLedgerSourceType::AssetOperationConsume.as_str(), "asset_operation_consume" ); assert_eq!( RuntimeProfileWalletLedgerSourceType::AssetOperationRefund.as_str(), "asset_operation_refund" ); assert_eq!( RuntimeProfileWalletLedgerSourceType::DailyTaskReward.as_str(), "daily_task_reward" ); } #[test] fn new_user_registration_wallet_reward_starts_with_ten_points() { assert_eq!(PROFILE_NEW_USER_INITIAL_WALLET_POINTS, 10); assert_eq!( calculate_runtime_profile_wallet_balance( 0, PROFILE_NEW_USER_INITIAL_WALLET_POINTS as i64, ) .expect("new user registration reward should fit wallet balance"), 10 ); } #[test] fn runtime_profile_beijing_day_key_uses_business_day_boundary() { let before_beijing_midnight = 1_714_927_999_999_999; let after_beijing_midnight = 1_714_928_000_000_000; assert_eq!( runtime_profile_beijing_day_key(before_beijing_midnight), runtime_profile_beijing_day_key(after_beijing_midnight) - 1 ); } #[test] fn analytics_date_dimension_handles_iso_week_across_year() { let date_key = parse_analytics_calendar_date_key("2024-12-31").unwrap(); let dimension = build_analytics_date_dimension_from_date_key(date_key); assert_eq!(dimension.calendar_date, "2024-12-31"); assert_eq!(dimension.weekday, 2); assert_eq!(dimension.iso_week_key, 202501); assert_eq!( dimension.week_start_date_key, parse_analytics_calendar_date_key("2024-12-30").unwrap() ); assert_eq!( dimension.week_end_date_key, parse_analytics_calendar_date_key("2025-01-05").unwrap() ); } #[test] fn analytics_date_dimension_handles_leap_day() { let date_key = parse_analytics_calendar_date_key("2024-02-29").unwrap(); let dimension = build_analytics_date_dimension_from_date_key(date_key); assert_eq!(dimension.calendar_date, "2024-02-29"); assert_eq!(dimension.weekday, 4); assert_eq!(dimension.month_key, 202402); assert_eq!(dimension.month_end_date_key, date_key); assert_eq!(dimension.quarter_key, 20241); } #[test] fn analytics_date_dimension_handles_quarter_boundary() { let date_key = parse_analytics_calendar_date_key("2024-04-01").unwrap(); let dimension = build_analytics_date_dimension_from_date_key(date_key); assert_eq!(dimension.quarter_key, 20242); assert_eq!(dimension.quarter_start_date_key, date_key); assert_eq!( dimension.quarter_end_date_key, parse_analytics_calendar_date_key("2024-06-30").unwrap() ); assert_eq!( dimension.year_start_date_key, parse_analytics_calendar_date_key("2024-01-01").unwrap() ); assert_eq!( dimension.year_end_date_key, parse_analytics_calendar_date_key("2024-12-31").unwrap() ); } #[test] fn runtime_profile_task_status_matches_progress_and_claim() { assert_eq!( resolve_runtime_profile_task_status(false, 1, 1, false), RuntimeProfileTaskStatus::Disabled ); assert_eq!( resolve_runtime_profile_task_status(true, 0, 1, false), RuntimeProfileTaskStatus::Incomplete ); assert_eq!( resolve_runtime_profile_task_status(true, 1, 1, false), RuntimeProfileTaskStatus::Claimable ); assert_eq!( resolve_runtime_profile_task_status(true, 1, 1, true), RuntimeProfileTaskStatus::Claimed ); } #[test] fn build_task_config_input_rejects_invalid_reward_and_threshold() { assert_eq!( build_runtime_profile_task_config_admin_upsert_input( "admin".to_string(), PROFILE_TASK_ID_DAILY_LOGIN.to_string(), "每日登录".to_string(), "".to_string(), PROFILE_TASK_EVENT_KEY_DAILY_LOGIN.to_string(), RuntimeProfileTaskCycle::Daily, RuntimeTrackingScopeKind::User, 0, 10, true, 10, 1, ) .expect_err("zero threshold should fail"), RuntimeProfileFieldError::InvalidTaskThreshold ); assert_eq!( build_runtime_profile_task_config_admin_upsert_input( "admin".to_string(), PROFILE_TASK_ID_DAILY_LOGIN.to_string(), "每日登录".to_string(), "".to_string(), PROFILE_TASK_EVENT_KEY_DAILY_LOGIN.to_string(), RuntimeProfileTaskCycle::Daily, RuntimeTrackingScopeKind::User, 1, 0, true, 10, 1, ) .expect_err("zero reward should fail"), RuntimeProfileFieldError::InvalidTaskReward ); } #[test] fn build_task_config_input_accepts_only_user_scope() { let input = build_runtime_profile_task_config_admin_upsert_input( "admin".to_string(), PROFILE_TASK_ID_DAILY_LOGIN.to_string(), "每日登录".to_string(), "".to_string(), PROFILE_TASK_EVENT_KEY_DAILY_LOGIN.to_string(), RuntimeProfileTaskCycle::Daily, RuntimeTrackingScopeKind::User, 1, 10, true, 10, 1, ) .expect("user scope should be accepted"); assert_eq!(input.scope_kind, RuntimeTrackingScopeKind::User); for scope_kind in [ RuntimeTrackingScopeKind::Site, RuntimeTrackingScopeKind::Module, RuntimeTrackingScopeKind::Work, ] { assert_eq!( build_runtime_profile_task_config_admin_upsert_input( "admin".to_string(), PROFILE_TASK_ID_DAILY_LOGIN.to_string(), "每日登录".to_string(), "".to_string(), PROFILE_TASK_EVENT_KEY_DAILY_LOGIN.to_string(), RuntimeProfileTaskCycle::Daily, scope_kind, 1, 10, true, 10, 1, ) .expect_err("non-user scope should fail"), RuntimeProfileFieldError::UnsupportedProfileTaskScopeKind ); } } #[test] fn recharge_product_catalog_matches_reference_prices() { let point_products = runtime_profile_recharge_point_products(); let membership_products = runtime_profile_recharge_membership_products(); assert_eq!(point_products.len(), 6); assert_eq!(point_products[0].product_id, "points_60"); assert_eq!(point_products[0].title, "60泥点"); assert_eq!(point_products[0].price_cents, 600); assert_eq!(point_products[0].bonus_points, 60); assert_eq!(point_products[0].description, "首充送60泥点"); assert_eq!(point_products[5].product_id, "points_3280"); assert_eq!(point_products[5].price_cents, 32800); assert_eq!(point_products[5].bonus_points, 3280); assert_eq!(point_products[5].description, "首充送3280泥点"); assert_eq!(membership_products.len(), 3); assert_eq!(membership_products[0].title, "月卡"); assert_eq!(membership_products[0].price_cents, 2800); assert_eq!(membership_products[2].duration_days, 365); let benefits = runtime_profile_membership_benefits(); assert!( benefits .iter() .any(|benefit| benefit.benefit_name == "免泥点回合数") ); } #[test] fn recharge_point_products_resolve_effective_first_bonus_display() { let first_recharge_products = resolve_runtime_profile_recharge_point_products(false); assert_eq!(first_recharge_products[0].bonus_points, 60); assert_eq!(first_recharge_products[0].badge_label, "首充双倍"); assert_eq!(first_recharge_products[0].description, "首充送60泥点"); let repeated_recharge_products = resolve_runtime_profile_recharge_point_products(true); assert_eq!(repeated_recharge_products[0].bonus_points, 0); assert_eq!(repeated_recharge_products[0].badge_label, ""); assert_eq!(repeated_recharge_products[0].description, "60泥点"); assert_eq!(repeated_recharge_products[5].bonus_points, 0); assert_eq!(repeated_recharge_products[5].badge_label, ""); assert_eq!(repeated_recharge_products[5].description, "3280泥点"); } #[test] fn build_recharge_order_input_rejects_unknown_product() { let error = build_runtime_profile_recharge_order_create_input( "user-1".to_string(), "bad-product".to_string(), "mock".to_string(), 1, ) .expect_err("unknown product should fail"); assert_eq!(error, RuntimeProfileFieldError::UnknownRechargeProduct); } #[test] fn runtime_profile_identity_helpers_keep_existing_key_shape() { assert_eq!( build_runtime_profile_played_world_id(" user-1 ", " custom:world "), "user-1:custom:world" ); assert_eq!( build_runtime_profile_snapshot_wallet_ledger_id(" user-1 ", 100, 30), "user-1:100:30" ); assert_eq!( build_runtime_profile_recharge_wallet_ledger_id("user-1", 200, "points_60"), "user-1:200:points_60" ); let order_id = build_runtime_profile_recharge_order_id("user-1", 200, "points_60"); assert!(order_id.starts_with("rcg")); assert!(order_id.len() <= 32); assert!( order_id .chars() .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit()) ); assert_eq!( build_runtime_profile_redeem_code_usage_id("GIFT", "user-1", 300, 2), "redeem:GIFT:user-1:300:2" ); assert_eq!( build_runtime_profile_redeem_code_ledger_id("redeem:GIFT:user-1:300:2"), "redeem:GIFT:user-1:300:2:ledger" ); } #[test] fn runtime_profile_membership_purchase_extends_from_active_expiry() { let update = resolve_runtime_profile_membership_purchase_update(Some(10), Some(200), 100, 30); assert_eq!(update.started_at_micros, 10); assert_eq!( update.expires_at_micros, 200 + 30 * PROFILE_RUNTIME_DAY_MICROS ); let expired_update = resolve_runtime_profile_membership_purchase_update(Some(10), Some(80), 100, 1); assert_eq!(expired_update.started_at_micros, 10); assert_eq!( expired_update.expires_at_micros, 100 + PROFILE_RUNTIME_DAY_MICROS ); } #[test] fn runtime_profile_wallet_balance_calculation_guards_edges() { assert_eq!( convert_runtime_profile_wallet_unsigned_delta(8).expect("small amount should convert"), 8 ); assert_eq!( convert_runtime_profile_wallet_unsigned_delta(i64::MAX as u64 + 1) .expect_err("oversized amount should fail"), RuntimeProfileFieldError::WalletAmountOverflow ); assert_eq!( calculate_runtime_profile_wallet_balance(10, 5).expect("positive delta should add"), 15 ); assert_eq!( calculate_runtime_profile_wallet_balance(10, -4) .expect("negative delta should subtract"), 6 ); assert_eq!( calculate_runtime_profile_wallet_balance(3, -4).expect_err("overspend should fail"), RuntimeProfileFieldError::InsufficientWalletBalance ); } #[test] fn runtime_profile_redeem_code_usage_validation_matches_modes() { let base = RuntimeProfileRedeemCodeSnapshot { code: "GIFT".to_string(), mode: RuntimeProfileRedeemCodeMode::Public, reward_points: 30, max_uses: 2, global_used_count: 0, enabled: true, allowed_user_ids: Vec::new(), created_by: "admin".to_string(), created_at_micros: 1, updated_at_micros: 1, }; validate_runtime_profile_redeem_code_usage(&base, "user-1", 1) .expect("public code under per-user limit should pass"); assert_eq!( validate_runtime_profile_redeem_code_usage(&base, "user-1", 2) .expect_err("public code over per-user limit should fail"), RuntimeProfileFieldError::RedeemCodeUsesExhausted ); let private = RuntimeProfileRedeemCodeSnapshot { mode: RuntimeProfileRedeemCodeMode::Private, allowed_user_ids: vec!["user-2".to_string()], ..base.clone() }; assert_eq!( validate_runtime_profile_redeem_code_usage(&private, "user-1", 0) .expect_err("private code should check allow list"), RuntimeProfileFieldError::RedeemCodeNotAllowedForUser ); let disabled = RuntimeProfileRedeemCodeSnapshot { enabled: false, ..base }; assert_eq!( validate_runtime_profile_redeem_code_usage(&disabled, "user-1", 0) .expect_err("disabled code should fail"), RuntimeProfileFieldError::RedeemCodeDisabled ); } #[test] fn runtime_save_checkpoint_update_rejects_session_mismatch() { let existing = RuntimeSnapshotRecord { user_id: "user-1".to_string(), version: SAVE_SNAPSHOT_VERSION, saved_at: "2026-04-29T00:00:00Z".to_string(), saved_at_micros: 1, bottom_tab: "story".to_string(), game_state: serde_json::json!({ "runtimeSessionId": "session-old", "runtimeStats": { "playTimeMs": 10, "lastPlayTickAt": "2026-04-29T00:00:00Z" } }), current_story: None, game_state_json: "{}".to_string(), current_story_json: None, created_at_micros: 1, updated_at_micros: 1, }; let input = RuntimeSaveCheckpointInput { session_id: "session-new".to_string(), bottom_tab: "story".to_string(), saved_at_micros: 2, updated_at_micros: 3, }; assert_eq!( build_runtime_save_checkpoint_update(input, existing) .expect_err("mismatched session should fail"), RuntimeProfileFieldError::RuntimeSessionMismatch { expected_session_id: "session-old".to_string(), actual_session_id: "session-new".to_string(), } ); } }