muse-agent-example/tests/集成/test_两阶段写手.py
zizi b9af240b68 R2 收尾:W30 唯一写入观察与最终验收(C08 章后接通 / 构建身份重建 / 最终备份与恢复演练)
- 章后处理:事实提案命令身份加入类型、批内去重改为先校验后去重(REPEAT_OBJECT_TYPE 记跳过、不再静默丢重复);查询章后状态回传事实跳过;可按需发布/核对可登记任务与 config_not_declared 语义收紧
- 模型治理:推理字符数计量(ChatCompletions/Messages/执行合同)、探针在声明关闭推理却仍有推理时拒绝、计价支持缓存协议与未知模型兜底
- 运行态:资源包重建(发布身份 21d850f7…),三处配置同步,post-extract v5 代次 5 启用,服务 PID 96675(health/ready 通过)
- 门禁:make 检查通过;离线 1008 passed;定点数据库 101 passed;最终完整备份 bkp-1f5591d8… 与空库恢复演练 verified(152 表/158 关系/迁移 45,演练库已 DROP)
- 文档与证据:运行手册、功能覆盖、工作包清单 W30 证据、测试用例清单(17 条 execution_evidence 回填 + 历史死指针说明)、目标文件清单
- 旧实现与旧库直连材料退出(含明文凭据文件移除);新配置 配置/本机正式.toml、配置/本机维护.toml、配置/计费/、配置/运行配置/ 入库,均只含受控引用
- R2 执行证据在 .agents.local/改造/R2-20260909/(不入库)
2026-09-16 08:23:39 +08:00

835 lines
32 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.

"""两阶段写手全链路:真实任务、PG 账本与合成 HTTP 流贯通;不据此宣称真实模型或文学效果。"""
import json
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
from uuid import uuid4
import pytest
from pydantic import TypeAdapter
from muse.上下文.接口 import 上下文错误
from muse.任务运行.接口 import 任务状态
from muse.任务运行.模型 import 内容哈希
from muse.任务运行.配置版本 import (
凭据引用,
提供方配置,
运行配置内容,
配置版本管理,
配置验证证据,
)
from muse.任务运行.预算管理 import (
任务预算计划,
角色预算,
预算管理,
额度策略,
)
from muse.作品规划.接口 import 档案保存, 规划保存, 规划定位
from muse.元数据.接口 import 元数据服务, 导入内置结构
from muse.共享.调用身份 import 内容用途, 用途, 调用身份
from muse.共享.错误 import Muse错误
from muse.启动 import 构建, 装配生成正文
from muse.故事世界.接口 import 事实定位, 事实提案请求, 定位字面引文, 正文依据
from muse.正式变更.接口 import 作者动作, 变更错误
from muse.正文写作.写作任务 import 写作任务错误
from muse.正文写作.接口 import 候选定位, 文本节点, 正文草稿, 段落
from muse.正文写作.正文格式 import 可见文本
from muse.编排.生成正文 import 发起生成正文, 处理器版本集, 探索工具集, 流程版本, 生成正文错误
from muse.配置 import 应用配置
处理器能力 = list(处理器版本集)
pytestmark = pytest.mark.数据库
class 合成计价:
版本 = "synthetic-price-1"
def 金额(self, 结果):
return Decimal("0.125") if 结果.用量 else None
def 细纲内容(parent_id: str, 目标章: int = 3, 钩子: str = "仓库的门后有人应声。") -> dict:
return {
"targetChapter": 目标章,
"sourceRef": {"sourceId": parent_id, "sourceVersion": 1},
"chapterGoal": "邮差在旧码头确认收信人。",
"hardConstraints": ["上一章结尾雨还没停。"],
"keyEvents": [
{
"id": "arrival",
"order": 1,
"event": "邮差到达旧码头",
"participants": ["lin-shen"],
"trigger": "收到退回的信",
"resultDirection": "确认码头还在使用",
}
],
"adjustableBeats": [],
"declaredNewFacts": [],
"mustAppearEntities": ["lin-shen"],
"chapterEndHook": 钩子,
}
@pytest.fixture
def 生成环境(应用测试库, tmp_path):
"""作品、细纲、事实、配置、流程与合成宿主一次装配;剧本由用例注入。"""
库 = 应用测试库[用途.生产]
with 应用测试库[用途.维护].连接() as 连, 连.transaction():
导入内置结构(元数据服务(连))
from muse.任务运行.角色策略 import 角色策略目录
策略 = 角色策略目录.从发布包()
from muse.编排.接口 import 流程登记
登记 = 流程登记()
装配 = 构建(
应用配置(
库.引用,
策略.资源发布身份,
运行用途=库.用途,
原文暂存=str(tmp_path / "raw"),
),
流程=登记,
)
from pathlib import Path
装配生成正文(
装配,
合成计价(),
Path(__file__).parents[2] / "配置" / "流程模板" / "生成正文.yaml",
)
作者 = 调用身份("gen-author", None, 库.用途, 内容用途.生成)
作品, 正文 = 装配.要求作品(), 装配.要求正文()
结构选择 = 作品.新档案结构(作者, "gen-work", "work_core", 1)
作品.保存档案(
作者,
"create-gen-work",
档案保存(
"gen-work", 0, {"名称": "雨码头", "题材定位": "现实", "主题立意": "守约"}, 结构选择
),
)
for i in range(1, 5):
作品.添加章节(作者, f"add-ch-{i}", "gen-work", f"ch-{i}", f"第{i}章", 预期目录版本=i - 1)
正文.保存人工(
作者,
"body-ch-1",
"ch-1",
0,
正文草稿((段落("p-1", (文本节点("林深停在码头,雨还没停。"),)),)),
)
外结构 = 作品.新档案结构(作者, "other-book", "work_core", 1)
作品.保存档案(
作者,
"create-other-book",
档案保存("other-book", 0, {"名称": "别的书", "题材定位": "现实", "主题立意": "无"}, 外结构),
)
作品.添加章节(作者, "add-foreign", "other-book", "foreign", "外章", 预期目录版本=0)
规划 = 装配.要求规划()
def 保存并确认(请求, 命令: str, **关键字) -> dict:
回执 = 规划.保存人工候选(作者, 命令 + "-draft", 请求)
候选 = 规划.读取候选(作者, 回执["results"][0]["candidate_id"])
定位 = 规划定位(
候选["candidate_id"],
候选["revision"],
候选["candidate_hash"],
候选["plan_id"],
请求.expected_revision,
)
审阅 = 规划.打开审阅(作者, 定位)
规划.决定(
作者,
命令 + "-accept",
定位,
作者动作.采纳,
审阅["review_id"],
审阅["review_hash"],
("plan",),
)
return 规划.读取规划(作者, 候选["plan_id"])
世界 = 装配.要求故事世界()
def 正文源(章: str, 引文: str) -> 正文依据:
记录 = 正文.读取正文(作者, 章)
草稿 = TypeAdapter(正文草稿).validate_python(记录["document"])
return 正文依据(
章, "main", 记录["revision"], 记录["document_hash"], (定位字面引文(草稿, 引文),)
)
def 确认事实(对象, 类型, 内容, 来源=()) -> None:
选择 = 世界.新事实结构(作者, "gen-work", 对象, 类型, 1)
回执 = 世界.提出事实(
作者,
"propose-" + 对象,
事实提案请求("gen-work", 对象, 0, 选择, 内容, tuple(来源), "ch-1"),
)
提案 = 世界.读取提案(作者, 回执["results"][0]["proposal_id"])
定位 = 事实定位(
提案["proposal_id"], 提案["revision"], 提案["proposal_hash"], "world:" + 对象, 0
)
审阅 = 世界.打开事实审阅(作者, 定位)
世界.决定事实(
作者,
"confirm-" + 对象,
定位,
作者动作.采纳,
审阅["review_id"],
审阅["review_hash"],
("object",),
)
确认事实("lin-shen", "character", {"名称": "林深"}, (正文源("ch-1", "林深停在码头"),))
父 = 保存并确认(
规划保存(
"gen-work", 0, 规划.新规划结构(作者, "gen-work", "outline", 1), {"名称": "雨码头"}
),
"outline",
)
保存并确认(
规划保存(
"gen-work",
0,
规划.新规划结构(作者, "gen-work", "fine_outline", 1),
细纲内容(父["plan_id"], 目标章=3),
chapter_id="ch-3",
),
"fine-outline-ch3",
)
保存并确认(
规划保存(
"gen-work",
0,
规划.新规划结构(作者, "gen-work", "fine_outline", 1),
细纲内容(父["plan_id"], 目标章=4, 钩子="天亮前灯还亮着。"),
chapter_id="ch-4",
),
"fine-outline-ch4",
)
收到: list[dict] = []
剧本: list = []
def 回应(数据):
收到.append(数据)
动作 = 剧本.pop(0) if 剧本 else {"类型": "空对象"}
if 动作.get("类型") == "失败":
raise RuntimeError("合成宿主按剧本失败")
if 动作.get("类型") == "工具调用":
输出 = [
{
"type": "function_call",
"id": f"fc-{len(收到)}",
"call_id": 动作["调用ID"],
"name": 动作["名称"],
"arguments": json.dumps(动作.get("参数", {}), ensure_ascii=False),
}
]
else:
输出 = [
{
"type": "message",
"content": [
{
"type": "output_text",
"text": json.dumps(动作.get("文本", {}), ensure_ascii=False),
}
],
}
]
载荷 = {
"type": "response.completed",
"response": {
"id": f"gen-response-{len(收到)}",
# 合成宿主按请求回声模型身份;角色策略的实际模型白名单据此核对。
"model": 数据.get("model") or "claude-opus-4-8[1M]",
"status": "completed",
"output": 输出,
"usage": {"input_tokens": 5, "output_tokens": 7},
},
}
return ("data: " + json.dumps(载荷, ensure_ascii=False) + "\n\n").encode()
服务: list = []
def 启动(处理):
class 请求处理(BaseHTTPRequestHandler):
def do_POST(self):
try:
输入 = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
输出 = 处理(输入)
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(输出)))
self.end_headers()
self.wfile.write(输出)
except Exception:
self.send_error(500)
服务.append("失败")
def log_message(self, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), 请求处理)
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
服务.append((server, thread))
return f"http://127.0.0.1:{server.server_port}/v1/responses"
地址 = 启动(回应)
凭据 = tmp_path / "gen-key"
凭据.write_text("synthetic-generation-credential")
原配置 = 运行配置内容(
"direct",
"1",
策略.定义["version"],
策略.资源发布身份,
"synthetic",
{
"writer": {
"provider": "synthetic",
"model": "claude-opus-4-8[1M]",
"thinking": "high",
"stage_tools": {"探索": list(探索工具集), "生成": []},
}
},
(凭据引用("provider-key", "受控存储", str(凭据)),),
(提供方配置("synthetic", "responses", 地址, "provider-key"),),
合成计价().版本,
)
class 离线配置检查:
身份 = "synthetic-generation-validation"
def 验证(self, 内容, 执行用途):
return 配置验证证据(
内容哈希(内容.冻结()),
内容.角色策略版本,
内容.资源发布身份,
执行用途,
("synthetic-generation-contract",),
"runtime",
)
管理 = 配置版本管理(库, 离线配置检查())
管理.保存草案("gen-config", "1", 原配置)
回执 = 管理.验证版本("gen-config", "1")
管理.启用("gen-config", "1", 验证回执=回执, 批准引用="synthetic-approval", 预期代次=0)
环境 = {
"库": 库,
"装配": 装配,
"登记": 登记,
"作者": 作者,
"作品": 作品,
"正文": 正文,
"规划": 规划,
"剧本": 剧本,
"收到": 收到,
"管理": 管理,
"服务": 服务,
"raw": str(tmp_path / "raw"),
}
yield 环境
for 项 in 服务:
if isinstance(项, tuple):
项[0].shutdown()
项[0].server_close()
def 发起并跑完(环境, *, 章位="ch-3", 命令=None, 剧本=None, 冻结后=None):
库, 装配 = 环境["库"], 环境["装配"]
if 剧本:
环境["剧本"].extend(剧本)
回执 = 发起生成正文(
装配,
环境["作者"],
命令 or f"gen-{uuid4()}",
"new_chapter",
work_id="gen-work",
chapter_id=章位,
配置ID="gen-config",
)
任务ID = 回执["task_id"]
预算 = 预算管理(库, "synthetic")
预算.登记策略(额度策略("synthetic", "1", Decimal("20"), 40))
预算.登记任务预算(
任务ID,
任务预算计划(
Decimal("10"),
(角色预算("writer", 8, 8, Decimal("1")),),
"approval",
datetime.now(UTC) + timedelta(minutes=10),
),
)
运行 = 装配.任务运行
while True:
快照 = 运行.读取任务(任务ID)
if 快照.状态 in {任务状态.已完成, 任务状态.已失败, 任务状态.已取消}:
break
领取 = 运行.领取步骤("worker", 处理器能力)
assert 领取 is not None, "任务未完成却没有可领取步骤"
运行.执行一步(领取)
if 领取.步骤ID == "冻结回放":
with 库.连接(只读=True) as 连:
已消费 = 连.execute(
"SELECT COUNT(*) FROM muse_method_usage WHERE task_id=%s",
(任务ID,),
).fetchone()
assert 已消费[0] == 0, "冻结材料不能冒充已完成的模型消费"
if 冻结后 is not None:
冻结后(任务ID)
return 任务ID, 运行.读取任务(任务ID)
def 正常剧本(正文段):
return [
{"类型": "工具调用", "调用ID": "exp-plan", "名称": "query_work_plan", "参数": {}},
{"类型": "工具调用", "调用ID": "exp-facts", "名称": "query_facts_as_of", "参数": {}},
{
"类型": "工具调用",
"调用ID": "exp-hist",
"名称": "read_history_document",
"参数": {"chapter_id": "ch-1", "revision": 1},
},
{"类型": "空对象"},
{"类型": "文本", "文本": {"paragraphs": [{"text": 段} for 段 in 正文段]}},
]
def test_任意合法章位生成模型候选并落独立检查报告__b41e29(生成环境):
from test_上下文快照与索引写入 import 准备已确认消费方法
# 来源、结构与确认均真实;历史启用夹具不代表正式启用或方法效果。
_, 入选版本 = 准备已确认消费方法(
生成环境["装配"],
生成环境["作者"],
"gen-work",
标题="入选消费方法",
原理="用动作展现犹豫,避免替人物解释。",
)
_, 省略版本 = 准备已确认消费方法(
生成环境["装配"],
生成环境["作者"],
"gen-work",
标题="超预算消费方法",
原理="超出上下文预算的材料。" * 10_000,
)
正文段 = [
"雨点敲着铁皮屋顶,林深把退回的信压在掌心。",
"他沿跳板走向仓库,门后传来一声应答。",
]
任务ID, 快照 = 发起并跑完(生成环境, 章位="ch-3", 剧本=正常剧本(正文段))
assert 快照.状态 == 任务状态.已完成, [s for s in 快照.步骤]
环境 = 生成环境
库, 作者 = 环境["库"], 环境["作者"]
# 写作回合禁工具:合成宿主收到的最后一次请求 tools 为空。
写作请求 = 环境["收到"][-1]
assert not 写作请求.get("tools"), "写作回合必须禁工具"
assert "无工具写作" in 写作请求["instructions"]
assert "方法字段仅供规划定位,不应发给写手。" not in 写作请求["input"]
冻结步 = next(s for s in 快照.步骤 if s["step_id"] == "冻结回放")
assert {m["version_id"] for m in 冻结步["checkpoint"]["方法材料"]} == {入选版本}
材料快照 = 环境["装配"].要求上下文().读取快照(冻结步["checkpoint"]["快照ID"])
方法身份 = {条["binding_id"]: 条 for 条 in 材料快照.冻结范围["methods"]}
实绑 = [绑 for 绑 in 材料快照.来源绑定 if 绑.来源ID in 方法身份]
assert {方法身份[绑.来源ID]["version_id"] for 绑 in 实绑} == {入选版本}
assert all(绑.结构哈希 == 方法身份[绑.来源ID]["schema_hash"] for 绑 in 实绑)
assert all(绑.投影版本 == 方法身份[绑.来源ID]["projection_version"] for 绑 in 实绑)
from muse.上下文.接口 import 装配材料
原材料 = {键: 材料快照.装配["文本"][开始:结束] for 键, 开始, 结束 in 材料快照.装配["材料区间"]}
方法键 = 方法身份[实绑[0].来源ID]["材料键"]
# 方法全文仍在总文本中,但放在了错误材料键下,子串检查会误放行。
原材料["目标章细纲"], 原材料[方法键] = 原材料[方法键], 原材料["目标章细纲"]
错配 = 装配材料(原材料, ("目标章细纲",), 字节上限=材料快照.装配["字节数"])
with pytest.raises(上下文错误, match="正文与授权材料不一致"):
环境["装配"].要求上下文().冻结上下文(任务ID, 错配, 材料快照.来源绑定)
with 库.连接(只读=True) as 连:
消费版本 = 连.execute(
"SELECT method_version_id,result_ref FROM muse_method_usage "
"WHERE task_id=%s AND kind='generation'",
(任务ID,),
).fetchall()
assert {str(r[0]) for r in 消费版本} == {入选版本}
写作步 = next(s for s in 快照.步骤 if s["step_id"] == "无工具写作")
assert {r[1] for r in 消费版本} == {"model-call:" + 写作步["checkpoint"]["调用ID"]}
审阅消费 = 连.execute(
"SELECT COUNT(*) FROM muse_method_usage WHERE task_id=%s AND kind='review'",
(任务ID,),
).fetchone()
assert 审阅消费[0] == 0, "未使用方法的确定性报告不得生成审阅消费记录"
from dataclasses import replace
from muse.共享.调用身份 import 用途
from muse.共享.错误 import Muse错误
from muse.知识方法.接口 import 消费输入, 记录消费
方法服务 = 环境["装配"].要求知识方法()
实际消费 = 消费输入(
method_version_id=入选版本,
kind="generation",
context_ref=冻结步["checkpoint"]["快照ID"],
fragment_hash=冻结步["checkpoint"]["方法材料"][0]["材料哈希"],
result_ref="model-call:" + 写作步["checkpoint"]["调用ID"],
task_id=任务ID,
)
assert 方法服务.记录消费(作者, 实际消费, 模型输入=写作请求["input"])["幂等"]
with 库.连接() as 连, pytest.raises(Muse错误) as 跨用途:
记录消费(
连,
作者.作者,
实际消费,
模型输入=写作请求["input"],
执行用途=用途.评测,
)
assert 跨用途.value.错误码 == "USAGE_SCOPE_UNSUPPORTED"
for 假回执, 假输入 in (
(replace(实际消费, task_id=str(uuid4())), 写作请求["input"]),
(replace(实际消费, result_ref="model-call:invented"), 写作请求["input"]),
(replace(实际消费, kind="review"), 写作请求["input"]),
(replace(实际消费, method_version_id=省略版本), 写作请求["input"]),
(replace(实际消费, context_ref=str(uuid4())), 写作请求["input"]),
(replace(实际消费, fragment_hash="0" * 64), 写作请求["input"]),
(实际消费, 写作请求["input"] + "\\n"),
):
with pytest.raises(Muse错误):
方法服务.记录消费(作者, 假回执, 模型输入=假输入)
assert len(方法服务.列出消费(作者, 入选版本)) == 1, "伪造回执不能追加消费"
assert "用动作展现犹豫" in json.dumps(写作请求, ensure_ascii=False)
assert "超出上下文预算的材料" not in json.dumps(写作请求, ensure_ascii=False)
检查步 = next(s for s in 快照.步骤 if s["step_id"] == "检查与候选")
候选ID = 检查步["result"]["candidate_id"]
assert 检查步["result"]["verdict"] == "passed"
with 库.连接(只读=True) as 连:
行 = 连.execute(
"SELECT origin,current_revision FROM muse_writing_candidate WHERE candidate_id=%s",
(候选ID,),
).fetchone()
assert 行[0] == "model" and 行[1] == 1
报告 = 连.execute(
"SELECT verdict,basic,continuity FROM muse_review_report "
"WHERE candidate_id=%s AND candidate_revision=1",
(候选ID,),
).fetchone()
assert 报告[0] == "passed"
assert all(项["判定"] == "passed" for 项 in 报告[1]["checks"])
来源 = 连.execute(
"SELECT task_id,flow_version FROM muse_model_candidate WHERE candidate_id=%s",
(候选ID,),
).fetchone()
assert str(来源[0]) == 任务ID and 来源[1] == 流程版本
事件 = 环境["装配"].任务运行.续接事件(任务ID, 0, 数量=100)
类型序 = [e.类型.value for e in 事件.事件]
assert "text.delta" in 类型序 and "candidate.saved" in 类型序
assert "check.completed" in 类型序
候选 = 环境["正文"].读取候选(作者, 候选ID)
assert 可见文本(TypeAdapter(正文草稿).validate_python(候选["document"])) == "\n".join(正文段)
定位 = 候选定位(
候选ID,
候选["revision"],
候选["candidate_hash"],
候选["document_id"],
候选["current_document_revision"],
)
审阅 = 环境["正文"].打开候选审阅(作者, 定位)
# 第二个合法章位(ch-4,另一细纲)同样可生成。
任务2, 快照2 = 发起并跑完(
生成环境,
章位="ch-4",
剧本=正常剧本(["天快亮了,林深数着灯。", "最后一盏还亮着。"]),
)
assert 快照2.状态 == 任务状态.已完成
检查2 = next(s for s in 快照2.步骤 if s["step_id"] == "检查与候选")
assert 检查2["result"]["candidate_id"] != 候选ID
失效任务集 = []
def 停用入选方法(任务ID):
失效任务集.append(任务ID)
with 库.连接() as 连:
连.execute(
"UPDATE muse_method SET state='disabled' WHERE method_id="
"(SELECT method_id FROM muse_method_version WHERE version_id=%s)",
(入选版本,),
)
# 冻结之后、模型发送之前停用:阻断写作,不能凭旧快照继续消费。
with pytest.raises(生成正文错误, match="冻结来源失效"):
发起并跑完(
生成环境,
剧本=正常剧本(["不应发送给模型的写作结果。"]),
冻结后=停用入选方法,
)
失败任务 = 失效任务集[0]
assert 环境["装配"].任务运行.读取任务(失败任务).状态 == 任务状态.已失败
assert len(环境["剧本"]) == 1, "模型写作请求必须在发送前被阻断"
with pytest.raises(上下文错误, match="采纳时方法依据已失效"):
环境["正文"].决定候选(
作者,
f"adopt-stale-{uuid4()}",
定位,
作者动作.采纳,
审阅["review_id"],
审阅["review_hash"],
("document",),
)
assert 环境["正文"].读取候选(作者, 候选ID)["decision"] == "undecided"
# 恢复测试夹具的历史启用状态,不走也不证明正式启用资格。
with 库.连接() as 连:
连.execute(
"UPDATE muse_method SET state='enabled' WHERE method_id="
"(SELECT method_id FROM muse_method_version WHERE version_id=%s)",
(入选版本,),
)
环境["正文"].决定候选(
作者,
f"adopt-valid-{uuid4()}",
定位,
作者动作.采纳,
审阅["review_id"],
审阅["review_hash"],
("document",),
)
assert 环境["正文"].读取候选(作者, 候选ID)["decision"] == "adopt"
with 库.连接(只读=True) as 连:
assert (
连.execute(
"SELECT COUNT(*) FROM muse_method_usage WHERE task_id=%s", (失败任务,)
).fetchone()[0]
== 0
)
# 带真实方法材料的任务在模型交付失败时不能产生消费。
环境["剧本"].clear()
断流任务 = []
with pytest.raises((Muse错误, RuntimeError, OSError)):
发起并跑完(
生成环境,
章位="ch-4",
冻结后=断流任务.append,
剧本=[
{
"类型": "工具调用",
"调用ID": "exp-plan-fail",
"名称": "query_work_plan",
"参数": {},
},
{"类型": "空对象"},
{"类型": "失败"},
],
)
assert len(断流任务) == 1
with 库.连接(只读=True) as 连:
assert (
连.execute(
"SELECT COUNT(*) FROM muse_method_usage WHERE task_id=%s", (断流任务[0],)
).fetchone()[0]
== 0
)
def test_跨作品与无细纲章位拒绝发起__c73d0f(生成环境):
环境 = 生成环境
# 无细纲章位:授权细纲失败(作品内合法章位但无已确认细纲)。
with pytest.raises(上下文错误):
发起生成正文(
环境["装配"],
环境["作者"],
f"gen-{uuid4()}",
"new_chapter",
work_id="gen-work",
chapter_id="ch-2",
配置ID="gen-config",
)
# 跨作品章位:章位不属于目标作品。
with pytest.raises(写作任务错误, match="不属于目标作品"):
发起生成正文(
环境["装配"],
环境["作者"],
f"gen-{uuid4()}",
"new_chapter",
work_id="gen-work",
chapter_id="foreign",
配置ID="gen-config",
)
def test_必需检查失败不能采纳且保留失败候选证据__d52a8e(生成环境):
# 复读细纲硬约束措辞 → 连续性冲突 → verdict failed。
剧本 = 正常剧本(
[
"上一章结尾雨还没停,林深攥着信封走进雨幕。",
"仓库门后有人应声,他把信递了过去。",
]
)
任务ID, 快照 = 发起并跑完(生成环境, 章位="ch-3", 剧本=剧本)
assert 快照.状态 == 任务状态.已完成
环境 = 生成环境
检查步 = next(s for s in 快照.步骤 if s["step_id"] == "检查与候选")
候选ID = 检查步["result"]["candidate_id"]
assert 检查步["result"]["verdict"] == "failed"
正文服务 = 环境["正文"]
作者 = 环境["作者"]
候选 = 正文服务.读取候选(作者, 候选ID)
定位 = 候选定位(
候选ID,
候选["revision"],
候选["candidate_hash"],
候选["document_id"],
候选["current_document_revision"],
)
审阅 = 正文服务.打开候选审阅(作者, 定位)
with pytest.raises(变更错误) as 异常:
正文服务.决定候选(
作者,
f"adopt-{uuid4()}",
定位,
作者动作.采纳,
审阅["review_id"],
审阅["review_hash"],
("document",),
)
assert 异常.value.错误码 == "CHECK_FAILED"
# 失败候选保留为证据:仍可读取与拒绝。
正文服务.决定候选(
作者,
f"reject-{uuid4()}",
定位,
作者动作.拒绝,
审阅["review_id"],
审阅["review_hash"],
(),
)
assert 正文服务.读取候选(作者, 候选ID)["decision"] == "reject"
def test_无终态断流可定位且重开后任务与事件仍可找回__e6f317(生成环境):
环境 = 生成环境
# 探索后写作回合宿主失败:步骤失败、任务可定位、事件保留、无候选。
剧本 = [
{"类型": "工具调用", "调用ID": "exp-plan", "名称": "query_work_plan", "参数": {}},
{"类型": "空对象"},
{"类型": "失败"},
]
回执 = 发起生成正文(
环境["装配"],
环境["作者"],
f"gen-{uuid4()}",
"new_chapter",
work_id="gen-work",
chapter_id="ch-3",
配置ID="gen-config",
)
任务ID = 回执["task_id"]
预算 = 预算管理(环境["库"], "synthetic")
预算.登记策略(额度策略("synthetic", "1", Decimal("20"), 40))
预算.登记任务预算(
任务ID,
任务预算计划(
Decimal("10"),
(角色预算("writer", 8, 8, Decimal("1")),),
"approval",
datetime.now(UTC) + timedelta(minutes=10),
),
)
环境["剧本"].extend(剧本)
运行 = 环境["装配"].任务运行
with pytest.raises((Muse错误, RuntimeError, OSError)):
while True:
快照 = 运行.读取任务(任务ID)
if 快照.状态 in {任务状态.已完成, 任务状态.已失败, 任务状态.已取消}:
break
领取 = 运行.领取步骤("worker", 处理器能力)
assert 领取 is not None
运行.执行一步(领取)
终态 = 运行.读取任务(任务ID)
# 断流定位:任务未无声消失,写作步骤未完成,后续领取把发出未回的调用标记待对账。
assert 终态.状态 != 任务状态.已完成
写作步 = next(s for s in 终态.步骤 if s["step_id"] == "无工具写作")
assert 写作步["state"] != "completed", 终态.步骤
运行.领取步骤("worker", 处理器能力)
对账态 = 运行.读取任务(任务ID)
assert 对账态.状态 in {
任务状态.待对账,
任务状态.已失败,
任务状态.运行中,
}, 对账态.状态
事件 = 运行.续接事件(任务ID, 0, 数量=100)
assert 事件.事件, "断流后事件必须可回放定位"
with 环境["库"].连接(只读=True) as 连:
数 = 连.execute(
"SELECT count(*) FROM muse_writing_candidate c "
"JOIN muse_model_candidate m USING(candidate_id) WHERE m.task_id=%s",
(任务ID,),
).fetchone()[0]
assert 数 == 0
def test_模型自填无效输出被输出合同拒绝__93c5b7(生成环境):
# 模型试图自填检查结论与额外字段:输出合同 additionalProperties=false 直接拒绝。
剧本 = [
{"类型": "工具调用", "调用ID": "exp-plan", "名称": "query_work_plan", "参数": {}},
{"类型": "空对象"},
{
"类型": "文本",
"文本": {
"paragraphs": [{"text": "林深收好信。"}],
"checks_passed": True,
"author_confirmed": True,
},
},
]
环境 = 生成环境
回执 = 发起生成正文(
环境["装配"],
环境["作者"],
f"gen-{uuid4()}",
"new_chapter",
work_id="gen-work",
chapter_id="ch-3",
配置ID="gen-config",
)
任务ID = 回执["task_id"]
预算 = 预算管理(环境["库"], "synthetic")
预算.登记策略(额度策略("synthetic", "1", Decimal("20"), 40))
预算.登记任务预算(
任务ID,
任务预算计划(
Decimal("10"),
(角色预算("writer", 8, 8, Decimal("1")),),
"approval",
datetime.now(UTC) + timedelta(minutes=10),
),
)
环境["剧本"].extend(剧本)
运行 = 环境["装配"].任务运行
with pytest.raises((Muse错误, RuntimeError, OSError)):
while True:
快照 = 运行.读取任务(任务ID)
if 快照.状态 in {任务状态.已完成, 任务状态.已失败, 任务状态.已取消}:
break
领取 = 运行.领取步骤("worker", 处理器能力)
assert 领取 is not None
运行.执行一步(领取)
终态 = 运行.读取任务(任务ID)
assert any(s["state"] == "failed" for s in 终态.步骤)