"""真实元数据、正文、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", (文本节点(内容),)),)) )