✨ feat: WP-02 角色域——完整生命周期、铸魂、内驱力与随缘
- 迁移 6:character(生命周期六态检查约束、起源、现实人物治理标记、道体基线、内驱力)+ character_version(baseline/first_statement/drive_revision/reforging/persona_candidate 五类不可变版本与审核态) - 创建:手动与随缘(确定性词库组合,同种子同候选、一次一个——不是抽卡);现实人物治理(已故拒绝、未声明授权拒绝) - 启镜(draft→active + 首个 baseline 版本)与初照自陈(候选→确认归真并入道体) - 生命周期端点:静修/归藏/复照(归档可恢复、复照不抹历史)+ 死亡两步确认(7 天冷静期可撤销、未满确认 422);非法状态迁移 422 - 铸魂(显式 confirm 强确认,直接更新道体)与人格候选(pending→审核批准才并入道体,不静默覆盖) - 内驱力编辑:写当前值并落 drive_revision 版本(可追溯,ADR-018) - 测试 52 项全绿(+8):状态机全路径、随缘确定性、现实人物三例、铸魂确认、候选审核与重复审核 409、跨域 fail-closed
This commit is contained in:
1 parent
603a683c14
commit
40221170ae
7 files changed
+859
No files matched your search
@@ -459,3 +459,151 @@ class TestRevisions:
|
||||
f"/v1/messages/{mid}/revisions", json={"content": [{"type": "text", "text": "篡改"}]}
|
||||
)
|
||||
assert rev.status_code in (401, 403, 404)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("client")
|
||||
class TestCharacters:
|
||||
async def _create(self, client, headers, body) -> dict:
|
||||
resp = await client.post("/v1/characters", json=body, headers=headers)
|
||||
assert resp.status_code == 201, resp.text
|
||||
return resp.json()
|
||||
|
||||
async def test_manual_create_activate_and_first_statement(self, client: AsyncClient) -> None:
|
||||
headers = await login(client, "char1@example.com")
|
||||
c = await self._create(
|
||||
client, headers, {"name": "昭星", "identity_baseline": {"core_traits": ["沉静"]}}
|
||||
)
|
||||
assert c["status"] == "draft" and c["origin"] == "original"
|
||||
act = await client.post(f"/v1/characters/{c['id']}/activations", headers=headers)
|
||||
assert act.status_code == 201 and act.json()["status"] == "active"
|
||||
assert act.json()["current_version_id"] is not None
|
||||
fs = await client.post(
|
||||
f"/v1/characters/{c['id']}/first-statement",
|
||||
json={"text": "初次见面,我是昭星。"},
|
||||
headers=headers,
|
||||
)
|
||||
assert fs.status_code == 201 and fs.json()["status"] == "pending"
|
||||
confirm = await client.post(
|
||||
f"/v1/characters/{c['id']}/first-statement/confirmations",
|
||||
json={"version_id": fs.json()["id"]},
|
||||
headers=headers,
|
||||
)
|
||||
assert confirm.status_code == 201 and confirm.json()["status"] == "approved"
|
||||
detail = await client.get(f"/v1/characters/{c['id']}", headers=headers)
|
||||
assert detail.json()["identity_baseline"]["first_statement"] == "初次见面,我是昭星。"
|
||||
|
||||
async def test_serendipity_is_deterministic_single(self, client: AsyncClient) -> None:
|
||||
"""随缘:同种子同候选(可复现),一次一个,不是抽卡。"""
|
||||
headers = await login(client, "char2@example.com")
|
||||
a = await self._create(client, headers, {"origin": "serendipity", "seed": 3})
|
||||
b = await self._create(client, headers, {"origin": "serendipity", "seed": 3})
|
||||
assert a["name"] == b["name"] and a["origin"] == "serendipity"
|
||||
assert a["drives"] and a["drives"][0]["name"]
|
||||
|
||||
async def test_real_person_governance(self, client: AsyncClient) -> None:
|
||||
headers = await login(client, "char3@example.com")
|
||||
# 已故现实人物:拒绝
|
||||
dead = await client.post(
|
||||
"/v1/characters",
|
||||
json={"name": "某人", "based_on_real_person": True, "deceased_real_person": True,
|
||||
"real_person_attested": True},
|
||||
headers=headers,
|
||||
)
|
||||
assert dead.status_code == 422
|
||||
# 未声明授权:拒绝
|
||||
no_attest = await client.post(
|
||||
"/v1/characters", json={"name": "某人", "based_on_real_person": True}, headers=headers
|
||||
)
|
||||
assert no_attest.status_code == 422
|
||||
# 本人声明:允许
|
||||
ok = await self._create(
|
||||
client, headers,
|
||||
{"name": "我自己", "based_on_real_person": True, "real_person_attested": True},
|
||||
)
|
||||
assert ok["based_on_real_person"] is True
|
||||
|
||||
async def test_lifecycle_full_path(self, client: AsyncClient) -> None:
|
||||
headers = await login(client, "char4@example.com")
|
||||
c = await self._create(client, headers, {"name": "汀棠"})
|
||||
await client.post(f"/v1/characters/{c['id']}/activations", headers=headers)
|
||||
# 静修 → 归藏 → 复照
|
||||
rest = await client.post(f"/v1/characters/{c['id']}/rests", headers=headers)
|
||||
assert rest.json()["status"] == "resting"
|
||||
arch = await client.post(f"/v1/characters/{c['id']}/archives", headers=headers)
|
||||
assert arch.json()["status"] == "archived"
|
||||
revive = await client.post(f"/v1/characters/{c['id']}/revivals", headers=headers)
|
||||
assert revive.json()["status"] == "active"
|
||||
# 死亡:请求 → 撤销(复照)→ 再请求 → 未满冷静期确认被拒
|
||||
req = await client.post(f"/v1/characters/{c['id']}/death-requests", headers=headers)
|
||||
assert req.json()["status"] == "death_pending"
|
||||
cancel = await client.post(f"/v1/characters/{c['id']}/death-cancellations", headers=headers)
|
||||
assert cancel.json()["status"] == "active"
|
||||
await client.post(f"/v1/characters/{c['id']}/death-requests", headers=headers)
|
||||
early = await client.post(f"/v1/characters/{c['id']}/death-confirmations", headers=headers)
|
||||
assert early.status_code == 422 # 冷静期未满
|
||||
|
||||
async def test_invalid_transition_rejected(self, client: AsyncClient) -> None:
|
||||
headers = await login(client, "char5@example.com")
|
||||
c = await self._create(client, headers, {"name": "砚谷"})
|
||||
# 灵胚不能直接归藏
|
||||
resp = await client.post(f"/v1/characters/{c['id']}/archives", headers=headers)
|
||||
assert resp.status_code == 422
|
||||
|
||||
async def test_reforging_requires_confirm(self, client: AsyncClient) -> None:
|
||||
headers = await login(client, "char6@example.com")
|
||||
c = await self._create(client, headers, {"name": "朔川"})
|
||||
await client.post(f"/v1/characters/{c['id']}/activations", headers=headers)
|
||||
no = await client.post(
|
||||
f"/v1/characters/{c['id']}/reforgings",
|
||||
json={"identity_baseline": {"core_traits": ["焕新"]}},
|
||||
headers=headers,
|
||||
)
|
||||
assert no.status_code == 422
|
||||
yes = await client.post(
|
||||
f"/v1/characters/{c['id']}/reforgings",
|
||||
json={"confirm": True, "identity_baseline": {"core_traits": ["焕新"]}},
|
||||
headers=headers,
|
||||
)
|
||||
assert yes.status_code == 201 and yes.json()["kind"] == "reforging"
|
||||
detail = await client.get(f"/v1/characters/{c['id']}", headers=headers)
|
||||
assert detail.json()["identity_baseline"]["core_traits"] == ["焕新"]
|
||||
|
||||
async def test_drives_update_and_persona_review(self, client: AsyncClient) -> None:
|
||||
headers = await login(client, "char7@example.com")
|
||||
c = await self._create(client, headers, {"name": "霁灯"})
|
||||
await client.post(f"/v1/characters/{c['id']}/activations", headers=headers)
|
||||
upd = await client.patch(
|
||||
f"/v1/characters/{c['id']}/drives",
|
||||
json={"drives": [{"name": "求知", "description": "想弄明白一切", "intensity": 3}]},
|
||||
headers=headers,
|
||||
)
|
||||
assert upd.status_code == 200 and upd.json()["drives"][0]["name"] == "求知"
|
||||
# 人格候选:pending → 批准后并入道体
|
||||
cand = await client.post(
|
||||
f"/v1/characters/{c['id']}/persona-candidates",
|
||||
json={"identity_baseline": {"core_traits": ["温柔"]}},
|
||||
headers=headers,
|
||||
)
|
||||
assert cand.status_code == 201 and cand.json()["status"] == "pending"
|
||||
approve = await client.post(
|
||||
f"/v1/characters/{c['id']}/versions/{cand.json()['id']}/decisions",
|
||||
json={"approve": True},
|
||||
headers=headers,
|
||||
)
|
||||
assert approve.json()["status"] == "approved"
|
||||
detail = await client.get(f"/v1/characters/{c['id']}", headers=headers)
|
||||
assert detail.json()["identity_baseline"]["core_traits"] == ["温柔"]
|
||||
# 已审核版本重复审核:409
|
||||
again = await client.post(
|
||||
f"/v1/characters/{c['id']}/versions/{cand.json()['id']}/decisions",
|
||||
json={"approve": False},
|
||||
headers=headers,
|
||||
)
|
||||
assert again.status_code == 409
|
||||
|
||||
async def test_cross_domain_character_404(self, client: AsyncClient) -> None:
|
||||
alice = await login(client, "char8@example.com")
|
||||
c = await self._create(client, alice, {"name": "澜川"})
|
||||
await login(client, "char8-evil@example.com")
|
||||
resp = await client.get(f"/v1/characters/{c['id']}")
|
||||
assert resp.status_code in (401, 403, 404)
|
||||
Reference in new issue
Block a user