"""章后派生失败与重建:两作品连续两章、抽取失败恢复、旧版本派生拒绝与重建幂等。""" import pytest from pydantic import TypeAdapter from muse.共享.错误 import Muse错误 from muse.故事世界.接口 import 事实定位 from muse.正式变更.接口 import 作者动作 from muse.正文写作.接口 import 正文草稿 pytestmark = pytest.mark.数据库 def _事实(引文: str, object_id: str = "lin-shen") -> list[dict]: return [ { "type": "character", "object_id": object_id, "quote": 引文, "content": {"名称": "林深"}, } ] def _确认提案(环境, 提案ID: str, object_id: str) -> None: 世界 = 环境["装配"].要求故事世界() 作者 = 环境["作者"] 提案 = 世界.读取提案(作者, 提案ID) 定位 = 事实定位( 提案["proposal_id"], 提案["revision"], 提案["proposal_hash"], "world:" + object_id, 0 ) 审阅 = 世界.打开事实审阅(作者, 定位) 世界.决定事实( 作者, f"confirm-{object_id}-{提案ID[:8]}", 定位, 作者动作.采纳, 审阅["review_id"], 审阅["review_hash"], ("object",), ) def test_两作品各连续两章派生确认事实并按时点消费__16c01a(章后环境, 章后工具): 环境 = 章后环境 环境["新作品"]("post-work-b", 2, "b-ch-") 环境["剧本"].append(章后工具.抽取输出("第一章:林深滞留码头。", _事实("林深把伞收在了门边"))) 任务1, _ = 章后工具.保存一章("ch-1", "林深把伞收在了门边,雨还没停。") 快照1 = 章后工具.跑任务(任务1) assert 快照1.状态.value == "completed" 检查点1 = {s["step_id"]: (s.get("checkpoint") or {}) for s in 快照1.步骤} 索引1 = 检查点1["登记摘要与索引"]["索引"] assert 索引1["revision"] == 1 and 索引1["document_hash"] 提案1 = 检查点1["登记事实提案"]["提案"] assert len(提案1) == 1 and not 检查点1["登记事实提案"]["跳过"] _确认提案(环境, 提案1[0]["proposal_id"], "lin-shen") 环境["剧本"].append(章后工具.抽取输出("第二章:林深离开码头。", [])) 任务2, _ = 章后工具.保存一章("ch-2", "林深沿着码头走了出去。") assert 章后工具.跑任务(任务2).状态.value == "completed" # 连续写作按正确时点消费确认事实:截至第一章的世界状态包含已确认事实。 世界 = 环境["装配"].要求故事世界() 时点 = 世界.查询世界(环境["作者"], "post-work", "ch-1") 对象 = {o["object_id"]: o for o in 时点["facts"]} assert 对象["lin-shen"]["content"]["名称"] == "林深" # 第二部作品同旅程;确认事实不跨作品。 环境["剧本"].append(章后工具.抽取输出("B 第一章。", [])) 任务B, _ = 章后工具.保存一章("b-ch-1", "B 章文本。") assert 章后工具.跑任务(任务B).状态.value == "completed" 时点B = 世界.查询世界(环境["作者"], "post-work-b", "b-ch-1") assert "lin-shen" not in {o["object_id"] for o in 时点B["facts"]} def test_抽取失败后正文可用且恢复重试完成__16c02b(章后环境, 章后工具): 环境 = 章后环境 from muse.任务运行.接口 import 任务状态 # 输出合同不满足:调用已结算但终态无效 → 步骤失败、任务失败(无未知调用)。 环境["剧本"].append({"类型": "文本", "文本": {"summary": "只有摘要"}}) 任务1, 回执 = 章后工具.保存一章("ch-1", "林深把伞收在了门边。") with pytest.raises((Muse错误, RuntimeError, OSError)): 章后工具.跑任务(任务1) 终态 = 环境["装配"].任务运行.读取任务(任务1) assert 终态.状态 == 任务状态.已失败 抽取步 = next(s for s in 终态.步骤 if s["step_id"] == "抽取提案") assert 抽取步["state"] == "failed" # 正文成功保存:派生失败不回滚作者正文。 正文 = 环境["装配"].要求正文().读取正文(环境["作者"], "ch-1") assert 正文["revision"] == 1 assert TypeAdapter(正文草稿).validate_python(正文["document"]) 环境["剧本"].append(章后工具.抽取输出("恢复后的摘要。", [])) 环境["装配"].任务运行.控制任务( 任务1, 环境["作者"].作者, 任务状态.已失败, "恢复", 命令ID=f"resume-{任务1[:8]}" ) 快照 = 章后工具.跑任务(任务1) assert 快照.状态.value == "completed" assert 回执["receipt_id"] def test_编辑后旧版本派生被拒绝且按新版本重建__16c03c(章后环境, 章后工具): 环境 = 章后环境 # 保存 r1 不跑派生;编辑出 r2 后再跑 r1 任务,必须被来源复检拒绝。 任务1, _ = 章后工具.保存一章("ch-1", "第一版正文,林深在码头。") # r1 在来源复检即失败,不消费抽取剧本;r2 消费队头的新版本摘要。 环境["剧本"].append(章后工具.抽取输出("新版本摘要。", [])) 任务2, _ = 章后工具.保存一章("ch-1", "第二版正文,林深在码头整理行装。", 预期版本=1) with pytest.raises((Muse错误, RuntimeError, OSError)): 章后工具.跑任务(任务1) 快照1 = 环境["装配"].任务运行.读取任务(任务1) assert 快照1.状态.value == "failed" assert any(s["step_id"] == "来源复检" and s["state"] == "failed" for s in 快照1.步骤), ( 快照1.步骤 ) 快照2 = 章后工具.跑任务(任务2) assert 快照2.状态.value == "completed" from muse.编排.章后处理 import 查询章后状态 状态 = 查询章后状态(环境["装配"], 环境["作者"].作者, "ch-1") assert 状态["revision"] == 2 and 状态["摘要"] == "新版本摘要。" # 旧任务的提案登记不得覆盖新版本投影:旧任务未产出提案。 旧步骤 = {s["step_id"]: s for s in 快照1.步骤} assert not (旧步骤["登记事实提案"].get("checkpoint") or {}).get("提案") def test_重建幂等不重复事实提案__16c04d(章后环境, 章后工具): 环境 = 章后环境 事实 = _事实("林深在码头") 环境["剧本"].append(章后工具.抽取输出("摘要一。", 事实)) 任务1, _ = 章后工具.保存一章("ch-1", "林深在码头。雨停了。") 章后工具.跑任务(任务1) from muse.编排.章后处理 import 发起章后处理, 查询章后状态 重建 = 发起章后处理( 环境["装配"], 环境["作者"], f"rebuild-{任务1[:8]}", work_id="post-work", chapter_id="ch-1", ) 环境["剧本"].append(章后工具.抽取输出("摘要一。", 事实)) 章后工具.跑任务(重建["task_id"]) 状态 = 查询章后状态(环境["装配"], 环境["作者"].作者, "ch-1") assert 状态["摘要"] == "摘要一。" with 环境["库"].连接(只读=True) as 连: 数 = 连.execute( "SELECT count(DISTINCT p.proposal_id) FROM muse_fact_proposal p " "WHERE p.work_id=%s AND p.object_id=%s", ("post-work", "lin-shen"), ).fetchone()[0] assert 数 == 1, "重建不得重复事实提案"