Files
hunjing/backend/tests/api/test_characters.py
T
chenxin 7e23d6fa84 M6:缘契、人格与世界——契约版本链、铸魂分灵、河图境界与三界分流
- 缘契域 domains/yuanqi:covenants(当前版)+ covenant_events(不可变历史);提议—问心—铭契—撤回全链;边界变更立即生效;多维关系并存,服务层拒绝 affinity/好感度单一键;浪漫维度成人资格标记位
- 道体铸魂与分灵 domains/jingsheng/soul.py:soul_forgings 确认流(锚点变更 pending→confirmed 应用并签发新魂籍版本,确认前人物锚点不变);分灵创建新人物(继承资料与谱系 lineage 链、不继承原身份,魂籍从 v1 起)
- 河图域 domains/hetu:persistent_worlds 境界 + world_entities/world_relations 境界作用域重建 + 世谱挂接;境界正史视图(scope=persistent_world 归真集合);canonize 后置推进正史计数
- 三界分流:reality→息壤提取(不变);persistent_world→境界实录+正史推进;simulation→只记试演实录;factual_records 增 scope 列——境界/幻境实录与真界本相隔离
- 正影打通:引文 target_type=memory 时被引记忆 superseded → 返回 successor_memory_id 提示,不静默替换
- 前端:人物详情页缘契面板(提议维度选择/铭契/撤回/事件史)+ 世界页重建(境界目录/实体/正史概览)+ covenants/worlds api 库
- 退役:旧 personality/drive/evolution/world 模块、服务、端点、仓储、schema、前端五页与组件样式;迁移 007 退役九张旧表与 characters.current_personality_version_id
- 验收(test_m6_domains 六用例):多维无单一好感度/铭契版本链/事件不可抹除/撤回立即生效/边界即时/铸魂确认流+魂籍版本/分灵谱系/境界正史不进真界(实录 scope 隔离+真界候选=0)/引文正影后继
- 四套件:后端 379、前端 199、BFF 4、神识 5 包全绿(2 个预存环境失败不变);迁移 007 净库+±1 干净
2026-08-27 20:10:11 +08:00

154 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""人物管理 API 测试(api-spec §6)。
注意:所有需要用户认证的端点使用 ``auth_headers`` 注入 Bearer token。
"""
from collections.abc import AsyncGenerator
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
@pytest.fixture
async def client(auth_headers: dict[str, str]) -> AsyncGenerator[AsyncClient, None]:
transport = ASGITransport(app=app)
async with AsyncClient(
transport=transport,
base_url="http://test",
headers=auth_headers,
) as http_client:
yield http_client
@pytest.mark.asyncio
async def test_list_returns_seeded_default(client: AsyncClient) -> None:
resp = await client.get("/api/v1/characters")
assert resp.status_code == 200
body = resp.json()
assert body["total"] >= 1
item = body["items"][0]
assert item["id"].startswith("character_")
assert item["status"] == "active"
assert item["personality_version"] == 0 # 旧人格版本表退役,版本语义由魂籍承载
assert item["memory_count"] == 0
@pytest.mark.asyncio
async def test_create_character_defaults_to_wuji(client: AsyncClient) -> None:
resp = await client.post("/api/v1/characters", json={"name": "新人物"})
assert resp.status_code == 200
body = resp.json()
assert body["status"] == "wuji"
assert body["name"] == "新人物"
assert body["owner_scope"] is not None
assert body["is_system_template"] is False
assert body["model_policy"]["default_chat_model_hint"]
listed = await client.get("/api/v1/characters")
assert listed.json()["total"] >= 2
@pytest.mark.asyncio
async def test_create_character_rejects_empty_name(client: AsyncClient) -> None:
resp = await client.post("/api/v1/characters", json={"name": ""})
assert resp.status_code == 422
@pytest.mark.asyncio
async def test_list_filter_by_status(client: AsyncClient) -> None:
await client.post("/api/v1/characters", json={"name": "空白人物"})
resp = await client.get("/api/v1/characters?status=wuji")
assert resp.status_code == 200
body = resp.json()
assert body["total"] == 1
assert body["items"][0]["status"] == "wuji"
@pytest.mark.asyncio
async def test_get_character_and_not_found(client: AsyncClient) -> None:
active = await client.get("/api/v1/characters/active")
character_id = active.json()["id"]
resp = await client.get(f"/api/v1/characters/{character_id}")
assert resp.status_code == 200
assert resp.json()["id"] == character_id
missing = await client.get("/api/v1/characters/character_does_not_exist")
assert missing.status_code == 404
@pytest.mark.asyncio
async def test_update_profile_and_model_policy(client: AsyncClient) -> None:
character_id = (await client.get("/api/v1/characters/active")).json()["id"]
profile = await client.patch(
f"/api/v1/characters/{character_id}/profile",
json={"display_name": "昵称", "background": "背景故事"},
)
assert profile.status_code == 200
assert profile.json()["display_name"] == "昵称"
assert profile.json()["profile"]["background"] == "背景故事"
policy = await client.patch(
f"/api/v1/characters/{character_id}/model-policy",
json={"privacy_level": "sensitive", "default_chat_model_hint": "custom-chat"},
)
assert policy.status_code == 200
body = policy.json()
assert body["privacy_level"] == "sensitive"
assert body["model_policy"]["privacy_level"] == "sensitive"
assert body["model_policy"]["default_chat_model_hint"] == "custom-chat"
@pytest.mark.asyncio
async def test_activate_switches_active_character(client: AsyncClient) -> None:
char_a = (await client.get("/api/v1/characters/active")).json()["id"]
char_b = (await client.post("/api/v1/characters", json={"name": "B"})).json()["id"]
resp = await client.post(f"/api/v1/characters/{char_b}/activate")
assert resp.status_code == 200
assert resp.json() == {"active_character_id": char_b, "status": "active"}
# A 被切到 paused(A1 互斥)
a_after = await client.get(f"/api/v1/characters/{char_a}")
assert a_after.json()["status"] == "paused"
# 当前激活指向 B
assert (await client.get("/api/v1/characters/active")).json()["id"] == char_b
@pytest.mark.asyncio
async def test_pause_resume_archive(client: AsyncClient) -> None:
char_b = (await client.post("/api/v1/characters", json={"name": "B"})).json()["id"]
await client.post(f"/api/v1/characters/{char_b}/activate")
paused = await client.post(f"/api/v1/characters/{char_b}/pause")
assert paused.status_code == 200
assert paused.json()["status"] == "paused"
# 暂停当前唯一激活人物后无 active
no_active = await client.get("/api/v1/characters/active")
assert no_active.status_code == 404
resumed = await client.post(f"/api/v1/characters/{char_b}/resume")
assert resumed.status_code == 200
assert resumed.json()["status"] == "active"
archived = await client.post(f"/api/v1/characters/{char_b}/archive")
assert archived.status_code == 200
assert archived.json()["status"] == "archived"
# 归档人物不可直接激活
again = await client.post(f"/api/v1/characters/{char_b}/activate")
assert again.status_code == 409
@pytest.mark.asyncio
async def test_pause_rejects_non_active(client: AsyncClient) -> None:
char_b = (await client.post("/api/v1/characters", json={"name": "B"})).json()["id"]
# wuji 状态不可暂停
resp = await client.post(f"/api/v1/characters/{char_b}/pause")
assert resp.status_code == 409