"""大周天候选生成 + 半自动应用调度测试。""" import pytest from app.application.big_cycle import drain_pending_big_cycles, maybe_trigger_big_cycle from app.core.config import settings from app.db.session import AsyncSessionLocal from app.models.memory import Memory from app.repositories import big_cycle as big_cycle_repo from app.repositories import evolution as evolution_repo from app.services.characters import get_active_character_model _MIXED_RESPONSE = ( "[" '{"type":"personality","level":"L4","scope":"character_scope",' '"title":"身份演化","proposal":"调整身份表达",' '"delta":{"identity_profile":"新的身份描述"},' '"confidence":0.7,"benefit_score":0.6,"risk_score":0.4,' '"auto_apply_allowed":false,"evidence":[0]},' '{"type":"procedural","level":"L2","scope":"task_scope",' '"title":"确认假设","proposal":"技术问题先确认前提",' '"delta":{"title":"确认假设","content":"先确认前提再作答"},' '"confidence":0.8,"benefit_score":0.7,"risk_score":0.2,' '"auto_apply_allowed":true,"evidence":[0]}' "]" ) 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_evolution_chat(monkeypatch: pytest.MonkeyPatch) -> None: async def fake_chat(*_args, **_kwargs): return _MIXED_RESPONSE monkeypatch.setattr("app.application.big_cycle._call_model", fake_chat) @pytest.mark.asyncio async def test_big_cycle_generates_candidates_and_auto_applies( mock_evolution_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: cycle = await big_cycle_repo.get_cycle(session, cycle_id) candidates, total = await evolution_repo.list_candidates(session, character.id) assert cycle is not None assert cycle.status == "completed" assert cycle.evaluation_snapshot_json is not None assert total == 2 personality = next(c for c in candidates if c.type == "personality") assert personality.status == "pending" # L4 auto_apply_allowed=false assert personality.level == "L4" assert personality.source == "big_cycle" assert personality.cycle_id == cycle_id procedural = next(c for c in candidates if c.type == "procedural") assert procedural.status == "applied" # L2 auto_apply_allowed=true assert procedural.resulting_target_id is not None @pytest.mark.asyncio async def test_auto_apply_degrades_when_applier_missing( monkeypatch: pytest.MonkeyPatch, ) -> None: response = ( '[{"type":"tool_strategy","level":"L2","scope":"task_scope",' '"title":"工具策略","proposal":"偏好shell",' '"delta":{},"auto_apply_allowed":true}]' ) async def fake_chat(*_args, **_kwargs): return response monkeypatch.setattr("app.application.big_cycle._call_model", fake_chat) async with AsyncSessionLocal() as session: character = await get_active_character_model(session) await _seed_memories(character.id, 20) await maybe_trigger_big_cycle(character.id, trigger="test") await drain_pending_big_cycles() async with AsyncSessionLocal() as session: candidates, _ = await evolution_repo.list_candidates(session, character.id) assert len(candidates) == 1 cand = candidates[0] assert cand.status == "pending" # applier 缺失,降级不自动应用 assert cand.auto_apply_allowed is True @pytest.mark.asyncio async def test_big_cycle_uses_configured_model_hint( monkeypatch: pytest.MonkeyPatch, ) -> None: captured: dict = {} result = type("FakeResponse", (), {"content": "[]"})() class _FakeManager: def to_message_list(self, *_args: object, **_kwargs: object) -> list: return [] async def chat(self, messages: object, model_hint: str | None = None) -> object: # noqa: ARG002 captured["model_hint"] = model_hint return result def fake_get_provider_manager() -> _FakeManager: return _FakeManager() monkeypatch.setattr( "app.application.big_cycle._get_provider_manager", fake_get_provider_manager ) async with AsyncSessionLocal() as session: character = await get_active_character_model(session) await _seed_memories(character.id, 20) await maybe_trigger_big_cycle(character.id, trigger="test") await drain_pending_big_cycles() assert captured["model_hint"] == settings.big_cycle_model