716 lines
29 KiB
Python
716 lines
29 KiB
Python
"""真实元数据、正文、B02与S01共同验证事实来源和时点,不调用模型。"""
|
||
|
||
from dataclasses import replace
|
||
|
||
import pytest
|
||
from pydantic import TypeAdapter
|
||
|
||
from muse.作品规划.接口 import 档案保存
|
||
from muse.元数据.接口 import 元数据服务, 导入内置结构
|
||
from muse.共享.调用身份 import 内容用途, 用途, 调用身份
|
||
from muse.共享.错误 import Muse错误
|
||
from muse.启动 import 构建
|
||
from muse.故事世界.接口 import 事实定位, 事实提案请求, 定位字面引文, 正文依据
|
||
from muse.正式变更.接口 import 作者动作
|
||
from muse.正文写作.接口 import 文本节点, 正文草稿, 段落
|
||
from muse.配置 import 应用配置
|
||
|
||
pytestmark = pytest.mark.数据库
|
||
|
||
|
||
@pytest.fixture
|
||
def 故事环境(应用测试库):
|
||
with 应用测试库[用途.维护].连接() as 连, 连.transaction():
|
||
导入内置结构(元数据服务(连))
|
||
装配 = 构建(应用配置(应用测试库[用途.生产].引用, "synthetic"))
|
||
作者 = 调用身份("author", None, 用途.生产, 内容用途.规划)
|
||
作品 = 装配.要求作品()
|
||
结构 = 作品.新档案结构(作者, "story-work", "work_core", 1)
|
||
作品.保存档案(作者, "create-story", 档案保存("story-work", 0, {"名称": "合成故事世界"}, 结构))
|
||
for i in range(1, 4):
|
||
作品.添加章节(
|
||
作者, f"chapter-{i}", "story-work", f"chapter-{i}", f"第{i}幕", 预期目录版本=i - 1
|
||
)
|
||
正文 = 装配.要求正文()
|
||
for i, text in [
|
||
(1, "林深停在码头,何岚站在船旁。"),
|
||
(2, "何岚知道林深已经登船。"),
|
||
(3, "林深来到北岸。"),
|
||
]:
|
||
正文.保存人工(
|
||
作者, f"body-{i}", f"chapter-{i}", 0, 正文草稿((段落(f"p{i}", (文本节点(text),)),))
|
||
)
|
||
return 装配, 作者, 应用测试库
|
||
|
||
|
||
def 来源(装配, 作者, chapter, quote):
|
||
记录 = 装配.要求正文().读取正文(作者, chapter)
|
||
原文 = TypeAdapter(正文草稿).validate_python(记录["document"])
|
||
return 正文依据(
|
||
chapter, "main", 记录["revision"], 记录["document_hash"], (定位字面引文(原文, quote),)
|
||
)
|
||
|
||
|
||
def 提案(
|
||
装配,
|
||
作者,
|
||
object_id,
|
||
*,
|
||
sources=(),
|
||
independent=False,
|
||
effective="chapter-1",
|
||
mode="fact",
|
||
known_to=(),
|
||
):
|
||
世界 = 装配.要求故事世界()
|
||
结构 = 世界.新事实结构(作者, "story-work", object_id, "character", 1)
|
||
请求 = 事实提案请求(
|
||
"story-work",
|
||
object_id,
|
||
0,
|
||
结构,
|
||
{"名称": object_id},
|
||
tuple(sources),
|
||
effective,
|
||
independent=independent,
|
||
knowledge_mode=mode,
|
||
known_to=tuple({"actor_id": id_, "from_chapter_id": effective} for id_ in known_to),
|
||
)
|
||
回执 = 世界.提出事实(作者, "propose-" + object_id, 请求)
|
||
记录 = 世界.读取提案(作者, 回执["results"][0]["proposal_id"])
|
||
return 记录, 事实定位(
|
||
记录["proposal_id"], 记录["revision"], 记录["proposal_hash"], "world:" + object_id, 0
|
||
)
|
||
|
||
|
||
def 确认(装配, 作者, 定位, command):
|
||
世界 = 装配.要求故事世界()
|
||
审阅 = 世界.打开事实审阅(作者, 定位)
|
||
return 世界.决定事实(
|
||
作者, command, 定位, 作者动作.采纳, 审阅["review_id"], 审阅["review_hash"], ("object",)
|
||
)
|
||
|
||
|
||
def test_事实确认与来源失效保留独立设定__ac1001(故事环境):
|
||
装配, 作者, _ = 故事环境
|
||
源 = 来源(装配, 作者, "chapter-1", "林深停在码头")
|
||
_, 位置 = 提案(装配, 作者, "person-dependent", sources=(源,))
|
||
assert 装配.要求故事世界().查询世界(作者, "story-work", "chapter-1")["facts"] == []
|
||
回执 = 确认(装配, 作者, 位置, "confirm-dependent")
|
||
assert 回执["results"][0]["revision"] == 1
|
||
_, 独立 = 提案(装配, 作者, "person-independent", independent=True)
|
||
确认(装配, 作者, 独立, "confirm-independent")
|
||
初 = 装配.要求故事世界().查询世界(作者, "story-work", "chapter-1")
|
||
assert {v["object_id"] for v in 初["facts"]} == {"person-dependent", "person-independent"}
|
||
装配.要求正文().保存人工(
|
||
作者,
|
||
"changed-source",
|
||
"chapter-1",
|
||
1,
|
||
正文草稿((段落("p1", (文本节点("码头上没有人。"),)),)),
|
||
)
|
||
后 = 装配.要求故事世界().查询世界(作者, "story-work", "chapter-1")
|
||
assert {v["object_id"] for v in 后["facts"]} == {"person-independent"}
|
||
assert {v["object_id"]: v["reason"] for v in 后["omitted"]}[
|
||
"person-dependent"
|
||
] == "SOURCE_STALE"
|
||
|
||
|
||
def test_叙事时点与系统确认截止分开__ac1002(故事环境):
|
||
装配, 作者, _ = 故事环境
|
||
_, 一 = 提案(装配, 作者, "person-before", independent=True)
|
||
确认(装配, 作者, 一, "confirm-before")
|
||
切点 = 装配.要求故事世界().查询世界(作者, "story-work", "chapter-1")["system_revision"]
|
||
_, 后 = 提案(
|
||
装配,
|
||
作者,
|
||
"person-future",
|
||
sources=(来源(装配, 作者, "chapter-3", "林深来到北岸"),),
|
||
effective="chapter-3",
|
||
)
|
||
确认(装配, 作者, 后, "confirm-future")
|
||
assert {
|
||
v["object_id"]
|
||
for v in 装配.要求故事世界().查询世界(作者, "story-work", "chapter-1")["facts"]
|
||
} == {"person-before"}
|
||
assert {
|
||
v["object_id"]
|
||
for v in 装配.要求故事世界().查询世界(作者, "story-work", "chapter-3", 系统版本=切点)[
|
||
"facts"
|
||
]
|
||
} == {"person-before"}
|
||
assert {
|
||
v["object_id"]
|
||
for v in 装配.要求故事世界().查询世界(作者, "story-work", "chapter-3")["facts"]
|
||
} == {"person-before", "person-future"}
|
||
|
||
|
||
def test_后文披露早期事件和角色猜测分别查询__ac1003(故事环境):
|
||
装配, 作者, _ = 故事环境
|
||
_, 早期 = 提案(
|
||
装配,
|
||
作者,
|
||
"past-recalled",
|
||
sources=(来源(装配, 作者, "chapter-3", "林深来到北岸"),),
|
||
effective="chapter-1",
|
||
)
|
||
确认(装配, 作者, 早期, "confirm-recalled")
|
||
世界 = 装配.要求故事世界()
|
||
assert [v["object_id"] for v in 世界.查询世界(作者, "story-work", "chapter-1")["facts"]] == [
|
||
"past-recalled"
|
||
]
|
||
读者 = 世界.查询世界(作者, "story-work", "chapter-1", 视角="reader")
|
||
assert 读者["facts"] == [] and 读者["omitted"][0]["reason"] == "FUTURE_EVIDENCE"
|
||
assert len(世界.查询世界(作者, "story-work", "chapter-3", 视角="reader")["facts"]) == 1
|
||
_, 主体 = 提案(装配, 作者, "knower", independent=True)
|
||
确认(装配, 作者, 主体, "confirm-knower")
|
||
_, 猜测 = 提案(装配, 作者, "guess", independent=True, mode="belief", known_to=("knower",))
|
||
确认(装配, 作者, 猜测, "confirm-guess")
|
||
_, 计划 = 提案(装配, 作者, "future-plan", independent=True, mode="plan", known_to=("knower",))
|
||
确认(装配, 作者, 计划, "confirm-plan")
|
||
角色 = 世界.查询世界(作者, "story-work", "chapter-1", 视角="character", 知情主体ID="knower")
|
||
assert [x["object_id"] for x in 角色["beliefs"]] == ["guess"]
|
||
assert [x["object_id"] for x in 角色["plans"]] == ["future-plan"]
|
||
assert 角色["facts"] == []
|
||
|
||
|
||
def test_其他充分正文依据保持已确认事实有效__ac1004(故事环境):
|
||
装配, 作者, _ = 故事环境
|
||
一 = 来源(装配, 作者, "chapter-1", "林深停在码头")
|
||
二 = 来源(装配, 作者, "chapter-3", "林深来到北岸")
|
||
_, 定位 = 提案(装配, 作者, "two-sources", sources=(一, 二))
|
||
确认(装配, 作者, 定位, "confirm-two-sources")
|
||
装配.要求正文().保存人工(
|
||
作者,
|
||
"change-one-source",
|
||
"chapter-1",
|
||
1,
|
||
正文草稿((段落("p1", (文本节点("码头上没有人。"),)),)),
|
||
)
|
||
当前 = 装配.要求故事世界().查询世界(作者, "story-work", "chapter-3")
|
||
assert [v["object_id"] for v in 当前["facts"]] == ["two-sources"]
|
||
|
||
|
||
def test_源变后旧审阅不能确认但可以重新审阅拒绝__ac1005(故事环境):
|
||
装配, 作者, _ = 故事环境
|
||
世界 = 装配.要求故事世界()
|
||
_, 定位 = 提案(
|
||
装配, 作者, "stale-review", sources=(来源(装配, 作者, "chapter-1", "林深停在码头"),)
|
||
)
|
||
审阅 = 世界.打开事实审阅(作者, 定位)
|
||
装配.要求正文().保存人工(
|
||
作者,
|
||
"change-reviewed-source",
|
||
"chapter-1",
|
||
1,
|
||
正文草稿((段落("p1", (文本节点("没有人停在码头。"),)),)),
|
||
)
|
||
with pytest.raises(Muse错误) as 捕获:
|
||
世界.决定事实(
|
||
作者,
|
||
"stale-accept",
|
||
定位,
|
||
作者动作.采纳,
|
||
审阅["review_id"],
|
||
审阅["review_hash"],
|
||
("object",),
|
||
)
|
||
assert 捕获.value.错误码 == "SOURCE_STALE"
|
||
新审阅 = 世界.打开事实审阅(作者, 定位)
|
||
回执 = 世界.决定事实(
|
||
作者, "reject-stale", 定位, 作者动作.拒绝, 新审阅["review_id"], 新审阅["review_hash"], ()
|
||
)
|
||
assert (
|
||
回执["action"] == "reject" and 世界.查询世界(作者, "story-work", "chapter-1")["facts"] == []
|
||
)
|
||
|
||
|
||
def test_新动态类型与计划窗口贯通真实持久化__ac1006(故事环境):
|
||
from muse.元数据.接口 import 值规则, 字段定义, 类型定义, 结构定义
|
||
from muse.故事世界.接口 import 叙事窗口
|
||
|
||
装配, 作者, 库 = 故事环境
|
||
with 库[用途.维护].连接() as 连, 连.transaction():
|
||
元数据 = 元数据服务(连)
|
||
元数据.登记类型(类型定义("landing_point", "登船点", "world", "entity", ("entity",)))
|
||
结构 = 结构定义(
|
||
"landing_point",
|
||
"landing_point",
|
||
1,
|
||
(
|
||
字段定义("landing.name", "名称", "名称", 值规则("text"), 必填=True),
|
||
字段定义(
|
||
"landing.hints", "位置提示", "位置提示", 值规则("list", 元素=值规则("text"))
|
||
),
|
||
),
|
||
)
|
||
元数据.登记结构候选(结构)
|
||
元数据.发布结构("landing_point", 1, 结构.内容哈希)
|
||
世界 = 装配.要求故事世界()
|
||
选择 = 世界.新事实结构(作者, "story-work", "landing-one", "landing_point", 1)
|
||
请求 = 事实提案请求(
|
||
"story-work",
|
||
"landing-one",
|
||
0,
|
||
选择,
|
||
{"名称": "码头", "位置提示": ["船旁"]},
|
||
(来源(装配, 作者, "chapter-1", "林深停在码头,何岚站在船旁"),),
|
||
"chapter-1",
|
||
planned_window=叙事窗口("chapter-2", "chapter-3"),
|
||
)
|
||
with pytest.raises(Muse错误) as 捕获:
|
||
世界.提出事实(
|
||
作者, "bad-window", replace(请求, planned_window=叙事窗口("chapter-3", "chapter-1"))
|
||
)
|
||
assert 捕获.value.错误码 == "FACT_TIME_INVALID"
|
||
回执 = 世界.提出事实(作者, "dynamic-proposal", 请求)
|
||
原 = 世界.读取提案(作者, 回执["results"][0]["proposal_id"])
|
||
确认(
|
||
装配,
|
||
作者,
|
||
事实定位(原["proposal_id"], 1, 原["proposal_hash"], "world:landing-one", 0),
|
||
"confirm-dynamic",
|
||
)
|
||
实体 = 世界.查询世界(作者, "story-work", "chapter-1")["facts"][0]
|
||
assert 实体["type_id"] == "landing_point" and 实体["content"] == 请求.content
|
||
assert 实体["planned_window"] == {"from_chapter_id": "chapter-2", "to_chapter_id": "chapter-3"}
|
||
读者 = 世界.查询世界(作者, "story-work", "chapter-1", 视角="reader")["facts"][0]
|
||
assert "planned_window" not in 读者
|
||
|
||
|
||
def test_HTTP事实提案确认与查询使用真实作者会话__ac1007(故事环境, tmp_path):
|
||
from dataclasses import asdict
|
||
|
||
from fastapi.testclient import TestClient
|
||
|
||
from muse.接入.http.应用 import 创建应用
|
||
from muse.配置 import 服务配置
|
||
|
||
装配, 作者, 库 = 故事环境
|
||
口令 = tmp_path / "fact-author.txt"
|
||
口令.write_text("synthetic-fact-author")
|
||
口令.chmod(0o600)
|
||
配置 = 应用配置(
|
||
库[用途.生产].引用,
|
||
"synthetic",
|
||
HTTP=服务配置(
|
||
str(口令),
|
||
作者ID="author",
|
||
公开地址="http://testserver",
|
||
允许来源=("http://testserver",),
|
||
),
|
||
)
|
||
with TestClient(创建应用(配置), headers={"origin": "http://testserver"}) as client:
|
||
assert client.get("/api/v1/works/story-work/facts").status_code == 401
|
||
assert (
|
||
client.post("/api/v1/session", json={"password": "synthetic-fact-author"}).status_code
|
||
== 200
|
||
)
|
||
frame = client.get(
|
||
"/api/v1/works/story-work/facts/schema",
|
||
params={"object_id": "http-person", "schema_id": "character"},
|
||
)
|
||
assert frame.status_code == 200, frame.text
|
||
req = {
|
||
"work_id": "story-work",
|
||
"object_id": "http-person",
|
||
"expected_revision": 0,
|
||
"schema": frame.json(),
|
||
"content": {"名称": "林深"},
|
||
"sources": [asdict(来源(装配, 作者, "chapter-1", "林深停在码头"))],
|
||
"effective_from": "chapter-1",
|
||
}
|
||
creation = {"command_id": "http-fact", "proposal": req}
|
||
result = client.post("/api/v1/works/story-work/fact-proposals", json=creation)
|
||
assert result.status_code == 200, result.text
|
||
assert (
|
||
client.post("/api/v1/works/story-work/fact-proposals", json=creation).json()
|
||
== result.json()
|
||
)
|
||
pid = result.json()["results"][0]["proposal_id"]
|
||
item = client.get(f"/api/v1/fact-proposals/{pid}").json()
|
||
assert client.get("/api/v1/works/story-work/facts").json()["facts"] == []
|
||
review_req = {
|
||
"proposal_id": pid,
|
||
"proposal_revision": 1,
|
||
"proposal_hash": item["proposal_hash"],
|
||
"target_ref": "world:http-person",
|
||
"expected_revision": 0,
|
||
}
|
||
review = client.post(f"/api/v1/fact-proposals/{pid}/reviews", json=review_req)
|
||
assert review.status_code == 200, review.text
|
||
decision = {
|
||
**review_req,
|
||
"command_id": "http-fact-confirm",
|
||
"action": "adopt",
|
||
"author_review_id": review.json()["review_id"],
|
||
"review_hash": review.json()["review_hash"],
|
||
"approved_changes": ["object"],
|
||
}
|
||
assert (
|
||
client.post(
|
||
f"/api/v1/fact-proposals/{pid}/decisions", json={**decision, "decided_by": "author"}
|
||
).status_code
|
||
== 422
|
||
)
|
||
accepted = client.post(f"/api/v1/fact-proposals/{pid}/decisions", json=decision)
|
||
assert accepted.status_code == 200, accepted.text
|
||
assert (
|
||
client.post(f"/api/v1/fact-proposals/{pid}/decisions", json=decision).json()
|
||
== accepted.json()
|
||
)
|
||
facts = client.get(
|
||
"/api/v1/works/story-work/facts", params={"as_of": "chapter-1", "view": "reader"}
|
||
).json()
|
||
assert facts["facts"][0]["content"]["名称"] == "林深" and facts["facts"][0]["revision"] == 1
|
||
|
||
|
||
def 关系提案(装配, 作者, object_id="relation-main"):
|
||
世界 = 装配.要求故事世界()
|
||
_, 甲 = 提案(
|
||
装配, 作者, "person-left", sources=(来源(装配, 作者, "chapter-1", "林深停在码头"),)
|
||
)
|
||
确认(装配, 作者, 甲, "confirm-left")
|
||
_, 乙 = 提案(装配, 作者, "person-right", independent=True)
|
||
确认(装配, 作者, 乙, "confirm-right")
|
||
schema = 世界.新事实结构(
|
||
作者, "story-work", object_id, "character_relation", 1, 家族="relation"
|
||
)
|
||
req = 事实提案请求(
|
||
"story-work",
|
||
object_id,
|
||
0,
|
||
schema,
|
||
{"甲方": "person-left", "乙方": "person-right"},
|
||
(来源(装配, 作者, "chapter-2", "何岚知道林深已经登船"),),
|
||
"chapter-2",
|
||
instance_family="relation",
|
||
)
|
||
return req
|
||
|
||
|
||
def test_关系端点和正文引文必须真实__ac1008(故事环境):
|
||
装配, 作者, _ = 故事环境
|
||
世界 = 装配.要求故事世界()
|
||
req = 关系提案(装配, 作者)
|
||
with pytest.raises(Muse错误):
|
||
世界.提出事实(作者, "missing-endpoint", replace(req, content={"甲方": "person-left"}))
|
||
with pytest.raises(Muse错误) as 错:
|
||
世界.提出事实(
|
||
作者,
|
||
"unknown-endpoint",
|
||
replace(req, content={"甲方": "person-left", "乙方": "missing-person"}),
|
||
)
|
||
assert 错.value.错误码 == "FACT_REFERENCE_INVALID"
|
||
anchor = replace(
|
||
req.sources[0].evidence[0], quote="正文没有这条依据句", end=len("正文没有这条依据句")
|
||
)
|
||
with pytest.raises(Muse错误) as 错:
|
||
世界.提出事实(
|
||
作者, "fake-quote", replace(req, sources=(replace(req.sources[0], evidence=(anchor,)),))
|
||
)
|
||
assert 错.value.错误码 == "FACT_QUOTE_NOT_FOUND"
|
||
result = 世界.提出事实(作者, "real-relation", req)
|
||
item = 世界.读取提案(作者, result["results"][0]["proposal_id"])
|
||
确认(
|
||
装配,
|
||
作者,
|
||
事实定位(item["proposal_id"], 1, item["proposal_hash"], "world:relation-main", 0),
|
||
"confirm-relation",
|
||
)
|
||
assert any(
|
||
v["object_id"] == "relation-main"
|
||
for v in 世界.查询世界(作者, "story-work", "chapter-2")["facts"]
|
||
)
|
||
|
||
|
||
def test_关系确认同时保护端点的正文依据__ac1009(故事环境, monkeypatch):
|
||
from concurrent.futures import ThreadPoolExecutor
|
||
from threading import Event
|
||
from time import monotonic, sleep
|
||
|
||
from muse.故事世界.存储 import 故事存储
|
||
|
||
装配, 作者, 库 = 故事环境
|
||
世界 = 装配.要求故事世界()
|
||
req = 关系提案(装配, 作者)
|
||
result = 世界.提出事实(作者, "relation-with-lock", req)
|
||
item = 世界.读取提案(作者, result["results"][0]["proposal_id"])
|
||
loc = 事实定位(item["proposal_id"], 1, item["proposal_hash"], "world:relation-main", 0)
|
||
review = 世界.打开事实审阅(作者, loc)
|
||
entered, release = Event(), Event()
|
||
pids = []
|
||
original = 故事存储.写对象
|
||
|
||
def pause_write(self, *args, **kwargs):
|
||
# 只暂停调度,随后调用真实存储;不替换数据库行为或回执。
|
||
pids.append(self.连.info.backend_pid)
|
||
entered.set()
|
||
assert release.wait(5)
|
||
return original(self, *args, **kwargs)
|
||
|
||
monkeypatch.setattr(故事存储, "写对象", pause_write)
|
||
with ThreadPoolExecutor(2) as pool:
|
||
accepted = pool.submit(
|
||
世界.决定事实,
|
||
作者,
|
||
"locked-confirm",
|
||
loc,
|
||
作者动作.采纳,
|
||
review["review_id"],
|
||
review["review_hash"],
|
||
("object",),
|
||
)
|
||
assert entered.wait(5)
|
||
edited = pool.submit(
|
||
装配.要求正文().保存人工,
|
||
作者,
|
||
"parallel-source-write",
|
||
"chapter-1",
|
||
1,
|
||
正文草稿((段落("p1", (文本节点("新的码头段落。"),)),)),
|
||
)
|
||
try:
|
||
end = monotonic() + 3
|
||
blocked = False
|
||
while monotonic() < end and not blocked:
|
||
with 库[用途.维护].连接() as conn:
|
||
blocked = conn.execute(
|
||
"SELECT EXISTS(SELECT 1 FROM pg_stat_activity "
|
||
"WHERE %s=ANY(pg_blocking_pids(pid)))",
|
||
(pids[0],),
|
||
).fetchone()[0]
|
||
if not blocked:
|
||
sleep(0.01)
|
||
assert blocked and not edited.done()
|
||
finally:
|
||
release.set()
|
||
assert accepted.result(timeout=5)["results"][0]["object_id"] == "relation-main"
|
||
assert edited.result(timeout=5)["results"][0]["revision"] == 2
|
||
|
||
|
||
def test_跨作品来源和提案目标拒绝__ac1010(故事环境):
|
||
装配, 作者, _ = 故事环境
|
||
作品 = 装配.要求作品()
|
||
schema = 作品.新档案结构(作者, "another-work", "work_core", 1)
|
||
作品.保存档案(
|
||
作者, "create-another", 档案保存("another-work", 0, {"名称": "另一部合成作品"}, schema)
|
||
)
|
||
作品.添加章节(
|
||
作者, "another-chapter", "another-work", "another-chapter", "另一章", 预期目录版本=0
|
||
)
|
||
装配.要求正文().保存人工(
|
||
作者,
|
||
"another-body",
|
||
"another-chapter",
|
||
0,
|
||
正文草稿((段落("other-p", (文本节点("另一部作品的角色。"),)),)),
|
||
)
|
||
世界 = 装配.要求故事世界()
|
||
frame = 世界.新事实结构(作者, "story-work", "wrong-source", "character", 1)
|
||
req = 事实提案请求(
|
||
"story-work",
|
||
"wrong-source",
|
||
0,
|
||
frame,
|
||
{"名称": "另一部作品的角色"},
|
||
(来源(装配, 作者, "another-chapter", "另一部作品的角色"),),
|
||
"chapter-1",
|
||
)
|
||
with pytest.raises(Muse错误) as err:
|
||
世界.提出事实(作者, "cross-source", req)
|
||
assert err.value.错误码 == "SCOPE_DENIED"
|
||
_, loc = 提案(装配, 作者, "own-person", independent=True)
|
||
with pytest.raises(Muse错误) as err:
|
||
世界.打开事实审阅(作者, replace(loc, target_ref="world:another-person"))
|
||
assert err.value.错误码 == "SCOPE_DENIED"
|
||
assert 世界.查询世界(作者, "story-work", "chapter-1")["facts"] == []
|
||
|
||
|
||
def test_事实确认失败回滚且原命令可恢复__ac1011(故事环境):
|
||
import psycopg
|
||
|
||
装配, 作者, 库 = 故事环境
|
||
_, loc = 提案(装配, 作者, "rollback-person", independent=True)
|
||
世界 = 装配.要求故事世界()
|
||
review = 世界.打开事实审阅(作者, loc)
|
||
with 库[用途.维护].连接() as conn:
|
||
conn.execute("""CREATE FUNCTION reject_world_decision() RETURNS trigger
|
||
LANGUAGE plpgsql AS $$
|
||
BEGIN RAISE EXCEPTION 'synthetic world decision failure'; END $$;
|
||
CREATE TRIGGER reject_world_decision BEFORE INSERT ON muse_candidate_decision
|
||
FOR EACH ROW EXECUTE FUNCTION reject_world_decision();""")
|
||
args = (
|
||
作者,
|
||
"world-retry",
|
||
loc,
|
||
作者动作.采纳,
|
||
review["review_id"],
|
||
review["review_hash"],
|
||
("object",),
|
||
)
|
||
with pytest.raises(psycopg.Error):
|
||
世界.决定事实(*args)
|
||
assert 世界.查询世界(作者, "story-work", "chapter-1")["system_revision"] == 0
|
||
with 库[用途.维护].连接() as conn:
|
||
assert conn.execute("SELECT count(*) FROM muse_world_object").fetchone()[0] == 0
|
||
conn.execute("DROP TRIGGER reject_world_decision ON muse_candidate_decision")
|
||
receipt = 世界.决定事实(*args)
|
||
assert 世界.决定事实(*args) == receipt
|
||
assert receipt["results"][0]["system_revision"] == 1
|
||
|
||
|
||
def test_事实修订追加版本且历史系统截止仍可回查__ac1012(故事环境):
|
||
装配, 作者, _ = 故事环境
|
||
世界 = 装配.要求故事世界()
|
||
_, loc = 提案(装配, 作者, "versioned-person", independent=True)
|
||
首 = 确认(装配, 作者, loc, "confirm-v1")
|
||
frame = 世界.新事实结构(作者, "story-work", "versioned-person", "character", 1)
|
||
req = 事实提案请求(
|
||
"story-work",
|
||
"versioned-person",
|
||
1,
|
||
frame,
|
||
{"名称": "作者修订后的名字"},
|
||
(),
|
||
"chapter-2",
|
||
independent=True,
|
||
)
|
||
created = 世界.提出事实(作者, "revised-proposal", req)
|
||
p = 世界.读取提案(作者, created["results"][0]["proposal_id"])
|
||
确认(
|
||
装配,
|
||
作者,
|
||
事实定位(p["proposal_id"], 1, p["proposal_hash"], "world:versioned-person", 1),
|
||
"confirm-v2",
|
||
)
|
||
assert (
|
||
世界.查询世界(作者, "story-work", "chapter-2")["facts"][0]["content"]["名称"]
|
||
== "作者修订后的名字"
|
||
)
|
||
早期 = 世界.查询世界(作者, "story-work", "chapter-1")["facts"][0]
|
||
assert 早期["revision"] == 1
|
||
历史 = 世界.查询世界(
|
||
作者, "story-work", "chapter-2", 系统版本=首["results"][0]["system_revision"]
|
||
)["facts"][0]
|
||
assert 历史["revision"] == 1 and 历史["content"] == 早期["content"]
|
||
with pytest.raises(Muse错误) as err:
|
||
世界.提出事实(作者, "old-version-write", req)
|
||
assert err.value.错误码 == "REVISION_CONFLICT"
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"模式,生效",
|
||
[("fact", "chapter-3"), ("plan", "chapter-1"), ("belief", "chapter-1")],
|
||
ids=["future", "plan", "belief"],
|
||
)
|
||
def test_事实引用不借用未来或猜测计划实体__ac1013(故事环境, 模式, 生效):
|
||
装配, 作者, _ = 故事环境
|
||
世界 = 装配.要求故事世界()
|
||
req = 关系提案(装配, 作者)
|
||
_, loc = 提案(装配, 作者, "unavailable-endpoint", independent=True, mode=模式, effective=生效)
|
||
确认(装配, 作者, loc, "confirm-unavailable-endpoint")
|
||
with pytest.raises(Muse错误) as 错:
|
||
世界.提出事实(
|
||
作者,
|
||
"fact-with-unavailable-endpoint",
|
||
replace(req, content={"甲方": "person-left", "乙方": "unavailable-endpoint"}),
|
||
)
|
||
assert 错.value.错误码 == "FACT_REFERENCE_INVALID"
|
||
# 规划仍能引用其时点内的规划对象,不能把计划伪装成事实。
|
||
if 模式 == "plan":
|
||
assert 世界.提出事实(
|
||
作者,
|
||
"planned-relation",
|
||
replace(
|
||
req,
|
||
knowledge_mode="plan",
|
||
content={"甲方": "person-left", "乙方": "unavailable-endpoint"},
|
||
),
|
||
)["results"]
|
||
|
||
|
||
def test_历史事实按保存结构解释并应用当前策略__ac1014(故事环境):
|
||
from muse.元数据.接口 import 值规则, 合成结构, 启用命令, 字段定义, 投影字段
|
||
from muse.故事世界.设定修订 import 事实授权
|
||
|
||
装配, 作者, 库 = 故事环境
|
||
世界 = 装配.要求故事世界()
|
||
_, loc = 提案(装配, 作者, "historical-schema", independent=True)
|
||
确认(装配, 作者, loc, "confirm-historical-schema")
|
||
before = 世界.查询世界(作者, "story-work", "chapter-1")["facts"][0]
|
||
with 库[用途.维护].连接() as 连, 连.transaction():
|
||
meta = 元数据服务(连)
|
||
old = meta.读取结构("character", 1)
|
||
new = replace(
|
||
old,
|
||
schema_version=2,
|
||
父版本=1,
|
||
字段=old.字段 + (字段定义("character.new", "新提示", "新提示", 值规则("text")),),
|
||
)
|
||
meta.登记结构候选(new)
|
||
meta.发布结构("character", 2, new.内容哈希)
|
||
effective = meta.读取有效结构("character", 2)
|
||
meta.启用绑定(
|
||
"world:historical-schema",
|
||
"story-work",
|
||
effective,
|
||
启用命令(
|
||
"schema-addition",
|
||
effective.effective_schema_hash,
|
||
1,
|
||
"author",
|
||
"world:historical-schema",
|
||
),
|
||
)
|
||
expected = 投影字段(
|
||
合成结构(old),
|
||
meta.当前策略("character"),
|
||
事实授权("author", "story-work"),
|
||
字段用途名="uiVisible",
|
||
内容用途="planning",
|
||
运行用途="production",
|
||
)
|
||
after = 世界.查询世界(作者, "story-work", "chapter-1")["facts"][0]
|
||
assert after["content"] == before["content"]
|
||
assert after["schema_binding"]["base_version"] == 1
|
||
assert after["projection_version"] == expected.projection_version
|
||
|
||
|
||
def test_角色获知时点独立于事件生效__ac1015(故事环境):
|
||
装配, 作者, _ = 故事环境
|
||
世界 = 装配.要求故事世界()
|
||
_, 主体 = 提案(装配, 作者, "late-knower", independent=True)
|
||
确认(装配, 作者, 主体, "confirm-late-knower")
|
||
schema = 世界.新事实结构(作者, "story-work", "late-known-event", "character", 1)
|
||
请求 = 事实提案请求(
|
||
"story-work",
|
||
"late-known-event",
|
||
0,
|
||
schema,
|
||
{"名称": "林深登船"},
|
||
(来源(装配, 作者, "chapter-1", "林深停在码头"),),
|
||
"chapter-1",
|
||
known_to=({"actor_id": "late-knower", "from_chapter_id": "chapter-3"},),
|
||
)
|
||
回执 = 世界.提出事实(作者, "propose-late-knowledge", 请求)
|
||
原 = 世界.读取提案(作者, 回执["results"][0]["proposal_id"])
|
||
确认(
|
||
装配,
|
||
作者,
|
||
事实定位(原["proposal_id"], 1, 原["proposal_hash"], "world:late-known-event", 0),
|
||
"confirm-late-knowledge",
|
||
)
|
||
assert (
|
||
世界.查询世界(作者, "story-work", "chapter-1", 视角="character", 知情主体ID="late-knower")[
|
||
"facts"
|
||
]
|
||
== []
|
||
)
|
||
assert [
|
||
v["object_id"]
|
||
for v in 世界.查询世界(
|
||
作者, "story-work", "chapter-3", 视角="character", 知情主体ID="late-knower"
|
||
)["facts"]
|
||
] == ["late-known-event"]
|