mod application; mod commands; mod domain; mod errors; mod events; pub use application::*; pub use commands::*; pub use domain::*; pub use errors::*; pub use events::*; #[cfg(test)] mod tests { use super::*; fn build_base_state() -> NpcStateSnapshot { normalize_npc_state_snapshot( NpcStateUpsertInput { runtime_session_id: "runtime_001".to_string(), npc_id: "npc_001".to_string(), npc_name: "宁霜".to_string(), affinity: 18, help_used: false, chatted_count: 0, gifts_given: 0, recruited: false, trade_stock_signature: None, revealed_facts: vec![], known_attribute_rumors: vec![], first_meaningful_contact_resolved: false, seen_backstory_chapter_ids: vec![], stance_profile: None, updated_at_micros: 10, }, None, ) .expect("base npc state should be valid") } #[test] fn relation_state_uses_expected_thresholds() { assert_eq!(build_relation_state(-1).stance, NpcRelationStance::Hostile); assert_eq!(build_relation_state(0).stance, NpcRelationStance::Guarded); assert_eq!(build_relation_state(15).stance, NpcRelationStance::Neutral); assert_eq!( build_relation_state(30).stance, NpcRelationStance::Cooperative ); assert_eq!(build_relation_state(60).stance, NpcRelationStance::Bonded); } #[test] fn normalize_npc_state_snapshot_builds_primary_fields() { let snapshot = build_base_state(); assert_eq!(snapshot.npc_state_id, "npcstate_runtime_001:npc_001"); assert_eq!(snapshot.relation_state.stance, NpcRelationStance::Neutral); assert_eq!(snapshot.created_at_micros, 10); assert_eq!(snapshot.updated_at_micros, 10); } #[test] fn chat_action_increases_affinity_and_marks_first_contact() { let next = apply_npc_social_action( build_base_state(), ResolveNpcSocialActionInput { runtime_session_id: "runtime_001".to_string(), npc_id: "npc_001".to_string(), npc_name: "宁霜".to_string(), action_kind: NpcSocialActionKind::Chat, affinity_gain_override: None, note: None, updated_at_micros: 20, }, ) .expect("chat should succeed"); assert_eq!(next.affinity, 24); assert_eq!(next.chatted_count, 1); assert!(next.first_meaningful_contact_resolved); assert_eq!(next.updated_at_micros, 20); } #[test] fn help_action_rejects_second_use() { let used_state = NpcStateSnapshot { help_used: true, ..build_base_state() }; let error = apply_npc_social_action( used_state, ResolveNpcSocialActionInput { runtime_session_id: "runtime_001".to_string(), npc_id: "npc_001".to_string(), npc_name: "宁霜".to_string(), action_kind: NpcSocialActionKind::Help, affinity_gain_override: None, note: None, updated_at_micros: 20, }, ) .expect_err("help should fail once used"); assert_eq!(error, NpcStateFieldError::HelpAlreadyUsed); } #[test] fn recruit_requires_threshold() { let error = apply_npc_social_action( build_base_state(), ResolveNpcSocialActionInput { runtime_session_id: "runtime_001".to_string(), npc_id: "npc_001".to_string(), npc_name: "宁霜".to_string(), action_kind: NpcSocialActionKind::Recruit, affinity_gain_override: None, note: None, updated_at_micros: 20, }, ) .expect_err("recruit should require threshold"); assert_eq!(error, NpcStateFieldError::RecruitAffinityTooLow); } #[test] fn recruit_marks_state_when_affinity_is_high_enough() { let recruitable = NpcStateSnapshot { affinity: 66, relation_state: build_relation_state(66), ..build_base_state() }; let next = apply_npc_social_action( recruitable, ResolveNpcSocialActionInput { runtime_session_id: "runtime_001".to_string(), npc_id: "npc_001".to_string(), npc_name: "宁霜".to_string(), action_kind: NpcSocialActionKind::Recruit, affinity_gain_override: None, note: None, updated_at_micros: 20, }, ) .expect("recruit should succeed"); assert!(next.recruited); assert!(next.first_meaningful_contact_resolved); } #[test] fn resolve_preview_talk_keeps_affinity_unchanged() { let result = resolve_npc_interaction( build_base_state(), ResolveNpcInteractionInput { runtime_session_id: "runtime_001".to_string(), npc_id: "npc_001".to_string(), npc_name: "宁霜".to_string(), interaction_function_id: NPC_PREVIEW_TALK_FUNCTION_ID.to_string(), release_npc_id: None, updated_at_micros: 20, }, ) .expect("preview talk should succeed"); assert_eq!(result.interaction_status, NpcInteractionStatus::Previewed); assert!(!result.affinity_changed); assert_eq!(result.previous_affinity, 18); assert_eq!(result.next_affinity, 18); } #[test] fn resolve_chat_updates_npc_state_and_returns_dialogue_status() { let result = resolve_npc_interaction( build_base_state(), ResolveNpcInteractionInput { runtime_session_id: "runtime_001".to_string(), npc_id: "npc_001".to_string(), npc_name: "宁霜".to_string(), interaction_function_id: NPC_CHAT_FUNCTION_ID.to_string(), release_npc_id: None, updated_at_micros: 20, }, ) .expect("chat interaction should succeed"); assert_eq!(result.interaction_status, NpcInteractionStatus::Dialogue); assert!(result.affinity_changed); assert_eq!(result.next_affinity, 24); assert!(result.npc_state.first_meaningful_contact_resolved); } #[test] fn resolve_fight_returns_battle_pending_without_affinity_change() { let result = resolve_npc_interaction( build_base_state(), ResolveNpcInteractionInput { runtime_session_id: "runtime_001".to_string(), npc_id: "npc_001".to_string(), npc_name: "宁霜".to_string(), interaction_function_id: NPC_FIGHT_FUNCTION_ID.to_string(), release_npc_id: None, updated_at_micros: 20, }, ) .expect("fight interaction should succeed"); assert_eq!( result.interaction_status, NpcInteractionStatus::BattlePending ); assert_eq!(result.battle_mode, Some(NpcInteractionBattleMode::Fight)); assert!(!result.affinity_changed); } }