"""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 伏笔与承诺 @pytest.mark.case_id( "NC-w31-c03dfd", environment="离线单元;临时目录与协议替身", given="伏笔/悬念/读者期待/章末钩子条目与本书章节顺序", when="登记一批伏笔条目", then=["合法条目按原样返回并默认铺设状态;", "铺设章节不存在或目标窗口早于铺设时精确拒绝"], contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md", ) 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" @pytest.mark.case_id( "NC-w31-325b88", environment="离线单元;临时目录与协议替身", given="一条处于铺设状态的伏笔条目", when="按状态机推进到推进、兑现并尝试越界转移", then=["铺设→推进→兑现合法;", "终态再转移被拒绝;", "推进章节早于铺设被拒绝"], contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md", ) 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" @pytest.mark.case_id( "NC-w31-0ff84c", environment="离线单元;临时目录与协议替身", given="铺设/长期/已兑现三类条目与当前章节", when="读取伏笔总览", then=["未决与已决分组正确;", "越过目标窗口仍未兑现的条目标入逾期未兑现"], contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md", ) 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 规则与代价 @pytest.mark.case_id( "NC-w31-cb8b0c", environment="离线单元;临时目录与协议替身", given="能力边界/行动代价/例外条目输入", when="校验规则卡并读回", then=["缺省适用条件补全书适用;", "例外缺适用条件与重复条目精确拒绝"], contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md", ) 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" @pytest.mark.case_id( "NC-w31-4f0cda", environment="离线单元;临时目录与协议替身", given="空批次、未知类别与非规则卡形状", when="执行规则卡校验与读回", then=["空批次、坏类别、坏形状均精确失败,不猜测兼容"], contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md", ) 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" @pytest.mark.case_id( "NC-w31-365f8a", environment="离线单元;临时目录与协议替身", given="规则卡内容与既有事实提案请求合同", when="组装独立设定提案并测试非独立约束", then=["规则卡内容可作为实体content进入提案管线;", "非独立编辑仍须正文依据,与既有合同一致"], contract="docs/系统架构/新版设计/模块设计/B02-故事世界.md", ) 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) ) @pytest.mark.case_id( "NC-w31-cd21a9", environment="离线单元;临时目录与协议替身", given="带节奏标注的场景序列与七类基础节奏", when="校验节奏标注", then=["合法标注排序规范化;", "未登记类别与缺失标注精确拒绝"], contract="docs/系统架构/新版设计/模块设计/B01-作品规划.md", ) 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" @pytest.mark.case_id( "NC-w31-fc17cf", environment="离线单元;临时目录与协议替身", given="偏斜与均衡两组场景节奏", when="对照内置题材档位检查分布", then=["区间外类别列出不足/过密偏差;", "均衡分布零偏差"], contract="docs/系统架构/新版设计/模块设计/B01-作品规划.md", ) 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 均衡["连续单一提示"] == [] @pytest.mark.case_id( "NC-w31-a1b1ec", environment="离线单元;临时目录与协议替身", given="自定义题材档位与全日常场景", when="登记调整档位并检查分布", then=["按题材调整档位生效;", "缺基础类别与非法区间拒绝"], contract="docs/系统架构/新版设计/模块设计/B01-作品规划.md", ) 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"