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_stackable_item(quantity: u32) -> InventoryItemSnapshot { InventoryItemSnapshot { item_id: "consumable_heal_potion".to_string(), category: "消耗品".to_string(), name: "疗伤药".to_string(), description: Some("用于恢复少量气血。".to_string()), quantity, rarity: InventoryItemRarity::Common, tags: vec!["healing".to_string()], stackable: true, stack_key: "heal_potion".to_string(), equipment_slot_id: None, source_kind: InventoryItemSourceKind::TreasureReward, source_reference_id: Some("treasure_001".to_string()), } } fn build_weapon_item(slot_id: &str, name: &str) -> InventorySlotSnapshot { InventorySlotSnapshot { slot_id: slot_id.to_string(), runtime_session_id: "runtime_001".to_string(), story_session_id: Some("storysess_001".to_string()), actor_user_id: "user_001".to_string(), container_kind: InventoryContainerKind::Backpack, slot_key: slot_id.to_string(), item_id: format!("weapon:{slot_id}"), category: "武器".to_string(), name: name.to_string(), description: Some("测试武器".to_string()), quantity: 1, rarity: InventoryItemRarity::Rare, tags: vec!["weapon".to_string(), "快剑".to_string()], stackable: false, stack_key: format!("weapon:{slot_id}"), equipment_slot_id: Some(InventoryEquipmentSlot::Weapon), source_kind: InventoryItemSourceKind::StoryReward, source_reference_id: Some("storyevt_001".to_string()), created_at_micros: 1, updated_at_micros: 1, } } fn build_mutation_input(mutation: InventoryMutation) -> InventoryMutationInput { InventoryMutationInput { mutation_id: "invmut_001".to_string(), runtime_session_id: "runtime_001".to_string(), story_session_id: Some("storysess_001".to_string()), actor_user_id: "user_001".to_string(), mutation, updated_at_micros: 10, } } #[test] fn grant_item_merges_existing_stackable_slot() { let current = vec![InventorySlotSnapshot { slot_id: "invslot_existing".to_string(), runtime_session_id: "runtime_001".to_string(), story_session_id: Some("storysess_001".to_string()), actor_user_id: "user_001".to_string(), container_kind: InventoryContainerKind::Backpack, slot_key: "invslot_existing".to_string(), item_id: "consumable_heal_potion".to_string(), category: "消耗品".to_string(), name: "疗伤药".to_string(), description: None, quantity: 2, rarity: InventoryItemRarity::Common, tags: vec!["healing".to_string()], stackable: true, stack_key: "heal_potion".to_string(), equipment_slot_id: None, source_kind: InventoryItemSourceKind::TreasureReward, source_reference_id: Some("treasure_000".to_string()), created_at_micros: 1, updated_at_micros: 1, }]; let outcome = apply_inventory_mutation( current, build_mutation_input(InventoryMutation::GrantItem(GrantInventoryItemInput { slot_id: "invslot_new".to_string(), item: build_stackable_item(3), })), ) .expect("grant should merge stackable row"); assert!(outcome.changed); assert_eq!(outcome.next_slots.len(), 1); assert_eq!(outcome.next_slots[0].quantity, 5); assert_eq!( outcome.updated_slot_ids, vec!["invslot_existing".to_string()] ); } #[test] fn grant_non_stackable_item_requires_single_quantity() { let error = apply_inventory_mutation( vec![], build_mutation_input(InventoryMutation::GrantItem(GrantInventoryItemInput { slot_id: "invslot_weapon".to_string(), item: InventoryItemSnapshot { item_id: "weapon_001".to_string(), category: "武器".to_string(), name: "试作短剑".to_string(), description: None, quantity: 2, rarity: InventoryItemRarity::Rare, tags: vec!["weapon".to_string()], stackable: false, stack_key: String::new(), equipment_slot_id: Some(InventoryEquipmentSlot::Weapon), source_kind: InventoryItemSourceKind::StoryReward, source_reference_id: None, }, })), ) .expect_err("non-stackable item quantity must stay single"); assert_eq!( error, InventoryMutationFieldError::NonStackableItemMustStaySingleQuantity ); } #[test] fn consume_item_removes_slot_when_quantity_exhausted() { let current = vec![InventorySlotSnapshot { slot_id: "invslot_potion".to_string(), runtime_session_id: "runtime_001".to_string(), story_session_id: Some("storysess_001".to_string()), actor_user_id: "user_001".to_string(), container_kind: InventoryContainerKind::Backpack, slot_key: "invslot_potion".to_string(), item_id: "consumable_heal_potion".to_string(), category: "消耗品".to_string(), name: "疗伤药".to_string(), description: None, quantity: 1, rarity: InventoryItemRarity::Common, tags: vec!["healing".to_string()], stackable: true, stack_key: "heal_potion".to_string(), equipment_slot_id: None, source_kind: InventoryItemSourceKind::TreasureReward, source_reference_id: None, created_at_micros: 1, updated_at_micros: 1, }]; let outcome = apply_inventory_mutation( current, build_mutation_input(InventoryMutation::ConsumeItem(ConsumeInventoryItemInput { slot_id: "invslot_potion".to_string(), quantity: 1, })), ) .expect("consume should remove exhausted slot"); assert!(outcome.next_slots.is_empty()); assert_eq!(outcome.removed_slot_ids, vec!["invslot_potion".to_string()]); } #[test] fn equip_item_swaps_existing_equipment_back_to_backpack() { let equipped = InventorySlotSnapshot { container_kind: InventoryContainerKind::Equipment, slot_key: InventoryEquipmentSlot::Weapon.as_str().to_string(), ..build_weapon_item("invslot_old_weapon", "旧佩剑") }; let backpack_weapon = build_weapon_item("invslot_new_weapon", "逐风短剑"); let outcome = apply_inventory_mutation( vec![equipped, backpack_weapon], build_mutation_input(InventoryMutation::EquipItem(EquipInventoryItemInput { slot_id: "invslot_new_weapon".to_string(), })), ) .expect("equip should swap weapon"); assert!(outcome.changed); assert_eq!( outcome.affected_equipment_slot, Some(InventoryEquipmentSlot::Weapon) ); let weapon_slot = outcome .next_slots .iter() .find(|slot| slot.slot_id == "invslot_new_weapon") .expect("new weapon slot should exist"); assert_eq!( weapon_slot.container_kind, InventoryContainerKind::Equipment ); assert_eq!(weapon_slot.slot_key, "weapon"); let old_weapon_slot = outcome .next_slots .iter() .find(|slot| slot.slot_id == "invslot_old_weapon") .expect("old weapon slot should exist"); assert_eq!( old_weapon_slot.container_kind, InventoryContainerKind::Backpack ); assert_eq!(old_weapon_slot.slot_key, "invslot_old_weapon"); } #[test] fn unequip_item_moves_equipment_back_to_backpack() { let equipped = InventorySlotSnapshot { container_kind: InventoryContainerKind::Equipment, slot_key: InventoryEquipmentSlot::Relic.as_str().to_string(), equipment_slot_id: Some(InventoryEquipmentSlot::Relic), ..build_weapon_item("invslot_relic", "旧誓护符") }; let outcome = apply_inventory_mutation( vec![equipped], build_mutation_input(InventoryMutation::UnequipItem(UnequipInventoryItemInput { slot_id: "invslot_relic".to_string(), })), ) .expect("unequip should move relic back to backpack"); assert!(outcome.changed); assert_eq!( outcome.affected_equipment_slot, Some(InventoryEquipmentSlot::Relic) ); assert_eq!(outcome.next_slots.len(), 1); assert_eq!( outcome.next_slots[0].container_kind, InventoryContainerKind::Backpack ); assert_eq!(outcome.next_slots[0].slot_key, "invslot_relic"); } #[test] fn build_runtime_inventory_state_query_input_trims_scope_fields() { let input = build_runtime_inventory_state_query_input( " runtime_001 ".to_string(), " user_001 ".to_string(), ) .expect("query input should build"); assert_eq!(input.runtime_session_id, "runtime_001"); assert_eq!(input.actor_user_id, "user_001"); } #[test] fn build_runtime_inventory_state_snapshot_splits_backpack_and_equipment() { let snapshot = build_runtime_inventory_state_snapshot( RuntimeInventoryStateQueryInput { runtime_session_id: "runtime_001".to_string(), actor_user_id: "user_001".to_string(), }, vec![ InventorySlotSnapshot { container_kind: InventoryContainerKind::Equipment, slot_key: "weapon".to_string(), ..build_weapon_item("invslot_weapon", "逐风短剑") }, InventorySlotSnapshot { slot_id: "invslot_potion".to_string(), runtime_session_id: "runtime_001".to_string(), story_session_id: Some("storysess_001".to_string()), actor_user_id: "user_001".to_string(), container_kind: InventoryContainerKind::Backpack, slot_key: "invslot_potion".to_string(), item_id: "consumable_heal_potion".to_string(), category: "消耗品".to_string(), name: "疗伤药".to_string(), description: Some("用于恢复少量气血。".to_string()), quantity: 2, rarity: InventoryItemRarity::Common, tags: vec!["healing".to_string()], stackable: true, stack_key: "heal_potion".to_string(), equipment_slot_id: None, source_kind: InventoryItemSourceKind::TreasureReward, source_reference_id: Some("treasure_001".to_string()), created_at_micros: 1, updated_at_micros: 2, }, ], ); assert_eq!(snapshot.backpack_items.len(), 1); assert_eq!(snapshot.equipment_items.len(), 1); assert_eq!(snapshot.backpack_items[0].slot_id, "invslot_potion"); assert_eq!(snapshot.equipment_items[0].slot_id, "invslot_weapon"); } }