"""内驱力 /drive API 测试。""" from collections.abc import AsyncGenerator import pytest from httpx import ASGITransport, AsyncClient from app.db.session import AsyncSessionLocal from app.main import app from app.models.big_cycle import BigCycle from app.models.drive import CuriosityQuestion, ExploratorySubgoal from app.services.characters import get_active_character_model @pytest.fixture async def client() -> AsyncGenerator[AsyncClient, None]: transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as http_client: yield http_client async def _seed(character_id: str, subgoal_status: str = "candidate") -> str: async with AsyncSessionLocal() as session: cycle = BigCycle( character_id=character_id, trigger="test", status="completed", memory_count_at_trigger=20, ) session.add(cycle) await session.flush() curios = CuriosityQuestion( character_id=character_id, cycle_id=cycle.id, question="用户为何频繁提到项目X", source="memory_gap", expected_value=0.7, urgency=0.5, risk_level="L2", ) session.add(curios) await session.flush() sub = ExploratorySubgoal( character_id=character_id, cycle_id=cycle.id, curiosity_question_id=curios.id, title="询问项目目标", description="主动询问", expected_information_gain=0.6, interruption_cost=0.3, risk_level="L2", status=subgoal_status, ) session.add(sub) await session.commit() return sub.id @pytest.mark.asyncio async def test_list_curiosities(client: AsyncClient) -> None: async with AsyncSessionLocal() as session: character = await get_active_character_model(session) await _seed(character.id) resp = await client.get(f"/api/v1/characters/{character.id}/curiosities") assert resp.status_code == 200 data = resp.json() assert data["total"] == 1 assert data["items"][0]["question"] == "用户为何频繁提到项目X" @pytest.mark.asyncio async def test_list_subgoals_with_status_filter(client: AsyncClient) -> None: async with AsyncSessionLocal() as session: character = await get_active_character_model(session) await _seed(character.id, "candidate") await _seed(character.id, "approved") resp = await client.get( f"/api/v1/characters/{character.id}/subgoals", params={"status": "candidate"} ) assert resp.status_code == 200 assert resp.json()["total"] == 1 @pytest.mark.asyncio async def test_approve_subgoal(client: AsyncClient) -> None: async with AsyncSessionLocal() as session: character = await get_active_character_model(session) sid = await _seed(character.id) resp = await client.post(f"/api/v1/subgoals/{sid}/approve") assert resp.status_code == 200 assert resp.json()["status"] == "approved" @pytest.mark.asyncio async def test_defer_subgoal(client: AsyncClient) -> None: async with AsyncSessionLocal() as session: character = await get_active_character_model(session) sid = await _seed(character.id) resp = await client.post(f"/api/v1/subgoals/{sid}/defer") assert resp.status_code == 200 assert resp.json()["status"] == "deferred" @pytest.mark.asyncio async def test_reject_subgoal(client: AsyncClient) -> None: async with AsyncSessionLocal() as session: character = await get_active_character_model(session) sid = await _seed(character.id) resp = await client.post(f"/api/v1/subgoals/{sid}/reject") assert resp.status_code == 200 assert resp.json()["status"] == "rejected" @pytest.mark.asyncio async def test_approve_subgoal_creates_goal(client: AsyncClient) -> None: """审批通过次生目标后应自动创建太一目标。""" from sqlalchemy import select from app.models.governance import Goal async with AsyncSessionLocal() as session: character = await get_active_character_model(session) sid = await _seed(character.id) resp = await client.post(f"/api/v1/subgoals/{sid}/approve") assert resp.status_code == 200 # 验证生成了 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 goal = matching[0] assert goal.level == "task" assert goal.type == "exploration" assert goal.status == "active"