Files
hunjing/backend/tests/application/test_big_cycle_drive.py
T
2026-08-21 16:00:13 +08:00

119 lines
4.3 KiB
Python

"""大周天内驱力生成测试(_phase_evaluation 子步骤 _generate_drive)。"""
import pytest
from app.application.big_cycle import drain_pending_big_cycles, maybe_trigger_big_cycle
from app.db.session import AsyncSessionLocal
from app.models.memory import Memory
from app.repositories import drive as drive_repo
from app.services.characters import get_active_character_model
_DRIVE_RESPONSE = (
'[{"question":"用户为何频繁提到项目X","source":"memory_gap",'
'"expected_value":0.7,"urgency":0.5,"risk_level":"L2",'
'"related_entities":["项目X"],'
'"subgoals":[{"title":"询问项目X目标","description":"主动询问用户",'
'"expected_information_gain":0.6,"interruption_cost":0.3,"risk_level":"L4"}]}]'
)
async def _seed_memories(character_id: str, count: int) -> None:
async with AsyncSessionLocal() as session:
for i in range(count):
session.add(
Memory(
character_id=character_id,
type="user",
title=f"记忆{i}",
content=f"内容{i}",
)
)
await session.commit()
@pytest.fixture
def mock_drive_chat(monkeypatch: pytest.MonkeyPatch) -> None:
async def fake_chat(*_args, **_kwargs):
return _DRIVE_RESPONSE
monkeypatch.setattr("app.application.big_cycle._call_model", fake_chat)
@pytest.mark.asyncio
async def test_big_cycle_generates_curiosity_and_subgoal(
mock_drive_chat, # noqa: ARG001 - pytest fixture 靠 monkeypatch 副作用生效
) -> None:
async with AsyncSessionLocal() as session:
character = await get_active_character_model(session)
await _seed_memories(character.id, 20)
cycle_id = await maybe_trigger_big_cycle(character.id, trigger="test")
assert cycle_id is not None
await drain_pending_big_cycles()
async with AsyncSessionLocal() as session:
curiosities, c_total = await drive_repo.list_curiosities(session, character.id)
subgoals, s_total = await drive_repo.list_subgoals(session, character.id)
assert c_total == 1
assert curiosities[0].question == "用户为何频繁提到项目X"
assert curiosities[0].source == "memory_gap"
assert curiosities[0].cycle_id == cycle_id
assert s_total == 1
assert subgoals[0].status == "candidate"
assert subgoals[0].curiosity_question_id == curiosities[0].id
assert subgoals[0].title == "询问项目X目标"
@pytest.mark.asyncio
async def test_big_cycle_auto_approves_low_risk_subgoal(
mock_drive_chat, # noqa: ARG001
) -> None:
"""低风险次生目标(L0-L2)应在大周天中自动通过并生成 Goal。"""
from sqlalchemy import select
from app.models.governance import Goal
# 使用低风险的子目标响应
_low_risk_response = (
'[{"question":"还有哪些未知领域","source":"world_uncertainty",'
'"expected_value":0.6,"urgency":0.4,"risk_level":"L1",'
'"subgoals":[{"title":"探索未知领域","description":"自动探索",'
'"expected_information_gain":0.5,"interruption_cost":0.2,"risk_level":"L1"}]}]'
)
async def fake_low_risk(*_args, **_kwargs):
return _low_risk_response
import app.application.big_cycle as bc
original = bc._call_model
bc._call_model = fake_low_risk
async with AsyncSessionLocal() as session:
character = await get_active_character_model(session)
await _seed_memories(character.id, 20)
cycle_id = await maybe_trigger_big_cycle(character.id, trigger="test")
assert cycle_id is not None
await drain_pending_big_cycles()
bc._call_model = original
async with AsyncSessionLocal() as session:
subgoals, s_total = await drive_repo.list_subgoals(session, character.id)
assert s_total == 1
assert subgoals[0].status in ("approved", "auto_execute_pending"), (
f"预期 approved 或 auto_execute_pending,实际={subgoals[0].status}"
)
# 验证自动生成了 Goal
async with AsyncSessionLocal() as session:
result = await session.execute(
select(Goal).where(Goal.character_id == character.id)
)
goals = result.scalars().all()
matching = [g for g in goals if g.title == "探索未知领域"]
assert len(matching) == 1
assert matching[0].level == "task"