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