"""资料导入候选审核测试(5c)。""" from collections.abc import AsyncGenerator import pytest from httpx import ASGITransport, AsyncClient from app.application.imports import drain_pending_extractions from app.main import app async def _get_character_id(client: AsyncClient) -> str: """通过已认证的 API 获取当前用户活跃角色 ID。""" resp = await client.get("/api/v1/characters/active") assert resp.status_code == 200 return resp.json()["id"] class _FakeChatResult: def __init__(self, content: str) -> None: self.content = content _PROFILE_RESPONSE = ( '[{"field":"background","value":"来自北京,从事设计工作",' '"confidence":0.9,"reason":"资料提及","evidence":[0]}]' ) _MEMORY_RESPONSE = ( '[{"type":"preference","title":"喜欢简洁回答","content":"用户偏好简洁直接的回答",' '"importance":0.8,"sensitivity":"low","reason":"多次提及","evidence":[0]}]' ) @pytest.fixture async def client( monkeypatch: pytest.MonkeyPatch, auth_headers: dict[str, str] ) -> AsyncGenerator[AsyncClient, None]: async def fake_chat(*_args, model_hint, **_kwargs): if model_hint == "memory-extraction": return _FakeChatResult(_MEMORY_RESPONSE) return _FakeChatResult(_PROFILE_RESPONSE) class _MockMessage: def __init__(self, content: str): self.content = content class _MockManager: async def chat(self, _messages, model_hint=None): return await fake_chat(None, model_hint=model_hint) def to_message_list(self, system, _messages): return [_MockMessage(s) for s in system] monkeypatch.setattr("app.application.imports._get_provider_manager", lambda: _MockManager()) transport = ASGITransport(app=app) async with AsyncClient( transport=transport, base_url="http://test", headers=auth_headers ) as http_client: yield http_client async def _setup(client: AsyncClient, kind: str) -> tuple[str, str, str]: character_id = await _get_character_id(client) import_id = ( await client.post( f"/api/v1/characters/{character_id}/imports", json={"material_type": "background_story", "content": "我叫小魂,来自北京。"}, ) ).json()["id"] await client.post(f"/api/v1/characters/{character_id}/imports/{import_id}/{kind}") await drain_pending_extractions() candidates = ( await client.get(f"/api/v1/characters/{character_id}/imports/{import_id}/{kind}") ).json() return character_id, import_id, candidates[0]["id"] @pytest.mark.asyncio async def test_batch_review_profile_approve_writes_profile(client: AsyncClient) -> None: character_id, _import_id, candidate_id = await _setup(client, "profile-candidates") result = await client.post( f"/api/v1/characters/{character_id}/profile-candidates/batch-review", json={"decisions": [{"candidate_id": candidate_id, "decision": "approve"}]}, ) assert result.status_code == 200 assert result.json()["applied"] == 1 character = (await client.get(f"/api/v1/characters/{character_id}")).json() assert character["profile"]["background"] == "来自北京,从事设计工作" @pytest.mark.asyncio async def test_batch_review_memory_approve_creates_memory(client: AsyncClient) -> None: character_id, _import_id, candidate_id = await _setup(client, "memory-candidates") result = await client.post( f"/api/v1/characters/{character_id}/memory-candidates/batch-review", json={"decisions": [{"candidate_id": candidate_id, "decision": "approve"}]}, ) assert result.json()["applied"] == 1 memories = (await client.get("/api/v1/memory")).json() assert memories["total"] >= 1 assert any(m["title"] == "喜欢简洁回答" for m in memories["items"]) @pytest.mark.asyncio async def test_batch_review_reject_marks_candidate(client: AsyncClient) -> None: character_id, import_id, candidate_id = await _setup(client, "profile-candidates") result = await client.post( f"/api/v1/characters/{character_id}/profile-candidates/batch-review", json={"decisions": [{"candidate_id": candidate_id, "decision": "reject"}]}, ) assert result.json()["rejected"] == 1 rejected = ( await client.get( f"/api/v1/characters/{character_id}/imports/{import_id}/profile-candidates", params={"status": "rejected"}, ) ).json() assert any(c["id"] == candidate_id for c in rejected) @pytest.mark.asyncio async def test_batch_review_skips_non_pending(client: AsyncClient) -> None: character_id, _import_id, candidate_id = await _setup(client, "profile-candidates") # 先 approve await client.post( f"/api/v1/characters/{character_id}/profile-candidates/batch-review", json={"decisions": [{"candidate_id": candidate_id, "decision": "approve"}]}, ) # 再审同一候选(已 applied)→ skipped result = await client.post( f"/api/v1/characters/{character_id}/profile-candidates/batch-review", json={"decisions": [{"candidate_id": candidate_id, "decision": "approve"}]}, ) assert result.json()["skipped"] == 1 assert result.json()["applied"] == 0