* 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 #
615 lines
25 KiB
Python
615 lines
25 KiB
Python
"""
|
|
Tests for src/classes/mutual_action/ modules.
|
|
|
|
This module tests mutual action classes including:
|
|
- Talk: initiate conversation
|
|
- Spar: friendly combat for weapon proficiency
|
|
- Impart: master teaching disciple
|
|
|
|
Testing Strategy:
|
|
We mock `call_llm_with_task_name` to simulate LLM feedback responses.
|
|
This allows testing the action logic without actual LLM calls.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import MagicMock, AsyncMock, patch
|
|
|
|
from src.classes.mutual_action.talk import Talk
|
|
from src.classes.mutual_action.spar import Spar
|
|
from src.classes.mutual_action.impart import Impart
|
|
from src.classes.action_runtime import ActionStatus
|
|
from src.classes.relation.relation import Relation
|
|
|
|
|
|
class TestTalk:
|
|
"""Tests for Talk mutual action."""
|
|
|
|
@pytest.fixture
|
|
def target_avatar(self, base_world, dummy_avatar):
|
|
"""Create a target avatar for talk tests."""
|
|
from src.classes.avatar import Avatar, Gender
|
|
from src.classes.age import Age
|
|
from src.classes.cultivation import Realm
|
|
from src.classes.calendar import Year, Month, create_month_stamp
|
|
from src.classes.root import Root
|
|
from src.classes.alignment import Alignment
|
|
from src.utils.id_generator import get_avatar_id
|
|
|
|
target = Avatar(
|
|
world=base_world,
|
|
name="TalkTarget",
|
|
id=get_avatar_id(),
|
|
birth_month_stamp=create_month_stamp(Year(2000), Month.JANUARY),
|
|
age=Age(25, Realm.Qi_Refinement),
|
|
gender=Gender.FEMALE,
|
|
pos_x=0, # Same position as dummy_avatar
|
|
pos_y=0,
|
|
root=Root.WATER,
|
|
personas=[],
|
|
alignment=Alignment.NEUTRAL
|
|
)
|
|
target.weapon = MagicMock()
|
|
target.weapon.get_detailed_info.return_value = "Test Weapon"
|
|
target.thinking = ""
|
|
target.short_term_objective = ""
|
|
# Register in world
|
|
base_world.avatar_manager.avatars[target.name] = target
|
|
return target
|
|
|
|
def test_talk_can_start_success(self, dummy_avatar, target_avatar):
|
|
"""Test that Talk can start when target is in observation range."""
|
|
action = Talk(dummy_avatar, dummy_avatar.world)
|
|
|
|
with patch("src.classes.observe.is_within_observation", return_value=True):
|
|
can_start, reason = action.can_start(target_avatar)
|
|
|
|
assert can_start is True
|
|
assert reason == ""
|
|
|
|
def test_talk_cannot_start_target_not_in_range(self, dummy_avatar, target_avatar):
|
|
"""Test that Talk cannot start when target is out of range."""
|
|
action = Talk(dummy_avatar, dummy_avatar.world)
|
|
|
|
with patch("src.classes.observe.is_within_observation", return_value=False):
|
|
can_start, reason = action.can_start(target_avatar)
|
|
|
|
assert can_start is False
|
|
assert "不在交互范围内" in reason
|
|
|
|
def test_talk_cannot_start_self_target(self, dummy_avatar):
|
|
"""Test that Talk cannot target self."""
|
|
action = Talk(dummy_avatar, dummy_avatar.world)
|
|
can_start, reason = action.can_start(dummy_avatar)
|
|
|
|
assert can_start is False
|
|
assert "自己" in reason
|
|
|
|
def test_talk_cannot_start_target_not_exist(self, dummy_avatar):
|
|
"""Test that Talk cannot start with non-existent target."""
|
|
action = Talk(dummy_avatar, dummy_avatar.world)
|
|
can_start, reason = action.can_start("NonExistentAvatar")
|
|
|
|
assert can_start is False
|
|
assert "不存在" in reason
|
|
|
|
def test_talk_start_returns_event(self, dummy_avatar, target_avatar):
|
|
"""Test that Talk.start() returns proper event."""
|
|
action = Talk(dummy_avatar, dummy_avatar.world)
|
|
event = action.start(target_avatar)
|
|
|
|
assert event is not None
|
|
assert dummy_avatar.name in event.content
|
|
assert target_avatar.name in event.content
|
|
assert "攀谈" in event.content
|
|
assert dummy_avatar.id in event.related_avatars
|
|
assert target_avatar.id in event.related_avatars
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_talk_step_with_accept_feedback(self, dummy_avatar, target_avatar):
|
|
"""Test Talk step flow when target accepts."""
|
|
action = Talk(dummy_avatar, dummy_avatar.world)
|
|
action._start_month_stamp = 100
|
|
|
|
# Mock target's methods for loading action chain
|
|
target_avatar.load_decide_result_chain = MagicMock()
|
|
target_avatar.commit_next_plan = MagicMock(return_value=None)
|
|
|
|
mock_response = {
|
|
target_avatar.name: {
|
|
"thinking": "This person seems friendly.",
|
|
"feedback": "Talk"
|
|
}
|
|
}
|
|
|
|
with patch("src.classes.observe.is_within_observation", return_value=True):
|
|
with patch("src.classes.mutual_action.mutual_action.call_llm_with_task_name", new_callable=AsyncMock) as mock_llm:
|
|
mock_llm.return_value = mock_response
|
|
|
|
# First step: trigger LLM task
|
|
res1 = action.step(target_avatar)
|
|
assert res1.status == ActionStatus.RUNNING
|
|
assert action._feedback_task is not None
|
|
|
|
# Wait for task
|
|
await action._feedback_task
|
|
|
|
# Second step: consume result
|
|
res2 = action.step(target_avatar)
|
|
assert res2.status == ActionStatus.COMPLETED
|
|
|
|
# Should have accept event
|
|
assert len(res2.events) >= 1
|
|
assert "接受" in res2.events[0].content
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_talk_step_with_reject_feedback(self, dummy_avatar, target_avatar):
|
|
"""Test Talk step flow when target rejects."""
|
|
action = Talk(dummy_avatar, dummy_avatar.world)
|
|
action._start_month_stamp = 100
|
|
|
|
mock_response = {
|
|
target_avatar.name: {
|
|
"thinking": "I don't want to talk.",
|
|
"feedback": "Reject"
|
|
}
|
|
}
|
|
|
|
with patch("src.classes.observe.is_within_observation", return_value=True):
|
|
with patch("src.classes.mutual_action.mutual_action.call_llm_with_task_name", new_callable=AsyncMock) as mock_llm:
|
|
mock_llm.return_value = mock_response
|
|
|
|
res1 = action.step(target_avatar)
|
|
assert action._feedback_task is not None
|
|
await action._feedback_task
|
|
res2 = action.step(target_avatar)
|
|
|
|
assert res2.status == ActionStatus.COMPLETED
|
|
assert len(res2.events) >= 1
|
|
assert "拒绝" in res2.events[0].content
|
|
|
|
def test_talk_step_with_none_target(self, dummy_avatar):
|
|
"""Test Talk step with None target returns FAILED."""
|
|
action = Talk(dummy_avatar, dummy_avatar.world)
|
|
res = action.step(None) # type: ignore[arg-type] - intentionally testing None input
|
|
|
|
assert res.status == ActionStatus.FAILED
|
|
|
|
|
|
class TestSpar:
|
|
"""Tests for Spar mutual action."""
|
|
|
|
@pytest.fixture
|
|
def target_avatar(self, base_world, dummy_avatar):
|
|
"""Create a target avatar for spar tests."""
|
|
from src.classes.avatar import Avatar, Gender
|
|
from src.classes.age import Age
|
|
from src.classes.cultivation import Realm
|
|
from src.classes.calendar import Year, Month, create_month_stamp
|
|
from src.classes.root import Root
|
|
from src.classes.alignment import Alignment
|
|
from src.utils.id_generator import get_avatar_id
|
|
|
|
target = Avatar(
|
|
world=base_world,
|
|
name="SparTarget",
|
|
id=get_avatar_id(),
|
|
birth_month_stamp=create_month_stamp(Year(2000), Month.JANUARY),
|
|
age=Age(25, Realm.Qi_Refinement),
|
|
gender=Gender.MALE,
|
|
pos_x=0,
|
|
pos_y=0,
|
|
root=Root.FIRE,
|
|
personas=[],
|
|
alignment=Alignment.NEUTRAL
|
|
)
|
|
target.weapon = MagicMock()
|
|
target.weapon.get_detailed_info.return_value = "Test Sword"
|
|
target.weapon_proficiency = 50.0
|
|
target.thinking = ""
|
|
target.short_term_objective = ""
|
|
# Mock methods for spar
|
|
target.increase_weapon_proficiency = MagicMock()
|
|
target.add_event = MagicMock()
|
|
base_world.avatar_manager.avatars[target.name] = target
|
|
return target
|
|
|
|
def test_spar_has_cooldown(self):
|
|
"""Test that Spar has cooldown configured."""
|
|
assert Spar.ACTION_CD_MONTHS == 12
|
|
|
|
def test_spar_start_returns_event(self, dummy_avatar, target_avatar):
|
|
"""Test that Spar.start() returns proper event."""
|
|
action = Spar(dummy_avatar, dummy_avatar.world)
|
|
event = action.start(target_avatar)
|
|
|
|
assert event is not None
|
|
assert dummy_avatar.name in event.content
|
|
assert target_avatar.name in event.content
|
|
assert "切磋" in event.content
|
|
|
|
def test_spar_settle_feedback_accept(self, dummy_avatar, target_avatar):
|
|
"""Test that accepting spar increases weapon proficiency."""
|
|
action = Spar(dummy_avatar, dummy_avatar.world)
|
|
dummy_avatar.increase_weapon_proficiency = MagicMock()
|
|
dummy_avatar.add_event = MagicMock()
|
|
|
|
# Mock battle result
|
|
with patch("src.classes.mutual_action.spar.decide_battle") as mock_battle:
|
|
mock_battle.return_value = (dummy_avatar, target_avatar, 0, 0)
|
|
|
|
action._settle_feedback(target_avatar, "Accept")
|
|
|
|
# Both should have proficiency increased
|
|
dummy_avatar.increase_weapon_proficiency.assert_called_once()
|
|
target_avatar.increase_weapon_proficiency.assert_called_once()
|
|
|
|
# Winner (dummy_avatar) should get more
|
|
winner_gain = dummy_avatar.increase_weapon_proficiency.call_args[0][0]
|
|
loser_gain = target_avatar.increase_weapon_proficiency.call_args[0][0]
|
|
assert winner_gain > loser_gain
|
|
|
|
def test_spar_settle_feedback_reject(self, dummy_avatar, target_avatar):
|
|
"""Test that rejecting spar does nothing."""
|
|
action = Spar(dummy_avatar, dummy_avatar.world)
|
|
dummy_avatar.increase_weapon_proficiency = MagicMock()
|
|
|
|
action._settle_feedback(target_avatar, "Reject")
|
|
|
|
# Should not increase proficiency
|
|
dummy_avatar.increase_weapon_proficiency.assert_not_called()
|
|
target_avatar.increase_weapon_proficiency.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_spar_finish_generates_story(self, dummy_avatar, target_avatar):
|
|
"""Test that Spar.finish() generates a story event.
|
|
|
|
Note: cooldown_action decorator wraps finish() to use **kwargs,
|
|
so we must call with keyword arguments.
|
|
"""
|
|
action = Spar(dummy_avatar, dummy_avatar.world)
|
|
action._last_result = (dummy_avatar, target_avatar, 15.0, 5.0)
|
|
|
|
with patch("src.classes.mutual_action.spar.StoryTeller.tell_story", new_callable=AsyncMock) as mock_story:
|
|
mock_story.return_value = "A great sparring match occurred."
|
|
|
|
# cooldown_action wraps finish to accept **kwargs
|
|
result = action.finish(target_avatar=target_avatar)
|
|
events = await result # The wrapper returns coroutine
|
|
|
|
assert len(events) == 1
|
|
assert events[0].is_story is True
|
|
assert "great sparring match" in events[0].content
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_spar_finish_without_result(self, dummy_avatar, target_avatar):
|
|
"""Test that Spar.finish() returns empty list without result."""
|
|
action = Spar(dummy_avatar, dummy_avatar.world)
|
|
# No _last_result set
|
|
|
|
# cooldown_action wraps finish to accept **kwargs
|
|
result = action.finish(target_avatar=target_avatar)
|
|
events = await result
|
|
|
|
assert events == []
|
|
|
|
|
|
class TestImpart:
|
|
"""Tests for Impart mutual action."""
|
|
|
|
@pytest.fixture
|
|
def master_avatar(self, base_world):
|
|
"""Create a master avatar (high level).
|
|
|
|
Note: Avatar's cultivation_progress defaults to level 0.
|
|
We must manually set level to ensure level diff >= 20 for Impart.
|
|
"""
|
|
from src.classes.avatar import Avatar, Gender
|
|
from src.classes.age import Age
|
|
from src.classes.cultivation import Realm, CultivationProgress
|
|
from src.classes.calendar import Year, Month, create_month_stamp
|
|
from src.classes.root import Root
|
|
from src.classes.alignment import Alignment
|
|
from src.utils.id_generator import get_avatar_id
|
|
|
|
master = Avatar(
|
|
world=base_world,
|
|
name="MasterAvatar",
|
|
id=get_avatar_id(),
|
|
birth_month_stamp=create_month_stamp(Year(1900), Month.JANUARY),
|
|
age=Age(100, Realm.Nascent_Soul),
|
|
gender=Gender.MALE,
|
|
pos_x=0,
|
|
pos_y=0,
|
|
root=Root.GOLD,
|
|
personas=[],
|
|
alignment=Alignment.RIGHTEOUS
|
|
)
|
|
# Set high cultivation level (Nascent Soul = 91+)
|
|
master.cultivation_progress = CultivationProgress(level=95, exp=0)
|
|
master.weapon = MagicMock()
|
|
master.weapon.get_detailed_info.return_value = "Master Sword"
|
|
master.thinking = ""
|
|
master.short_term_objective = ""
|
|
base_world.avatar_manager.avatars[master.name] = master
|
|
return master
|
|
|
|
@pytest.fixture
|
|
def disciple_avatar(self, base_world):
|
|
"""Create a disciple avatar (low level)."""
|
|
from src.classes.avatar import Avatar, Gender
|
|
from src.classes.age import Age
|
|
from src.classes.cultivation import Realm, CultivationProgress
|
|
from src.classes.calendar import Year, Month, create_month_stamp
|
|
from src.classes.root import Root
|
|
from src.classes.alignment import Alignment
|
|
from src.utils.id_generator import get_avatar_id
|
|
|
|
disciple = Avatar(
|
|
world=base_world,
|
|
name="DiscipleAvatar",
|
|
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,
|
|
root=Root.WATER,
|
|
personas=[],
|
|
alignment=Alignment.RIGHTEOUS
|
|
)
|
|
# Set low cultivation level (Qi Refinement = 1-30)
|
|
disciple.cultivation_progress = CultivationProgress(level=10, exp=0)
|
|
disciple.weapon = MagicMock()
|
|
disciple.weapon.get_detailed_info.return_value = "Disciple Sword"
|
|
disciple.thinking = ""
|
|
disciple.short_term_objective = ""
|
|
disciple.add_event = MagicMock()
|
|
base_world.avatar_manager.avatars[disciple.name] = disciple
|
|
return disciple
|
|
|
|
def test_impart_has_cooldown(self):
|
|
"""Test that Impart has cooldown configured."""
|
|
assert Impart.ACTION_CD_MONTHS == 6
|
|
|
|
def test_impart_can_start_success(self, master_avatar, disciple_avatar):
|
|
"""Test Impart can start with valid master-disciple relation."""
|
|
action = Impart(master_avatar, master_avatar.world)
|
|
|
|
# Mock relation check: master has MASTER relation to disciple
|
|
master_avatar.get_relation = MagicMock(return_value=Relation.MASTER)
|
|
|
|
with patch("src.classes.observe.is_within_observation", return_value=True):
|
|
can_start, reason = action.can_start(target_avatar=disciple_avatar)
|
|
|
|
assert can_start is True
|
|
assert reason == ""
|
|
|
|
def test_impart_cannot_start_not_disciple(self, master_avatar, disciple_avatar):
|
|
"""Test Impart cannot start when target is not disciple."""
|
|
action = Impart(master_avatar, master_avatar.world)
|
|
|
|
# Mock relation check: not master-disciple
|
|
master_avatar.get_relation = MagicMock(return_value=Relation.FRIEND)
|
|
|
|
with patch("src.classes.observe.is_within_observation", return_value=True):
|
|
can_start, reason = action.can_start(target_avatar=disciple_avatar)
|
|
|
|
assert can_start is False
|
|
assert "徒弟" in reason
|
|
|
|
def test_impart_cannot_start_level_diff_too_small(self, master_avatar, disciple_avatar):
|
|
"""Test Impart cannot start when level difference < 20."""
|
|
action = Impart(master_avatar, master_avatar.world)
|
|
|
|
# Mock relation check
|
|
master_avatar.get_relation = MagicMock(return_value=Relation.MASTER)
|
|
|
|
# Set levels close together
|
|
master_avatar.cultivation_progress.level = 25
|
|
disciple_avatar.cultivation_progress.level = 10 # Diff = 15 < 20
|
|
|
|
with patch("src.classes.observe.is_within_observation", return_value=True):
|
|
can_start, reason = action.can_start(target_avatar=disciple_avatar)
|
|
|
|
assert can_start is False
|
|
assert "等级差不足20级" in reason
|
|
|
|
def test_impart_cannot_start_target_not_in_range(self, master_avatar, disciple_avatar):
|
|
"""Test Impart cannot start when target out of range."""
|
|
action = Impart(master_avatar, master_avatar.world)
|
|
|
|
master_avatar.get_relation = MagicMock(return_value=Relation.MASTER)
|
|
|
|
with patch("src.classes.observe.is_within_observation", return_value=False):
|
|
can_start, reason = action.can_start(target_avatar=disciple_avatar)
|
|
|
|
assert can_start is False
|
|
assert "不在交互范围内" in reason
|
|
|
|
def test_impart_start_returns_event(self, master_avatar, disciple_avatar):
|
|
"""Test that Impart.start() returns proper event."""
|
|
action = Impart(master_avatar, master_avatar.world)
|
|
event = action.start(disciple_avatar)
|
|
|
|
assert event is not None
|
|
assert master_avatar.name in event.content
|
|
assert disciple_avatar.name in event.content
|
|
assert "传道" in event.content
|
|
assert hasattr(action, '_impart_success')
|
|
assert action._impart_success is False
|
|
|
|
def test_impart_settle_feedback_accept(self, master_avatar, disciple_avatar):
|
|
"""Test that accepting impart gives exp to disciple."""
|
|
action = Impart(master_avatar, master_avatar.world)
|
|
action._impart_success = False
|
|
action._impart_exp_gain = 0
|
|
|
|
initial_exp = disciple_avatar.cultivation_progress.exp
|
|
|
|
action._settle_feedback(disciple_avatar, "Accept")
|
|
|
|
assert action._impart_success is True
|
|
assert action._impart_exp_gain == 2000 # 100 * 5 * 4
|
|
# Exp should be added to disciple
|
|
assert disciple_avatar.cultivation_progress.exp > initial_exp
|
|
|
|
def test_impart_settle_feedback_reject(self, master_avatar, disciple_avatar):
|
|
"""Test that rejecting impart does not give exp."""
|
|
action = Impart(master_avatar, master_avatar.world)
|
|
action._impart_success = False
|
|
action._impart_exp_gain = 0
|
|
|
|
initial_exp = disciple_avatar.cultivation_progress.exp
|
|
|
|
action._settle_feedback(disciple_avatar, "Reject")
|
|
|
|
assert action._impart_success is False
|
|
# Exp should not change
|
|
assert disciple_avatar.cultivation_progress.exp == initial_exp
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_impart_finish_with_success(self, master_avatar, disciple_avatar):
|
|
"""Test Impart.finish() generates result event on success.
|
|
|
|
Note: cooldown_action decorator wraps finish() to use **kwargs.
|
|
"""
|
|
action = Impart(master_avatar, master_avatar.world)
|
|
action._impart_success = True
|
|
action._impart_exp_gain = 2000
|
|
|
|
# cooldown_action wraps finish to accept **kwargs
|
|
result = action.finish(target_avatar=disciple_avatar)
|
|
events = await result
|
|
|
|
assert len(events) == 1
|
|
assert "2000" in events[0].content
|
|
assert disciple_avatar.name in events[0].content
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_impart_finish_with_failure(self, master_avatar, disciple_avatar):
|
|
"""Test Impart.finish() returns empty on rejection."""
|
|
action = Impart(master_avatar, master_avatar.world)
|
|
action._impart_success = False
|
|
action._impart_exp_gain = 0
|
|
|
|
# cooldown_action wraps finish to accept **kwargs
|
|
result = action.finish(target_avatar=disciple_avatar)
|
|
events = await result
|
|
|
|
assert events == []
|
|
|
|
|
|
class TestMutualActionBase:
|
|
"""Tests for MutualAction base class."""
|
|
|
|
@pytest.fixture
|
|
def target_avatar(self, base_world, dummy_avatar):
|
|
"""Create a generic target avatar."""
|
|
from src.classes.avatar import Avatar, Gender
|
|
from src.classes.age import Age
|
|
from src.classes.cultivation import Realm
|
|
from src.classes.calendar import Year, Month, create_month_stamp
|
|
from src.classes.root import Root
|
|
from src.classes.alignment import Alignment
|
|
from src.utils.id_generator import get_avatar_id
|
|
|
|
target = Avatar(
|
|
world=base_world,
|
|
name="GenericTarget",
|
|
id=get_avatar_id(),
|
|
birth_month_stamp=create_month_stamp(Year(2000), Month.JANUARY),
|
|
age=Age(25, Realm.Qi_Refinement),
|
|
gender=Gender.FEMALE,
|
|
pos_x=0,
|
|
pos_y=0,
|
|
root=Root.EARTH,
|
|
personas=[],
|
|
alignment=Alignment.NEUTRAL
|
|
)
|
|
target.weapon = MagicMock()
|
|
target.weapon.get_detailed_info.return_value = "Test Weapon"
|
|
target.thinking = ""
|
|
target.is_dead = False
|
|
base_world.avatar_manager.avatars[target.name] = target
|
|
return target
|
|
|
|
def test_cannot_start_with_dead_target(self, dummy_avatar, target_avatar):
|
|
"""Test that mutual action cannot start with dead target."""
|
|
action = Talk(dummy_avatar, dummy_avatar.world)
|
|
target_avatar.is_dead = True
|
|
|
|
can_start, reason = action.can_start(target_avatar)
|
|
|
|
assert can_start is False
|
|
assert "死亡" in reason
|
|
|
|
def test_get_target_avatar_by_name(self, dummy_avatar, target_avatar):
|
|
"""Test _get_target_avatar with string name."""
|
|
action = Talk(dummy_avatar, dummy_avatar.world)
|
|
|
|
result = action._get_target_avatar("GenericTarget")
|
|
|
|
assert result == target_avatar
|
|
|
|
def test_get_target_avatar_by_object(self, dummy_avatar, target_avatar):
|
|
"""Test _get_target_avatar with Avatar object."""
|
|
action = Talk(dummy_avatar, dummy_avatar.world)
|
|
|
|
result = action._get_target_avatar(target_avatar)
|
|
|
|
assert result == target_avatar
|
|
|
|
def test_get_target_avatar_not_found(self, dummy_avatar):
|
|
"""Test _get_target_avatar returns None for non-existent name."""
|
|
action = Talk(dummy_avatar, dummy_avatar.world)
|
|
|
|
result = action._get_target_avatar("NonExistent")
|
|
|
|
assert result is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_step_running_then_completed(self, dummy_avatar, target_avatar):
|
|
"""Test that step returns RUNNING first, then COMPLETED."""
|
|
action = Talk(dummy_avatar, dummy_avatar.world)
|
|
action._start_month_stamp = 100
|
|
|
|
mock_response = {
|
|
target_avatar.name: {
|
|
"thinking": "Test thinking",
|
|
"feedback": "Reject"
|
|
}
|
|
}
|
|
|
|
with patch("src.classes.observe.is_within_observation", return_value=True):
|
|
with patch("src.classes.mutual_action.mutual_action.call_llm_with_task_name", new_callable=AsyncMock) as mock_llm:
|
|
mock_llm.return_value = mock_response
|
|
|
|
# First call should be RUNNING
|
|
res1 = action.step(target_avatar)
|
|
assert res1.status == ActionStatus.RUNNING
|
|
assert action._feedback_task is not None
|
|
|
|
# Wait for task
|
|
await action._feedback_task
|
|
|
|
# Second call should be COMPLETED
|
|
res2 = action.step(target_avatar)
|
|
assert res2.status == ActionStatus.COMPLETED
|
|
assert action._feedback_task is None
|
|
assert action._feedback_cached is None
|
|
|
|
def test_build_prompt_infos(self, dummy_avatar, target_avatar):
|
|
"""Test _build_prompt_infos returns correct structure."""
|
|
action = Talk(dummy_avatar, dummy_avatar.world)
|
|
|
|
infos = action._build_prompt_infos(target_avatar)
|
|
|
|
assert "world_info" in infos
|
|
assert "avatar_infos" in infos
|
|
assert "avatar_name_1" in infos
|
|
assert "avatar_name_2" in infos
|
|
assert "action_name" in infos
|
|
assert "feedback_actions" in infos
|
|
assert infos["avatar_name_1"] == dummy_avatar.name
|
|
assert infos["avatar_name_2"] == target_avatar.name
|