muse-agent-example/tests/集成/test_故事事实确认与时点.py
zizi 2f15c5b3bc W12 本书事实、关系与时间边界基础:本书事实、关系提案、时点边界与角色知情历史。
按 R2 串行阶段整理提交;包内文件为该阶段交付(含后续小增量),状态以工作包清单为准。
2026-09-10 19:25:41 +08:00

716 lines
29 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.

"""真实元数据、正文、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"]