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