* update relation * feat: add relation_type to avatar info structure and update related components - Added `relation_type` to the avatar structured info in `info_presenter.py`. - Updated `AvatarDetail.vue` to utilize the new `relation_type` for displaying avatar relationships. - Modified `RelationRow.vue` to accept `type` as a prop for enhanced relationship representation. - Updated `core.ts` to include `relation_type` in the `RelationInfo` interface. Closes #
202 lines
8.4 KiB
Python
202 lines
8.4 KiB
Python
import pytest
|
||
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.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):
|
||
"""创建第二个角色作为赠送目标"""
|
||
av = 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,
|
||
personas=[], # 显式清空特质,避免随机特质影响价格测试
|
||
)
|
||
# 强制重算一次效果,确保属性干净
|
||
av.recalc_effects()
|
||
return av
|
||
|
||
@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', new_callable=MagicMock) 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 # Prices 系统接管后,价格由 Realm 决定 (练气期=150),不再手动指定
|
||
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
|
||
# 练气期武器基准价 150,卖出倍率 1.0 (无特质加成) -> 150
|
||
assert target_avatar.magic_stone == 150
|
||
|
||
# --- 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"]
|