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

423 lines
16 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.

"""声音草案统计不变成认可;规则及持久化观察分别沿实际接缝承接。"""
from copy import deepcopy
import pytest
from pydantic import ValidationError
from muse.共享.错误 import Muse错误
四类例证 = {"sf", "snf", "boundary", "regression"}
def _单规则库(规则库, 规则ID):
from muse.审校修订.接口 import 核对规则库
规则 = 规则库["rules"][规则ID]
引用 = [例证ID for 类型 in 四类例证 for 例证ID in 规则["samples"][类型]]
return 核对规则库([规则], [规则库["samples"][例证ID] for 例证ID in 引用])
def _稿(文本):
from muse.正文写作.接口 import 文本节点, 正文草稿, 段落
return 正文草稿((段落("p1", (文本节点(文本),)),))
def _整段替换(原稿, 替换):
from muse.正文写作.接口 import 段落哈希, 选段修改
段 = 原稿.paragraphs[0]
原文 = 段.content[0].text
return 选段修改("p1", 段落哈希(段), 0, len(原文), 原文, 替换, "finding-1")
def test_声音草案公开采样规模和未知项__5efcd5():
from muse.审校修订.接口 import 生成声音草案
文本 = "".join(f"第{i}句,门外的雨还在落下,青石路上没有一个人。\n" for i in range(1, 65))
草案 = 生成声音草案("synthetic:demo", [{"source_ref": "demo.txt", "text": 文本}])
assert 草案["status"] == "proposed" and 草案["persisted"] is False
assert 草案["sampling"]["sample_sufficient"] is True
assert "characters" in 草案["unknown_fields"]
assert 草案["metrics"]["sentence_length"]["median"] > 0
for 候选 in 草案["example_candidates"]:
assert 文本[候选["start"] : 候选["end"]] == 候选["quote"]
def test_声音草案拒绝来源哈希漂移__f1f21a():
from muse.审校修订.接口 import 生成声音草案
with pytest.raises(Muse错误, match="source_sha256"):
生成声音草案(
"synthetic:demo",
[{"source_ref": "demo.txt", "source_sha256": "0" * 64, "text": "甲走进门。"}],
)
def test_声音画像保持确定性__3bba02():
from muse.审校修订.接口 import 统计声音样本
文本 = "甲走进门。乙关上窗。雨落下来。"
assert 统计声音样本(文本) == 统计声音样本(文本)
assert 统计声音样本(文本)["sample_sufficient"] is False
@pytest.mark.parametrize(
"输入",
[
[],
[{"source_ref": "a", "text": " "}],
[{"text": "正文"}],
[{"source_ref": "a", "text": "正文", "source_sha256": "bad-hash"}],
],
)
def test_声音预览拒空样张和含糊哈希__22b001(输入):
from muse.审校修订.接口 import 生成声音草案
with pytest.raises(Muse错误):
生成声音草案("synthetic:demo", 输入)
def test_重复来源不增加样本且引号统计不冒语义识别__22b002():
from muse.审校修订.接口 import 生成声音草案, 统计声音样本
条 = {"source_ref": "same-source", "text": "同一份样张。"}
with pytest.raises(Muse错误, match="重复"):
生成声音草案("synthetic:demo", [条, 条])
原文 = "「" * 10000 + "未闭合的外引号“甲”“乙”"
assert 统计声音样本(原文)["quoted_span_ratio"] == round(6 / len(原文), 4)
嵌套 = "「引用“文字”仍属同一个外层片段」"
assert 统计声音样本(嵌套)["quoted_span_ratio"] == 1.0
def test_内置规则候选逐条具备四类反向可查例证__3accff():
from muse.审校修订.接口 import 核对规则启用, 读取内置规则
规则库 = 读取内置规则()
assert len(规则库["rules"]) >= 10
for 规则 in 规则库["rules"].values():
assert 规则["status"] == "candidate"
assert set(规则["samples"]) == 四类例证
for 类型 in 四类例证:
assert 规则["samples"][类型]
for 例证ID in 规则["samples"][类型]:
例证 = 规则库["samples"][例证ID]
assert 例证["type"] == 类型
assert 规则["id"] in 例证["rules"]
with pytest.raises(Muse错误, match="B10逐例评测和正式批准"):
核对规则启用(next(iter(规则库["rules"].values())), 规则库["samples"], {"pass": True})
def test_规则缺回归例证时拒绝候选合同__de273e():
from muse.审校修订.接口 import 核对规则, 读取内置规则
规则库 = 读取内置规则()
损坏 = deepcopy(规则库["rules"]["l002"])
损坏["samples"]["regression"] = []
with pytest.raises(Muse错误, match="四类例证"):
核对规则(损坏, 规则库["samples"])
def test_规则引用不存在例证时拒绝候选合同__33e0bd():
from muse.审校修订.接口 import 核对规则, 读取内置规则
规则库 = 读取内置规则()
损坏 = deepcopy(规则库["rules"]["l002"])
损坏["samples"]["regression"] = ["不存在的样例id"]
with pytest.raises(Muse错误, match="四类例证"):
核对规则(损坏, 规则库["samples"])
def test_叙述规则屏蔽对白且命中仍只询问__680ac7():
from muse.审校修订.接口 import 诊断文本, 读取内置规则
文本 = "「值得注意的是,这里是对白。」值得注意的是,这里是叙述。"
规则库 = _单规则库(读取内置规则(), "l002")
诊断 = 诊断文本(文本, work_id="synthetic:demo", 规则库=规则库, 预览候选=True)
assert len(诊断["findings"]) == 1
发现 = 诊断["findings"][0]
assert 发现["quote"] == "值得注意的是"
assert 发现["start"] == 文本.rindex("值得注意的是")
assert 发现["carrier"] == "narration"
assert 发现["decision_proposal"] == "ask"
from muse.审校修订.机器味诊断 import 定位载体, 载体区间
assert 定位载体(文本, 1, 7, 载体区间(文本)) == "dialogue"
def test_情态与新增时间数字不能通过保真门__dbe298():
from muse.审校修订.接口 import 核对修订保真
from muse.正文写作.接口 import 应用选段
用例 = [
("多半背景不凡", "背景都不凡", "推测情态"),
("谁也不知道", "那年秋分的夜里,谁也不知道", "新的时间"),
("谁也不知道", "2026年3天后的夜里,谁也不知道", "新的时间"),
]
for 原文, 替换, 错误片段 in 用例:
原稿 = _稿(原文)
修改 = _整段替换(原稿, 替换)
结果 = 核对修订保真(原稿, 应用选段(原稿, (修改,)), (修改,))
assert 结果["mechanical_pass"] is False
assert any(错误片段 in 错误 for 错误 in 结果["errors"])
def test_无确认声音不能变成文学通过__ccc984():
from muse.审校修订.接口 import 生成审校分片, 组装文学报告
文本 = "甲走进门。乙关上窗。"
片 = 生成审校分片(文本, "a" * 64)
结果 = [
{
"shard_id": 当前.片ID,
"dimensions": {
"continuity": {"status": "reviewed", "summary": "无冲突。", "findings": []},
"craft": {"status": "reviewed", "summary": "已审。", "findings": []},
"voice": {"status": "uncalibrated", "summary": "没有已确认声音。", "findings": []},
},
}
for 当前 in 片
]
报告 = 组装文学报告(文本, "a" * 64, 片, 结果, 允许依据=(), 已确认声音=False)
assert 报告["dimension_status"]["voice"] == "uncalibrated"
assert 报告["model_verified"] is False and 报告["persisted"] is False
assert "pass" not in 报告
with pytest.raises(Muse错误):
组装文学报告(文本, "a" * 64, 片, 结果, 允许依据=(), 已确认声音={"status": "candidate"})
def test_无声音账本时内置规则四类例证完整且为候选__438198():
from muse.审校修订.接口 import 读取内置规则
规则库 = 读取内置规则()
assert len(规则库["rules"]) >= 10
assert {s["type"] for s in 规则库["samples"].values()} >= {
"sf",
"snf",
"boundary",
"regression",
}
assert all(r["status"] == "candidate" for r in 规则库["rules"].values())
def test_声音提案版本字段值边界拒绝__f104cf():
from muse.审校修订.接口 import 声音提案请求, 声音样张引用
样张 = 声音样张引用("document", "chapter-1", 1, "a" * 64, "main")
with pytest.raises(Muse错误, match="声音候选需要"):
声音提案请求(
work_id="synthetic:demo",
expected_voice_revision=-1,
schema_id="voice",
base_version=1,
narrator={"叙述": "冷峻"},
samples=(样张,),
)
with pytest.raises(Muse错误, match="声音候选需要"):
声音提案请求(
work_id="synthetic:demo",
expected_voice_revision=0,
schema_id="voice",
base_version=0,
narrator={"叙述": "冷峻"},
samples=(样张,),
)
def test_声音结构版本拒绝非整数__b4afca():
from muse.审校修订.接口 import 声音提案请求, 声音样张引用
样张 = 声音样张引用("document", "chapter-1", 1, "a" * 64, "main")
with pytest.raises(ValidationError) as 错误:
声音提案请求(
work_id="synthetic:demo",
expected_voice_revision=0,
schema_id="voice",
base_version="nope",
narrator={"叙述": "冷峻"},
samples=(样张,),
)
assert "base_version" in str(错误.value)
def test_声音提案缺叙述者内容时拒绝__1fe56c():
from muse.审校修订.接口 import 声音提案请求, 声音样张引用
样张 = 声音样张引用("document", "chapter-1", 1, "a" * 64, "main")
with pytest.raises(Muse错误, match="叙述者内容"):
声音提案请求("synthetic:demo", 0, "voice", 1, {}, (样张,))
def _样张():
from muse.审校修订.接口 import 声音样张引用
return 声音样张引用("document", "chapter-1", 1, "a" * 64, "main")
def _案例(case_id, 哈希):
from muse.审校修订.接口 import 案例定位
return 案例定位(case_id, 1, 哈希)
def _确定性规则():
from muse.审校修订.接口 import 读取内置规则
return [
规则
for 规则 in 读取内置规则()["rules"].values()
if 规则["trigger"]["type"] != "model_judgment"
]
def test_确定性触发规则可回放四类合同__49204f():
from muse.审校修订.接口 import 核对规则, 读取内置规则
库 = 读取内置规则()
确定性 = _确定性规则()
assert len(确定性) >= 2
assert {规则["trigger"]["type"] for 规则 in 确定性} <= {"regex", "handler", "density"}
for 规则 in 确定性:
引用 = [
例证ID
for 类型 in ("sf", "snf", "boundary", "regression")
for 例证ID in 规则["samples"][类型]
]
核对规则(规则, {例证ID: 库["samples"][例证ID] for 例证ID in 引用})
assert 规则["status"] == "candidate"
assert 规则["default_disposition"] in {"advisory", "blocking", "candidate"}
def test_未提供样式时上下文不含声音节__2b1f41():
from muse.上下文.声音材料 import 冻结声音身份, 校验声音节
# 无已确认声音=合法空注入:B09 不把任何声音节放入上下文,writer 输入不含样式约束。
assert 校验声音节(None) is None
assert 冻结声音身份(None) is None
def test_确认声音约束冻结且投影只给写手__0ef41b():
import uuid
from muse.上下文.声音材料 import 冻结声音身份, 校验声音节, 身份键
版本 = str(uuid.uuid4())
材料 = {
"version_id": 版本,
"revision": 1,
"content_hash": "a" * 64,
"schema_hash": "b" * 64,
"projection_version": "c",
"材料键": "声音:" + 版本,
"材料哈希": "d" * 64,
}
校验声音节(材料) # 冻结身份完整时可用
assert 冻结声音身份(材料) == {k: 材料[k] for k in 身份键}
for 缺 in ("材料哈希", "version_id", "revision", "schema_hash"):
损坏 = {k: v for k, v in 材料.items() if k != 缺}
with pytest.raises(Muse错误):
校验声音节(损坏)
def test_空白样式约束被丢弃__0b66c8():
from muse.审校修订.接口 import 声音提案请求
for 关键字 in ({"verbal_tics": (" ",)}, {"protected_expressions": ("\n",)}):
with pytest.raises(Muse错误, match="空白"):
声音提案请求("synthetic:demo", 0, "voice", 1, {"叙事": "冷峻"}, (_样张(),), **关键字)
def test_声音来源依据不混入写手输入__e83f56():
from pydantic import ValidationError
from muse.上下文.声音材料 import 身份键
# 写手只看到声音内容三键;来源/版本身份只留在冻结节,不进入内容投影。
内容键 = {"narrator", "verbal_tics", "protected_expressions"}
assert not (set(身份键) & 内容键)
from muse.审校修订.接口 import 声音提案请求
# 旧“角色→口痒归属”字段不再成为写手内容的第二通道。
with pytest.raises(ValidationError):
声音提案请求(
"synthetic:demo",
0,
"voice",
1,
{"叙事": "冷峻"},
(_样张(),),
characters={"甲": ["嗯"]},
)
def test_声音草案产物可直接进入确认定位__c4cde7():
from uuid import uuid4
from muse.审校修订.接口 import 声音定位, 声音提案请求
# draft(候选请求)→ confirm(审阅定位)共享作品与基线;提案身份可被审阅定位引用。
请求 = 声音提案请求("synthetic:demo", 0, "voice", 1, {"句式画像": "短句叙述"}, (_样张(),))
定位 = 声音定位("synthetic:demo", str(uuid4()), 1, "a" * 64, 0)
assert 定位.work_id == 请求.work_id
assert 定位.expected_voice_revision == 请求.expected_voice_revision
with pytest.raises(Muse错误):
声音定位("synthetic:demo", "not-uuid", 1, "a" * 64, 0)
def test_完整声音候选经合同通过可提供确认__f1f4ce():
from muse.审校修订.接口 import 声音提案请求
# grounding 输入齐全(叙述者、样张、口癖与保护均有字段)时合同不误拒。
请求 = 声音提案请求(
"synthetic:demo",
0,
"voice",
1,
{"句式画像": "短句叙述", "达标样张": "甲走进门。"},
(_样张(),),
verbal_tics=("没有再说话",),
protected_expressions=("雨落下来。",),
)
assert 请求.narrator["达标样张"] and 请求.verbal_tics and 请求.protected_expressions
assert len(请求.samples) == 1
def test_规则草案拒绝不足四类或单边__6111a8():
from muse.审校修订.接口 import 案例规则请求
引用 = (
_案例("00000000-0000-4000-8000-000000000001", "a" * 64),
_案例("00000000-0000-4000-8000-000000000002", "b" * 64),
)
for 数量 in range(0, 4):
with pytest.raises(Muse错误, match="4至64"):
案例规则请求({"id": "candidate-x"}, 引用[:数量])
def test_额外样例只从已确认案例投影__6b7751():
from muse.审校修订.接口 import 案例规则请求
引用 = tuple(_案例(f"00000000-0000-4000-8000-{i:012d}", chr(97 + i) * 64) for i in range(4))
for 字段 in ("samples", "status", "case_card_ids", "evidence"):
with pytest.raises(Muse错误, match="样例、状态和依据由服务生成"):
案例规则请求({"id": "candidate-x", 字段: {}}, 引用)
def test_已确认声音内容作为写手唯一通道__cb0440():
from pydantic import ValidationError
from muse.审校修订.接口 import 声音提案请求
# 声音内容三键是写手 guidance 的唯一通道;角色级“同时归属”输入被合同拒绝。
with pytest.raises(ValidationError):
声音提案请求(
"synthetic:demo",
0,
"voice",
1,
{"叙事": "冷峻"},
(_样张(),),
角色归属={"甲": ["嗯"]},
)