muse-agent-example/tests/集成/test_写手声音消费.py
zizi cf0f4fb985 W20–W24:保存知识方法、迁移框架、审校修订与案例行为的集成成果
接续 88dd570,保存 W20–W24 已实现的共享接口、业务入口、迁移、工作台、测试与文档。
W20/W22/W23 保持 in_progress,W21/W24 保持 verified;此提交不宣称方法或规则正式启用、多轮返修、真实角色评测完成。

W25 新增实验、标定、逐调用交付与角色执行及其迁移/测试/索引留在实施工作树,原有私人和旧实现保留项不纳入。

验证:离线 571、前端 33 通过;PG 469 项通过、2 项浏览器未启用,2 项误带入的 W25 用例已移出本提交;最终任务与交付边界 37 项通过。make 检查、最终类型、84 项资源及 diff 检查通过。未重跑浏览器或 Pi 宿主,不以合成调用认证外部模型效果。
独立整体审查四维通过;证据保存在 R2-20260909/提交W20-W24。
2026-09-13 16:24:42 +08:00

275 lines
12 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.

"""真实确认声音进入写手请求;合成HTTP只验证工程链路,不证明文学收益。"""
import json
from uuid import uuid4
import pytest
import test_两阶段写手 as 写手测试
from test_两阶段写手 import 发起并跑完, 正常剧本
from muse.上下文.接口 import 上下文错误, 保护模型发送, 核对依赖, 装配材料
from muse.任务运行.接口 import 任务状态
from muse.审校修订.接口 import 声音定位, 声音提案请求, 声音样张引用
from muse.正文写作.接口 import 候选定位
pytestmark = pytest.mark.数据库
生成环境 = 写手测试.生成环境
def 确认本书声音(env, *, 确认=True, narrator=None):
服务, 作者 = env["装配"].要求审校(), env["作者"]
当前 = 服务.当前声音(作者, "gen-work")
文 = env["正文"].读取正文(作者, "ch-1")
请求 = 声音提案请求(
"gen-work",
当前["revision"] if 当前 else 0,
"style",
1,
narrator
or {"句式画像": "长短交替,不替人物解释感受。", "达标样张": "林深停在码头,雨还没停。"},
(声音样张引用("document", "ch-1", 1, 文["document_hash"], "main"),),
verbal_tics=("雨还没停",),
protected_expressions=("林深停在码头",),
)
提案 = 服务.提出声音(作者, "voice-" + str(uuid4()), 请求)["results"][0]
定位 = 声音定位(
"gen-work", 提案["proposal_id"], 1, 提案["proposal_hash"], 请求.expected_voice_revision
)
if 确认:
审阅 = 服务.打开声音审阅(作者, 定位)
服务.决定声音(
作者,
"voice-adopt-" + str(uuid4()),
定位,
审阅ID=审阅["review_id"],
审阅哈希=审阅["review_hash"],
)
return 定位
def 读取材料(env, task):
点 = next(s for s in task.步骤 if s["step_id"] == "冻结回放")["checkpoint"]
return env["装配"].要求上下文().读取快照(点["快照ID"])
def test_实际写作请求含真实声音及版本且不能错放材料__22d001(生成环境):
env = 生成环境
确认本书声音(env)
tid, task = 发起并跑完(env, 剧本=正常剧本(["林深停在码头,门后有人应声。"]))
assert task.状态 == 任务状态.已完成, task.步骤
快照 = 读取材料(env, task)
声音 = 快照.冻结范围["voice"]
assert 声音["revision"] == 1 and 声音["schema_hash"]
assert any(
b.来源ID == 声音["version_id"] and b.投影版本 == 声音["projection_version"]
for b in 快照.来源绑定
)
原材料 = {k: 快照.装配["文本"][a:b] for k, a, b in 快照.装配["材料区间"]}
声键 = 声音["材料键"]
指导 = json.loads(原材料[声键])
assert 指导["narrator"]["句式画像"] == "长短交替,不替人物解释感受。"
assert 指导["protected_expressions"] == ["林深停在码头"]
assert "samples" not in 指导
assert json.loads(env["收到"][-1]["input"])["装配材料"] == 快照.装配["文本"]
原材料["目标章细纲"], 原材料[声键] = 原材料[声键], 原材料["目标章细纲"]
with pytest.raises(上下文错误, match="声音"):
env["装配"].要求上下文().冻结上下文(
tid, 装配材料(原材料, ("目标章细纲", 声键), 字节上限=快照.装配["字节数"]), 快照.来源绑定
)
def test_未确认声音不进入写手且空基线也固定__22d002(生成环境):
env = 生成环境
确认本书声音(env, 确认=False)
_, task = 发起并跑完(env, 剧本=正常剧本(["林深停在码头,门后有人应声。"]))
assert task.状态 == 任务状态.已完成
快照 = 读取材料(env, task)
assert "voice" in 快照.冻结范围 and 快照.冻结范围["voice"] is None
assert all(not k.startswith("声音:") for k in 快照.装配["入选"])
assert "长短交替,不替人物解释感受。" not in env["收到"][-1]["input"]
@pytest.mark.parametrize("已有", [True, False])
def test_冻结后声音变化阻写作发送并标旧快照失效__22d003(生成环境, 已有):
env = 生成环境
if 已有:
确认本书声音(env)
任务ID = []
def 换声音(tid):
任务ID.append(tid)
确认本书声音(env, narrator={"句式画像": "作者改为更克制的指导。"})
from muse.编排.生成正文 import 生成正文错误
with pytest.raises(生成正文错误, match="声音"):
发起并跑完(env, 剧本=正常剧本(["林深停在码头。"]), 冻结后=换声音)
tid = 任务ID[0]
task = env["装配"].任务运行.读取任务(tid)
assert task.状态 == 任务状态.已失败
assert len(env["收到"]) == 4, "声音失效不能再发送写作回合"
快照 = 读取材料(env, task)
assert 快照.当前状态 == "SOURCE_STALE" and "声音" in 快照.失效说明
with env["库"].连接(只读=True) as 连:
状态 = 核对依赖(连, 快照.冻结范围)
assert not 状态.有效 and any("声音" in x for x in 状态.变化项)
with env["库"].连接() as 连, 连.transaction(), pytest.raises(上下文错误, match="声音"):
保护模型发送(
连,
env["库"].用途,
env["作者"].作者,
tid,
快照.快照ID,
json.dumps({"上下文快照ID": 快照.快照ID, "装配材料": 快照.装配["文本"]}),
)
def test_交付后声音变化不篡改历史但拒旧候选采纳__22d004(生成环境):
env = 生成环境
确认本书声音(env)
_, task = 发起并跑完(env, 剧本=正常剧本(["林深停在码头,门后有人应声。"]))
assert task.状态 == 任务状态.已完成
快照 = 读取材料(env, task)
保存 = next(s for s in task.步骤 if s["step_id"] == "检查与候选")["checkpoint"]["候选"]
c = env["正文"].读取候选(env["作者"], 保存["candidate_id"])
定位 = 候选定位(c["candidate_id"], c["revision"], c["candidate_hash"], c["document_id"], 0)
审阅 = env["正文"].打开候选审阅(env["作者"], 定位)
确认本书声音(env, narrator={"句式画像": "改变后的指导。"})
from muse.正式变更.接口 import 作者动作
with pytest.raises(上下文错误, match="声音"):
env["正文"].决定候选(
env["作者"],
"adopt-stale-voice",
定位,
作者动作.采纳,
审阅["review_id"],
审阅["review_hash"],
("document",),
)
后 = env["装配"].要求上下文().读取快照(快照.快照ID)
assert 后.装配 == 快照.装配 and 后.来源绑定 == 快照.来源绑定
assert env["正文"].读取正文(env["作者"], "ch-3")["revision"] == 0
def test_声音投影随任务用途且策略收紧阻旧发送__22d005(生成环境, 应用测试库):
from dataclasses import asdict, replace
from muse.上下文.接口 import 核对范围声音
from muse.元数据.接口 import 元数据服务, 启用命令, 字段限制, 定义哈希
from muse.共享.调用身份 import 内容用途, 用途
env = 生成环境
确认本书声音(env)
上下文 = env["装配"].要求上下文()
旧 = 上下文.授权任务范围(env["作者"], "gen-work", 2, 含目录=True, 含声音=True)
with 应用测试库[用途.维护].连接() as 连, 连.transaction():
元数据 = 元数据服务(连)
当前 = 元数据.当前策略("style")
新 = replace(
当前,
version=当前.version + 1,
限制=(*当前.限制, 字段限制("style.句式画像", ("aiContext:generation",))),
)
元数据.更新策略(
新,
启用命令(
"voice-restrict", 定义哈希(asdict(新)), 当前.version, env["作者"].作者, "type:style"
),
)
with env["库"].连接(只读=True) as 连, pytest.raises(上下文错误, match="声音"):
核对范围声音(连, 旧)
服务 = env["装配"].要求审校()
新声 = 服务.当前声音(env["作者"], "gen-work", 字段用途="aiContext")
检测声 = 服务.当前声音(
replace(env["作者"], 内容用途=内容用途.检测), "gen-work", 字段用途="aiContext"
)
assert "句式画像" not in 新声["narrator"] and "句式画像" in 检测声["narrator"]
assert 新声["projection_version"] != 检测声["projection_version"]
assert 新声["content_hash"] == 检测声["content_hash"] == 旧["voice"]["content_hash"]
assert 新声["revision"] == 1
def test_声音必读不能被预算省略__22d006(生成环境):
from muse.编排.生成正文 import 发起生成正文
env = 生成环境
确认本书声音(env, narrator={"句式画像": "超出本轮预算的声音指导。" * 10000})
env["剧本"].extend(正常剧本(["不应发送的正文"]))
回执 = 发起生成正文(
env["装配"],
env["作者"],
"voice-over-budget",
"new_chapter",
work_id="gen-work",
chapter_id="ch-3",
配置ID="gen-config",
)
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from muse.任务运行.预算管理 import 任务预算计划, 角色预算, 预算管理, 额度策略
预算 = 预算管理(env["库"], "synthetic")
预算.登记策略(额度策略("synthetic", "1", Decimal("20"), 40))
预算.登记任务预算(
回执["task_id"],
任务预算计划(
Decimal("10"),
(角色预算("writer", 8, 8, Decimal("1")),),
"approval",
datetime.now(UTC) + timedelta(minutes=10),
),
)
运行 = env["装配"].任务运行
with pytest.raises(上下文错误) as 错误:
while True:
领取 = 运行.领取步骤("worker", 写手测试.处理器能力)
assert 领取 is not None
运行.执行一步(领取)
assert 错误.value.错误码 == "CONTEXT_BUDGET_EXCEEDED"
assert len(env["收到"]) == 4
assert env["装配"].要求上下文().读取任务快照(回执["task_id"]) == ()
def test_首次声音确认与发送保护同锁且两确认只有一个成功__22d007(生成环境):
from concurrent.futures import ThreadPoolExecutor
from threading import Barrier
from psycopg.errors import LockNotAvailable
from muse.共享.错误 import Muse错误
from muse.审校修订.接口 import 读取确认声音
env = 生成环境
# null也锁定本书,另一个事务不能绕过首次确认的排序。
with env["库"].连接() as 一, 一.transaction():
assert 读取确认声音(一, env["作者"].作者, "gen-work", 保护到事务结束=True) is None
with env["库"].连接() as 二, 二.transaction(), pytest.raises(LockNotAvailable):
二.execute("SET LOCAL lock_timeout = '150ms'")
读取确认声音(二, env["作者"].作者, "gen-work", 保护到事务结束=True)
服务 = env["装配"].要求审校()
定位 = [确认本书声音(env, 确认=False) for _ in range(2)]
审阅 = [服务.打开声音审阅(env["作者"], 条) for 条 in 定位]
栅栏 = Barrier(2)
def 采纳(i):
栅栏.wait(timeout=5)
try:
return 服务.决定声音(
env["作者"],
"voice-concurrent-" + str(i),
定位[i],
审阅ID=审阅[i]["review_id"],
审阅哈希=审阅[i]["review_hash"],
)
except Muse错误 as e:
return e
with ThreadPoolExecutor(2) as 池:
结果 = list(池.map(采纳, range(2)))
assert sum(isinstance(r, dict) for r in 结果) == 1
assert sum(isinstance(r, Muse错误) for r in 结果) == 1
assert 服务.当前声音(env["作者"], "gen-work")["revision"] == 1