"""默认人物模板测试。""" from collections.abc import AsyncGenerator import pytest from httpx import ASGITransport, AsyncClient from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.errors import AppError from app.db.init_data import seed_default_data from app.db.session import AsyncSessionLocal from app.main import app from app.models.character import Character from app.models.personality import PersonalityVersion from app.models.user import User from app.repositories import characters as character_repo from app.repositories import personality as personality_repo from app.repositories.json_utils import dumps from app.modules.character import ( MODEL_POLICY_CREATE_DEFAULTS, SYSTEM_TEMPLATE_OWNER_SCOPE, ensure_character_mutable, ) @pytest.fixture async def db_session() -> AsyncGenerator[AsyncSession, None]: async with AsyncSessionLocal() as session: yield session async def _seeded_admin_scope(db: AsyncSession) -> str: """获取种子数据中管理员用户的 owner_scope。""" result = await db.execute(select(User).where(User.is_admin.is_(True)).limit(1)) admin = result.scalar_one_or_none() assert admin is not None, "种子数据应已创建管理员用户" return admin.id @pytest.mark.asyncio async def test_seed_creates_system_template_and_user_copy(db_session: AsyncSession) -> None: db = db_session template = await character_repo.get_system_template_by_key(db, "default") assert template is not None assert template.is_system_template is True assert template.owner_scope == "system" assert template.template_key == "default" admin_scope = await _seeded_admin_scope(db) character = await character_repo.get_active_character(db, admin_scope) assert character is not None assert character.is_system_template is False assert character.source_template_id == template.id assert character.id != template.id @pytest.mark.asyncio async def test_active_character_api_returns_user_copy() -> None: transport = ASGITransport(app=app) async with AsyncSessionLocal() as session: admin_scope = await _seeded_admin_scope(session) from app.services.auth import create_access_token token = create_access_token(subject=admin_scope, extra={"owner_scope": admin_scope}) async with AsyncClient( transport=transport, base_url="http://test", headers={"Authorization": f"Bearer {token}"}, ) as client: response = await client.get("/api/v1/characters/active") assert response.status_code == 200 body = response.json() assert body["is_system_template"] is False assert body["source_template_id"] assert not body["owner_scope"].startswith("local-user") @pytest.mark.asyncio async def test_personality_is_copied_from_template(db_session: AsyncSession) -> None: db = db_session template = await character_repo.get_system_template_by_key(db, "default") admin_scope = await _seeded_admin_scope(db) character = await character_repo.get_active_character(db, admin_scope) assert template is not None assert character is not None assert template.current_personality_version_id is not None assert character.current_personality_version_id is not None assert template.current_personality_version_id != character.current_personality_version_id template_personality = await personality_repo.get_personality( db, template.current_personality_version_id ) user_personality = await personality_repo.get_personality( db, character.current_personality_version_id ) assert template_personality is not None assert user_personality is not None assert template_personality.character_id == template.id assert user_personality.character_id == character.id assert user_personality.reason == "system_template_copy" from app.services.auth import create_access_token token = create_access_token(subject=admin_scope, extra={"owner_scope": admin_scope}) transport = ASGITransport(app=app) async with AsyncClient( transport=transport, base_url="http://test", headers={"Authorization": f"Bearer {token}"}, ) as client: response = await client.get("/api/v1/personality/current") assert response.status_code == 200 assert response.json()["id"] == user_personality.id @pytest.mark.asyncio async def test_seed_default_data_is_idempotent(db_session: AsyncSession) -> None: db = db_session await seed_default_data(db) await seed_default_data(db) # 种子数据幂等:只应有一名管理员用户 result = await db.execute(select(User).where(User.is_admin.is_(True))) admins = result.scalars().all() assert len(admins) == 1 admin_scope = admins[0].id template_count = await db.scalar( select(func.count()).select_from(Character).where(Character.is_system_template.is_(True)) ) user_count = await db.scalar( select(func.count()) .select_from(Character) .where( Character.owner_scope == admin_scope, Character.is_system_template.is_(False), ) ) personality_count = await db.scalar(select(func.count()).select_from(PersonalityVersion)) assert template_count == 5 # is_default=true 模板只有一个(default),所以用户角色数为 1 assert user_count == 1 # 人格版本:每个模板 + 每个角色的复制 assert personality_count >= 7 @pytest.mark.asyncio async def test_seed_does_not_overwrite_existing_user_character(db_session: AsyncSession) -> None: db = db_session admin_scope = await _seeded_admin_scope(db) character = await character_repo.get_active_character(db, admin_scope) assert character is not None character.profile_json = dumps({"background": "用户已自定义"}) character.model_policy_json = dumps({"default_chat_model_hint": "custom-model"}) await db.commit() await seed_default_data(db) await db.refresh(character) assert character.profile_json == dumps({"background": "用户已自定义"}) assert character.model_policy_json == dumps({"default_chat_model_hint": "custom-model"}) @pytest.mark.asyncio async def test_system_template_is_not_mutable(db_session: AsyncSession) -> None: db = db_session template = await character_repo.get_system_template_by_key(db, "default") admin_scope = await _seeded_admin_scope(db) character = await character_repo.get_active_character(db, admin_scope) assert template is not None assert character is not None with pytest.raises(ValueError, match="系统人物模板不可修改"): ensure_character_mutable(template.is_system_template) ensure_character_mutable(character.is_system_template)