muse-agent-example/tests/单元/test_规则节奏与伏笔.py
zizi d909d1bd1b 后端实现与用例身份:19 包集成落地并修复收尾缺陷
实现侧:
- 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。
- 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。
- 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。
- 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。
- 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。
- 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。
- 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。
- 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。

用例侧:
- 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存;
- 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
2026-09-18 01:15:00 +08:00

285 lines
12 KiB
Python
Raw Permalink 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 伏笔与承诺
@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"