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

180 lines
6.7 KiB
Python

"""默认人物模板测试。"""
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)