muse-agent-example/tests/集成/test_故事事实确认与时点.py
zizi d909d1bd1b 后端实现与用例身份:19 包集成落地并修复收尾缺陷
实现侧:
- 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。
- 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。
- 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。
- 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。
- 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。
- 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。
- 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。
- 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。

用例侧:
- 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存;
- 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
2026-09-18 01:15:00 +08:00

838 lines
36 KiB
Python
Raw Permalink 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 构建
from muse.故事世界.接口 import 事实定位, 事实提案请求, 事实错误, 定位字面引文, 正文依据
from muse.正式变更.接口 import 作者动作, 变更错误
from muse.正文写作.接口 import 文本节点, 正文草稿, 段落
from muse.配置 import 应用配置
pytestmark = pytest.mark.数据库
@pytest.fixture
def 故事环境(内置结构测试库):
应用测试库 = 内置结构测试库
装配 = 构建(应用配置(应用测试库[用途.生产].引用, "synthetic"))
with 装配.生命周期():
作者 = 调用身份("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),)),))
)
yield 装配, 作者, 应用测试库
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",)
)
@pytest.mark.case_id(
"NC-world-source-independence",
environment="真实隔离PostgreSQL;实际模块/ASGI,无模型调用",
given="正式正文支持的事实与独立作者设定均已确认",
when="更新其中唯一正文依据",
then=["依赖事实退出当前消费,独立设定保留,候选不提前转正"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
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", "码头上没有人。")
后 = 装配.要求故事世界().查询世界(作者, "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"
@pytest.mark.case_id(
"NC-world-temporal-system-cutoff",
environment="真实隔离PostgreSQL;实际模块/ASGI,无模型调用",
given="不同叙事位置和系统确认次序的事实",
when="按章节和系统版本查询",
then=["两个时轴分别过滤,不倒灌后来确认结果"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
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"}
@pytest.mark.case_id(
"NC-world-disclosure-and-belief",
environment="真实隔离PostgreSQL;实际模块/ASGI,无模型调用",
given="后章披露早期事件,以及只属于指定主体的猜测与计划",
when="按作者、读者及角色视角查询",
then=["支持回忆,早期读者不获知后文依据;猜测、计划与事实分组"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
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"] == []
@pytest.mark.case_id(
"NC-world-alternative-evidence",
environment="真实隔离PostgreSQL;实际模块/ASGI,无模型调用",
given="同一事实具有两个独立充分的正文依据",
when="其中一个正文版本改变",
then=["另一个当前依据足够时保留该事实"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
def test_其他充分正文依据保持已确认事实有效__ac1004(故事环境):
装配, 作者, _ = 故事环境
一 = 来源(装配, 作者, "chapter-1", "林深停在码头")
二 = 来源(装配, 作者, "chapter-3", "林深来到北岸")
_, 定位 = 提案(装配, 作者, "two-sources", sources=(一, 二))
确认(装配, 作者, 定位, "confirm-two-sources")
修改首章(装配, 作者, "change-one-source", "码头上没有人。")
当前 = 装配.要求故事世界().查询世界(作者, "story-work", "chapter-3")
assert [v["object_id"] for v in 当前["facts"]] == ["two-sources"]
@pytest.mark.case_id(
"NC-world-stale-review",
environment="真实隔离PostgreSQL;实际模块/ASGI,无模型调用",
given="已打开事实审阅,随后正文依据更新",
when="尝试原审阅确认,并重新审阅拒绝",
then=["旧采纳拒绝;作者仍可明确拒绝陈旧提案"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
def test_源变后旧审阅不能确认但可以重新审阅拒绝__ac1005(故事环境):
装配, 作者, _ = 故事环境
世界 = 装配.要求故事世界()
_, 定位 = 提案(
装配, 作者, "stale-review", sources=(来源(装配, 作者, "chapter-1", "林深停在码头"),)
)
审阅 = 世界.打开事实审阅(作者, 定位)
修改首章(装配, 作者, "change-reviewed-source", "没有人停在码头。")
with pytest.raises((变更错误, 事实错误)) 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"] == []
)
@pytest.mark.case_id(
"NC-world-dynamic-type-window",
environment="真实隔离PostgreSQL;实际模块/ASGI,无模型调用",
given="PG发布新的登船点结构和未来预期窗口",
when="提案、确认、读取;另给逆序窗口",
then=["无需改业务代码保存动态内容,非法区间拒绝,作者规划窗口不混入读者信息"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
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(事实错误) 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 读者
@pytest.mark.case_id(
"NC-world-http-author-flow",
environment="真实隔离PostgreSQL;实际模块/ASGI,无模型调用",
given="实际ASGI、作者认证、隔离PG与版本化正文",
when="从HTTP预览结构、提案、审阅、确认并查询",
then=["自报作者拒绝,只有明确决定产生事实,重复命令同回执"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
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
@pytest.mark.case_id(
"NC-world-reference-and-quote",
environment="真实隔离PostgreSQL;实际模块/ASGI,无模型调用",
given="本书已确认实体与有效关系结构",
when="提交缺端点、未知引用、伪引文和合法关系",
then=["坏提案拒绝,合法关系经S01实际落库"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
def test_关系端点和正文引文必须真实__ac1008(故事环境):
装配, 作者, _ = 故事环境
世界 = 装配.要求故事世界()
req = 关系提案(装配, 作者)
# 甲方/乙方在该结构里是可选取值(同一结构也服务 reference_entity 族),
# 缺端点由关系族的端点计数守卫拒绝,而不是元数据必填字段。
with pytest.raises(事实错误) as 缺字段:
世界.提出事实(作者, "missing-endpoint", replace(req, content={"甲方": "person-left"}))
assert 缺字段.value.错误码 == "FACT_REFERENCE_INVALID"
assert 缺字段.value.说明 == "关系结构必须提供真实端点"
with pytest.raises(事实错误) 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(事实错误) 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"]
)
@pytest.mark.case_id(
"NC-world-reference-source-lock",
environment="真实隔离PostgreSQL;实际模块/ASGI,无模型调用",
given="关系提案引用实体,实体另依赖一章正文",
when="真实关系确认与正文更新并发,暂停调度观察PG阻塞",
then=["关系事务持有所需正文当前指针;原存储实际执行后正文写入才能完成"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
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
@pytest.mark.case_id(
"NC-world-scope-binding",
environment="真实隔离PostgreSQL、S01及B02,不调用模型",
given="两个真实作品与各自正文",
when="跨书引用来源或改换提案目标",
then=["作用域拒绝,事实不产生"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
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(事实错误) as err:
世界.提出事实(作者, "cross-source", req)
assert err.value.错误码 == "SCOPE_DENIED"
_, loc = 提案(装配, 作者, "own-person", independent=True)
with pytest.raises(事实错误) as err:
世界.打开事实审阅(作者, replace(loc, target_ref="world:another-person"))
assert err.value.错误码 == "SCOPE_DENIED"
assert 世界.查询世界(作者, "story-work", "chapter-1")["facts"] == []
@pytest.mark.case_id(
"NC-world-decision-rollback",
environment="真实隔离PostgreSQL、S01及B02,不调用模型",
given="真实PG在作者决定阶段拒绝写入",
when="确认失败后解除故障并重放原命令",
then=["对象、绑定与世界系统版本整体回滚,同命令恢复后返回同一回执"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
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
@pytest.mark.case_id(
"NC-world-version-history",
environment="真实隔离PostgreSQL与S01/B02",
given="同一事实对象已有一个确认版本",
when="修订产生新版本,再按叙事时点和系统版本回查,尝试旧基线写入",
then=["修订只追加版本,历史可读且陈旧写入拒绝"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
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(事实错误) as err:
世界.提出事实(作者, "old-version-write", req)
assert err.value.错误码 == "REVISION_CONFLICT"
@pytest.mark.case_id(
"NC-world-reference-time-kind",
environment="真实隔离PostgreSQL与S01/B02",
given="本书存在后章生效或已确认计划、猜测实体",
when="早章事实提案引用上述对象,并以真实规划提案作对照",
then=["事实拒绝未来、计划和猜测端点;规划仍可引用该时点的规划对象"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
@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(事实错误) as 错:
世界.提出事实(
作者,
"fact-with-unavailable-endpoint",
replace(req, content={"甲方": "person-left", "乙方": "unavailable-endpoint"}),
)
assert 错.value.错误码 == "FACT_REFERENCE_INVALID"
# 规划仍能引用其时点内的规划对象,不能把计划伪装成事实。
if 模式 == "plan":
前 = 世界.查询世界(作者, "story-work", "chapter-2")
回执 = 世界.提出事实(
作者,
"planned-relation",
replace(
req,
knowledge_mode="plan",
content={"甲方": "person-left", "乙方": "unavailable-endpoint"},
),
)
assert len(回执["results"]) == 1
项 = 回执["results"][0]
assert 项["revision"] == 1 and 项["proposal_hash"]
提案行 = 世界.读取提案(作者, 项["proposal_id"])
assert 提案行["object_id"] == req.object_id
assert 提案行["knowledge_mode"] == "plan"
assert 提案行["content"] == {"甲方": "person-left", "乙方": "unavailable-endpoint"}
assert 世界.查询世界(作者, "story-work", "chapter-2") == 前
@pytest.mark.case_id(
"NC-world-history-schema",
environment="真实隔离PostgreSQL与S01/B02",
given="已确认事实保存了结构绑定v1",
when="当前绑定启用兼容v2后回查旧事实",
then=["旧值与投影使用旧版本保存结构,当前策略继续参与"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
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
@pytest.mark.case_id(
"NC-world-actor-knowledge-time",
environment="真实隔离PostgreSQL与S01/B02",
given="第1章事实在第3章才被指定角色获知",
when="分别查询角色第1章和第3章视图",
then=["第1章省略未获知事实,第3章可见"],
contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md",
)
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"]
def 修改首章(装配, 作者, 命令, 内容):
return 装配.要求正文().保存人工(
作者, 命令, "chapter-1", 1, 正文草稿((段落("p1", (文本节点(内容),)),))
)