refactor gift

This commit is contained in:
bridge
2026-01-11 20:33:54 +08:00
parent 2056538375
commit 3a0e432b02
4 changed files with 463 additions and 105 deletions

198
tests/test_action_gift.py Normal file
View File

@@ -0,0 +1,198 @@
import pytest
import asyncio
from unittest.mock import patch, MagicMock
from src.classes.mutual_action.gift import Gift
from src.classes.action_runtime import ActionResult, ActionStatus
from src.classes.avatar import Avatar, Gender
from src.classes.age import Age
from src.classes.cultivation import Realm
from src.classes.relation import Relation
from src.utils.id_generator import get_avatar_id
from src.classes.calendar import create_month_stamp, Year, Month
# -----------------------------------------------------------------------------
# Fixtures
# -----------------------------------------------------------------------------
@pytest.fixture
def target_avatar(base_world):
"""创建第二个角色作为赠送目标"""
return Avatar(
world=base_world,
name="TargetNPC",
id=get_avatar_id(),
birth_month_stamp=create_month_stamp(Year(2000), Month.JANUARY),
age=Age(20, Realm.Qi_Refinement),
gender=Gender.FEMALE,
pos_x=0,
pos_y=0
)
@pytest.fixture
def gift_action(dummy_avatar, base_world):
"""初始化 Gift 动作"""
# 模拟 _call_llm_feedback避免 step 中调用 asyncio.get_running_loop()
with patch.object(Gift, '_call_llm_feedback') as mock_llm:
# 返回一个 mock task确保 task.done() 初始为 False
# 但在这里我们主要是为了让 step 不报错
mock_llm.return_value = {}
action = Gift(dummy_avatar, base_world)
# 我们直接 Mock 掉 step 里的异步创建任务逻辑,
# 因为我们主要测试的是:
# 1. 参数是否正确解析 (step -> _resolve_gift)
# 2. _can_start 逻辑
# 3. 反馈结算逻辑 _settle_feedback / _apply_gift
# 但 step 本身有逻辑:
# 1. 解析参数
# 2. 检查 target
# 3. 创建 task (这里会报错)
# 我们采用 patch asyncio.get_running_loop 的方式更简单
yield action
# -----------------------------------------------------------------------------
# Tests
# -----------------------------------------------------------------------------
class TestGiftAction:
# --- 1. 赠送灵石 ---
def test_gift_spirit_stone_success(self, gift_action, dummy_avatar, target_avatar):
"""测试赠送灵石成功"""
dummy_avatar.magic_stone = 1000
target_avatar.magic_stone = 0
# Mock asyncio loop just to pass the step() check
with patch("asyncio.get_running_loop", return_value=MagicMock()):
gift_action.step(target_avatar, item_name="灵石", amount=100)
can_start, reason = gift_action._can_start(target_avatar)
assert can_start is True, f"Should be able to start: {reason}"
# 模拟接受
gift_action._settle_feedback(target_avatar, "Accept")
assert dummy_avatar.magic_stone == 900
assert target_avatar.magic_stone == 100
assert gift_action._gift_success is True
def test_gift_spirit_stone_insufficient(self, gift_action, dummy_avatar, target_avatar):
"""测试灵石不足"""
dummy_avatar.magic_stone = 50
with patch("asyncio.get_running_loop", return_value=MagicMock()):
gift_action.step(target_avatar, item_name="灵石", amount=100)
can_start, reason = gift_action._can_start(target_avatar)
assert can_start is False
assert "灵石不足" in reason
# --- 2. 赠送素材 ---
def test_gift_material_success(self, gift_action, dummy_avatar, target_avatar, mock_item_data):
"""测试赠送素材成功"""
test_material = mock_item_data["obj_material"]
dummy_avatar.add_material(test_material, quantity=5)
with patch("asyncio.get_running_loop", return_value=MagicMock()):
# 非灵石强制数量 1
gift_action.step(target_avatar, item_name=test_material.name, amount=999)
can_start, reason = gift_action._can_start(target_avatar)
assert can_start is True
gift_action._settle_feedback(target_avatar, "Accept")
assert dummy_avatar.get_material_quantity(test_material) == 4
assert target_avatar.get_material_quantity(test_material) == 1
assert gift_action._current_gift_context["amount"] == 1
def test_gift_material_not_owned(self, gift_action, dummy_avatar, target_avatar, mock_item_data):
"""测试赠送未持有的素材"""
test_material = mock_item_data["obj_material"]
with patch("asyncio.get_running_loop", return_value=MagicMock()):
gift_action.step(target_avatar, item_name=test_material.name, amount=1)
can_start, reason = gift_action._can_start(target_avatar)
assert can_start is False
# --- 3. 赠送装备 ---
def test_gift_weapon_success_and_auto_equip(self, gift_action, dummy_avatar, target_avatar, mock_item_data):
"""测试赠送装备成功,且目标自动装备"""
test_weapon = mock_item_data["obj_weapon"]
dummy_avatar.weapon = test_weapon
assert target_avatar.weapon is None
with patch("asyncio.get_running_loop", return_value=MagicMock()):
gift_action.step(target_avatar, item_name=test_weapon.name, amount=1)
can_start, reason = gift_action._can_start(target_avatar)
assert can_start is True
gift_action._settle_feedback(target_avatar, "Accept")
assert dummy_avatar.weapon is None
assert target_avatar.weapon == test_weapon
def test_gift_weapon_fail_not_equipped(self, gift_action, dummy_avatar, target_avatar, mock_item_data):
"""测试赠送未装备的装备"""
test_weapon = mock_item_data["obj_weapon"]
with patch("asyncio.get_running_loop", return_value=MagicMock()):
gift_action.step(target_avatar, item_name=test_weapon.name, amount=1)
can_start, reason = gift_action._can_start(target_avatar)
assert can_start is False
def test_gift_weapon_target_trade_in(self, gift_action, dummy_avatar, target_avatar, mock_item_data):
"""测试目标已有装备时,收到新装备会自动折价卖出旧的"""
from tests.conftest import create_test_weapon
from src.classes.cultivation import Realm
new_weapon = mock_item_data["obj_weapon"]
dummy_avatar.weapon = new_weapon
old_weapon = create_test_weapon("旧铁剑", Realm.Qi_Refinement, weapon_id=999)
old_weapon.price = 100
target_avatar.weapon = old_weapon
target_avatar.magic_stone = 0
with patch("asyncio.get_running_loop", return_value=MagicMock()):
gift_action.step(target_avatar, item_name=new_weapon.name, amount=1)
gift_action._settle_feedback(target_avatar, "Accept")
assert target_avatar.weapon == new_weapon
# 50% refund
assert target_avatar.magic_stone == 50
# --- 4. 上下文与描述 ---
def test_prompt_info_description(self, gift_action, dummy_avatar, target_avatar, mock_item_data):
"""验证传给 LLM 的 prompt 中包含具体物品描述"""
test_weapon = mock_item_data["obj_weapon"]
dummy_avatar.weapon = test_weapon
with patch("asyncio.get_running_loop", return_value=MagicMock()):
gift_action.step(target_avatar, item_name=test_weapon.name)
infos = gift_action._build_prompt_infos(target_avatar)
assert f"[{test_weapon.name}]" in infos["action_info"]
assert "赠送" in infos["action_info"]
def test_prompt_info_description_stones(self, gift_action, dummy_avatar, target_avatar):
dummy_avatar.magic_stone = 1000
with patch("asyncio.get_running_loop", return_value=MagicMock()):
gift_action.step(target_avatar, item_name="灵石", amount=500)
infos = gift_action._build_prompt_infos(target_avatar)
assert "500 灵石" in infos["action_info"]