157 lines
7.2 KiB
Python
157 lines
7.2 KiB
Python
"""章后派生失败与重建:两作品连续两章、抽取失败恢复、旧版本派生拒绝与重建幂等。"""
|
||
|
||
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, "重建不得重复事实提案"
|