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