muse-agent-example/tests/单元/test_规则节奏与伏笔.py
zizi 9e6f1c4481 R2 改造交付:新版模块化单体全量成果
- src/muse 新版全模块(装配/共享/上下文/任务运行/作品规划/故事世界/正文写作/审校修订/知识方法/作者经验/效果评测/交付连载/资料研究/正式变更/元数据/接入/基础设施/编排)+ 测试树(单元/契约/集成/架构/迁移/端到端/夹具)
- 129 项功能全部实现与自动验证(功能覆盖.json/矩阵),含 W31 补齐的规则与代价/节奏安排/伏笔与承诺
- 旧实现按处置清单退出(702 条中 324 删,保护合同与未迁移条目留存有据);web/app.py 旧工作台退役,新工作台为唯一写入口
- 数据库/旧库迁移:真实旧库内容批次迁移链(端点守卫/PG作品正文映射/质量资产缺省投影)
- 运行手册 docs/运行手册.md;W30 本机服务阶段一已运行(infra PG 为正式内容权威)
- R2 执行证据与私有运行材料在 .agents.local/改造/R2-20260909/(不入库)
2026-09-15 12:47:42 +08:00

213 lines
8.4 KiB
Python
Raw 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.

"""F04.02/F06.05/F07.04 三项补齐功能的单元行为;离线纯域校验,无数据库。"""
import pytest
from muse.作品规划.分层规划 import 内置题材档位, 校验节奏标注, 检查节奏分布, 登记题材档位
from muse.作品规划.接口 import 作品错误
from muse.故事世界.接口 import (
事实提案请求,
事实结构,
事实错误,
伏笔总览,
伏笔条目,
叙事窗口,
推进伏笔,
校验规则卡,
登记伏笔,
读取规则卡,
)
章节顺序 = {f"chapter-{i}": i for i in range(1, 13)}
# ---------------------------------------------------------------------------
# F07.04 伏笔与承诺
def test_伏笔登记校验种类ID章节与窗口__c03dfd():
条目们 = 登记伏笔(
(
伏笔条目(
"hook-blackvein",
"章末钩子",
"黑纹第一次缠上手臂",
"chapter-2",
目标窗口=叙事窗口("chapter-8", "chapter-12"),
),
伏笔条目("susp-core", "悬念", "异种核心的来历不明", "chapter-1"),
),
章节顺序,
)
assert [t.条目ID for t in 条目们] == ["hook-blackvein", "susp-core"]
assert 条目们[0].状态 == "铺设" and 条目们[1].目标窗口 is None
with pytest.raises(事实错误) as 错误:
登记伏笔((伏笔条目("a", "伏笔", "x", "chapter-99"),), 章节顺序)
assert 错误.value.错误码 == "FACT_TIME_INVALID"
with pytest.raises(事实错误) as 错误:
登记伏笔(
(伏笔条目("b", "伏笔", "x", "chapter-5", 目标窗口=叙事窗口("chapter-2", "chapter-3")),),
章节顺序,
)
assert 错误.value.错误码 == "FACT_TIME_INVALID"
def test_伏笔状态机拒绝非法转移与过早推进__325b88():
铺设 = 伏笔条目("hook", "伏笔", "旧钥匙的用途", "chapter-3")
推进 = 推进伏笔(铺设, "推进", 章节顺序, 截止章节="chapter-6")
assert 推进.状态 == "推进"
兑现 = 推进伏笔(推进, "兑现", 章节顺序, 截止章节="chapter-9")
with pytest.raises(事实错误) as 错误:
推进伏笔(兑现, "推进", 章节顺序) # 终态不可再动
assert 错误.value.错误码 == "FORESHADOW_TRANSITION_INVALID"
with pytest.raises(事实错误) as 错误:
推进伏笔(铺设, "推进", 章节顺序, 截止章节="chapter-1") # 早于铺设
assert 错误.value.错误码 == "FACT_TIME_INVALID"
def test_伏笔总览分组并标逾期未兑现__0ff84c():
条目们 = (
伏笔条目("a", "伏笔", "已铺未兑", "chapter-2", 目标窗口=叙事窗口("chapter-4", "chapter-6")),
伏笔条目("b", "读者期待", "长期期待", "chapter-1"),
推进伏笔(伏笔条目("c", "悬念", "已兑现", "chapter-2"), "兑现", 章节顺序),
)
总览 = 伏笔总览(条目们, 章节顺序, "chapter-9")
assert [行["条目ID"] for 行 in 总览["未决"]] == ["a", "b"]
assert [行["条目ID"] for 行 in 总览["已决"]] == ["c"]
assert 总览["逾期未兑现"] == ["a"] # 目标窗口止于 chapter-6,当前 chapter-9
# ---------------------------------------------------------------------------
# F04.02 规则与代价
def test_规则卡校验重复例外条件与读回__cb8b0c():
内容 = 校验规则卡(
(
{"类别": "能力边界", "内容": "黑纹一次最多缠一臂"},
{"类别": "行动代价", "内容": "每次缠臂消耗一日记忆", "适用条件": "主动使用"},
{"类别": "例外", "内容": "核心共鸣时全身覆盖", "适用条件": "濒死"},
)
)
assert 内容["规则条目"][0]["适用条件"] == "全书适用"
assert 读取规则卡(内容)[2]["适用条件"] == "濒死"
with pytest.raises(事实错误) as 错误:
校验规则卡(
(
{"类别": "资源限制", "内容": "x"},
{"类别": "资源限制", "内容": "x"},
)
)
assert 错误.value.错误码 == "RULE_CARD_DUPLICATE"
with pytest.raises(事实错误) as 错误:
校验规则卡(({"类别": "例外", "内容": "无条件的例外"},))
assert 错误.value.错误码 == "RULE_CARD_INVALID"
def test_规则卡空批与坏类别精确失败__4f0cda():
with pytest.raises(事实错误) as 错误:
校验规则卡(())
assert 错误.value.错误码 == "RULE_CARD_EMPTY"
with pytest.raises(事实错误) as 错误:
校验规则卡(({"类别": "未知类别", "内容": "x"},))
assert 错误.value.错误码 == "RULE_CARD_INVALID"
with pytest.raises(事实错误) as 错误:
读取规则卡({"别的形状": []})
assert 错误.value.错误码 == "RULE_CARD_INVALID"
def test_规则卡内容经实体提案形状保存__365f8a():
内容 = 校验规则卡(({"类别": "能力边界", "内容": "灵力上限三章不涨"},))
请求 = 事实提案请求(
work_id="synthetic-work",
object_id="rule-card-1",
expected_revision=0,
schema=事实结构("world_rule", 1, "hash", "proj", None),
content=内容,
sources=(),
effective_from=None,
independent=True,
knowledge_mode="setting",
)
assert 请求.content == 内容 and 请求.independent
# 非独立编辑仍须正文依据,与既有实体提案合同一致
with pytest.raises(事实错误) as 错误:
事实提案请求(
work_id="synthetic-work",
object_id="rule-card-1",
expected_revision=0,
schema=事实结构("world_rule", 1, "hash", "proj", None),
content=内容,
sources=(),
effective_from=None,
knowledge_mode="setting",
)
assert 错误.value.错误码 == "FACT_INVALID"
# ---------------------------------------------------------------------------
# F06.05 节奏安排
def 场景们(节奏表):
return tuple(
{"场景ID": f"s{i}", "顺序": i, "节奏": (节奏,)} for i, 节奏 in enumerate(节奏表, start=1)
)
def test_节奏标注校验非法类别与缺失标注__cd21a9():
允许 = frozenset(内置题材档位)
规范 = 校验节奏标注(场景们(["冲突", "行动", "日常"]), 允许)
assert [行["顺序"] for 行 in 规范] == [1, 2, 3]
with pytest.raises(作品错误) as 错误:
校验节奏标注(场景们(["打斗"]), 允许)
assert 错误.value.错误码 == "RHYTHM_INVALID"
with pytest.raises(作品错误) as 错误:
校验节奏标注(({"场景ID": "s1", "顺序": 1, "节奏": ()},), 允许)
assert 错误.value.错误码 == "RHYTHM_INVALID"
def test_节奏分布对照题材档位列偏差__fc17cf():
表 = ["冲突", "行动", "日常"] * 4 # 12 场景:各 1/3
报告 = 检查节奏分布(场景们(表), 内置题材档位)
assert 报告["场景数"] == 12
assert abs(报告["分布"]["冲突"] - 1 / 3) < 1e-9
assert "铺垫" in 报告["偏差"] and "不足" in 报告["偏差"]["铺垫"]
assert "日常" in 报告["偏差"] and "过密" in 报告["偏差"]["日常"]
均衡表 = [
"冲突",
"铺垫",
"冲突",
"日常",
"行动",
"情绪",
"行动",
"解释",
"行动",
"兑现",
"冲突",
"铺垫",
"日常",
"行动",
"情绪",
"冲突",
"行动",
"铺垫",
"日常",
"行动",
]
均衡 = 检查节奏分布(场景们(均衡表), 内置题材档位)
assert 均衡["偏差"] == {} and 均衡["连续单一提示"] == []
def test_题材档位自定义调整与非法区间__a1b1ec():
档位 = 登记题材档位({**内置题材档位, "日常": (0.01, 1.0)})
assert 档位["日常"] == (0.01, 1.0) and 档位["冲突"] == 内置题材档位["冲突"]
报告 = 检查节奏分布(场景们(["日常"] * 10), 档位)
assert "日常" not in 报告["偏差"] # 放宽后 100% 日常不再判过密
with pytest.raises(作品错误) as 错误:
登记题材档位({k: v for k, v in 内置题材档位.items() if k != "情绪"})
assert 错误.value.错误码 == "RHYTHM_PROFILE_INVALID"
with pytest.raises(作品错误) as 错误:
登记题材档位({**内置题材档位, "冲突": (0.8, 0.2)})
assert 错误.value.错误码 == "RHYTHM_PROFILE_INVALID"