643 lines
23 KiB
Python
643 lines
23 KiB
Python
"""两阶段写手全链路:真实任务、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": "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):
|
||
库, 装配 = 环境["库"], 环境["装配"]
|
||
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, "任务未完成却没有可领取步骤"
|
||
运行.执行一步(领取)
|
||
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(生成环境):
|
||
正文段 = [
|
||
"雨点敲着铁皮屋顶,林深把退回的信压在掌心。",
|
||
"他沿跳板走向仓库,门后传来一声应答。",
|
||
]
|
||
任务ID, 快照 = 发起并跑完(生成环境, 章位="ch-3", 剧本=正常剧本(正文段))
|
||
assert 快照.状态 == 任务状态.已完成, [s for s in 快照.步骤]
|
||
环境 = 生成环境
|
||
库, 作者 = 环境["库"], 环境["作者"]
|
||
|
||
# 写作回合禁工具:合成宿主收到的最后一次请求 tools 为空。
|
||
写作请求 = 环境["收到"][-1]
|
||
assert not 写作请求.get("tools"), "写作回合必须禁工具"
|
||
assert "无工具写作" in 写作请求["instructions"]
|
||
|
||
检查步 = 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(正文段)
|
||
|
||
# 第二个合法章位(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 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 终态.步骤)
|