muse-agent-example/tests/集成/test_受控规划方法消费.py
zizi d909d1bd1b 后端实现与用例身份:19 包集成落地并修复收尾缺陷
实现侧:
- 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。
- 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。
- 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。
- 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。
- 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。
- 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。
- 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。
- 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。

用例侧:
- 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存;
- 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
2026-09-18 01:15:00 +08:00

612 lines
26 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.

"""真实 S02/隔离 PG/合成宿主的规划链;不表示真实模型或方法效果通过。"""
import json
from dataclasses import replace
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from uuid import uuid4
import pytest
from test_两阶段写手 import 合成计价
from test_两阶段写手 import 生成环境 as 生成环境
from muse.任务运行.接口 import 任务状态, 任务预算计划, 角色预算, 预算管理, 额度策略
from muse.作品规划.接口 import 作品错误, 准备模型规划, 规划保存, 规划定位
from muse.共享.错误 import Muse错误
from muse.启动 import 装配生成规划
from muse.正式变更.接口 import 作者动作
from muse.编排.生成规划 import 发起生成规划, 处理器版本集
pytestmark = pytest.mark.数据库
@pytest.fixture
def 规划环境(生成环境):
环境 = 生成环境
管理 = 环境["管理"]
旧 = 管理.读取版本("gen-config", "1").内容
planner = {**旧.角色配置["writer"], "stage_tools": {"生成": []}}
管理.保存草案("plan-config", "1", replace(旧, 角色配置={"planner": planner}))
回执 = 管理.验证版本("plan-config", "1")
管理.启用("plan-config", "1", 验证回执=回执, 批准引用="synthetic-plan-approval", 预期代次=0)
装配生成规划(环境["装配"], 合成计价())
规划, 作者 = 环境["规划"], 环境["作者"]
请求 = 规划保存(
"gen-work", 1, 规划.新规划结构(作者, "gen-work", "fine_outline", 1), {}, chapter_id="ch-3"
)
with 环境["库"].连接(只读=True) as 连:
准备 = 准备模型规划(连, 作者.作者, 请求)
响应 = dict(准备["visible_base"])
assert "chapterEndHook" in 准备["output_schema"]["properties"]
响应["chapterEndHook"] = "邮差听见灯塔里响起第二次敲击。"
环境.update(规划请求=请求, 规划响应=响应, 规划准备=准备)
return 环境
def _历史方法(环境):
from test_上下文快照与索引写入 import 准备已确认消费方法
return 准备已确认消费方法(
环境["装配"],
环境["作者"],
"gen-work",
标题="动作显影",
原理="用动作表现角色的迟疑,不替角色解释。",
)
def _准备运行(环境):
环境["最后命令"] = f"plan-{uuid4()}"
回执 = 发起生成规划(
环境["装配"],
环境["作者"],
环境["最后命令"],
环境["规划请求"],
"细化本章承接",
配置ID="plan-config",
)
task_id = 回执["task_id"]
预算 = 预算管理(环境["库"], "synthetic")
预算.登记策略(额度策略("synthetic", "1", Decimal("20"), 40))
预算.登记任务预算(
task_id,
任务预算计划(
Decimal("4"),
(角色预算("planner", 2, 2, Decimal("1")),),
"synthetic-planner-budget",
datetime.now(UTC) + timedelta(minutes=10),
),
)
return task_id, 环境["装配"].任务运行
def _运行(环境, *, 内容=None):
task_id, 运行 = _准备运行(环境)
环境["剧本"].append({"类型": "文本", "文本": 内容 if 内容 is not None else 环境["规划响应"]})
while 运行.读取任务(task_id).状态 not in {任务状态.已完成, 任务状态.已失败}:
领取 = 运行.领取步骤("planner-test", list(处理器版本集))
assert 领取 is not None
运行.执行一步(领取)
if 领取.步骤ID == "冻结规划":
with 环境["库"].连接(只读=True) as 连:
assert (
连.execute(
"SELECT COUNT(*) FROM muse_method_usage WHERE task_id=%s", (task_id,)
).fetchone()[0]
== 0
)
return task_id, 运行.读取任务(task_id)
@pytest.mark.case_id(
"NC-w20-20c01a",
environment="隔离数据库",
given="隔离PG、真实任务执行器和合成模型宿主",
when="发起受控规划任务并验证候选及调用链",
then=["实际 planner 请求包含入选方法;有调用证据消费,模型只落候选,作者明确采纳才改正式规划"],
contract="docs/系统架构/新版设计/模块设计/B01-作品规划.md",
)
def test_规划真实方法消费且只产出候选__20c01a(规划环境):
环境 = 规划环境
method_id, version_id = _历史方法(环境)
task_id, 终态 = _运行(环境)
assert 终态.状态 == 任务状态.已完成
assert len(环境["收到"]) == 1 and not 环境["收到"][0].get("tools")
assert "规划师" in 环境["收到"][0]["instructions"]
输入 = json.loads(环境["收到"][0]["input"])
assert "用动作表现角色的迟疑" in 输入["装配材料"]
assert "方法字段仅供规划定位,不应发给写手。" in 输入["装配材料"]
保存 = next(s for s in 终态.步骤 if s["step_id"] == "保存规划候选")["result"]
候选 = 环境["规划"].读取候选(环境["作者"], 保存["candidate_id"])
assert 候选["origin"] == "model" and 候选["model_source"]["task_id"] == task_id
assert 候选["content"]["chapterEndHook"] == 环境["规划响应"]["chapterEndHook"]
assert 环境["规划"].读取规划(环境["作者"], 候选["plan_id"])["revision"] == 1
消费 = 环境["装配"].要求知识方法().列出消费(环境["作者"], version_id)
assert len(消费) == 1 and 消费[0]["kind"] == "planning"
assert 消费[0]["context_ref"] == 输入["上下文快照ID"]
assert 消费[0]["result_ref"] == "model-call:" + 候选["model_source"]["call_id"]
定位 = 规划定位(
候选["candidate_id"], 候选["revision"], 候选["candidate_hash"], 候选["plan_id"], 1
)
审阅 = 环境["规划"].打开审阅(环境["作者"], 定位)
# 模拟历史方法状态变化;不是绕过启用判据的产品入口。
with 环境["库"].连接() as 连:
连.execute("UPDATE muse_method SET state='disabled' WHERE method_id=%s", (method_id,))
with pytest.raises(Muse错误) as 错误:
环境["规划"].决定(
环境["作者"],
f"stale-{uuid4()}",
定位,
作者动作.采纳,
审阅["review_id"],
审阅["review_hash"],
("plan",),
)
assert 错误.value.错误码 == "SOURCE_STALE"
with 环境["库"].连接() as 连:
连.execute("UPDATE muse_method SET state='enabled' WHERE method_id=%s", (method_id,))
环境["规划"].决定(
环境["作者"],
f"adopt-{uuid4()}",
定位,
作者动作.采纳,
审阅["review_id"],
审阅["review_hash"],
("plan",),
)
assert 环境["规划"].读取规划(环境["作者"], 候选["plan_id"])["revision"] == 2
# 正式基线变化后,未知回执重试仍指回首次创建任务,不另冻新输入。
assert (
发起生成规划(
环境["装配"],
环境["作者"],
环境["最后命令"],
环境["规划请求"],
"细化本章承接",
配置ID="plan-config",
)["task_id"]
== task_id
)
with pytest.raises(作品错误, match="不能改换"):
发起生成规划(
环境["装配"],
环境["作者"],
环境["最后命令"],
环境["规划请求"],
"换一个任务意图",
配置ID="plan-config",
)
@pytest.mark.case_id(
"NC-w20-20c02b",
environment="隔离数据库",
given="隔离PG、真实任务执行器和合成模型宿主",
when="发起受控规划任务并验证候选及调用链",
then=["配置缺 planner 时整体拒绝且没有残留任务或模型调用"],
contract="docs/系统架构/新版设计/模块设计/B01-作品规划.md",
)
def test_错误角色配置不留孤儿规划任务__20c02b(规划环境):
环境 = 规划环境
with 环境["库"].连接(只读=True) as 连:
前 = 连.execute("SELECT COUNT(*) FROM muse_task").fetchone()[0]
with pytest.raises(作品错误, match="规划师角色"):
发起生成规划(
环境["装配"],
环境["作者"],
f"bad-plan-{uuid4()}",
环境["规划请求"],
"细化本章",
配置ID="gen-config",
)
with 环境["库"].连接(只读=True) as 连:
assert 连.execute("SELECT COUNT(*) FROM muse_task").fetchone()[0] == 前
assert not 环境["收到"]
for 预算 in ({"context_bytes": 0}, {"最大输出token": True}, {"unknown": 1}):
with pytest.raises(作品错误, match="预算"):
发起生成规划(
环境["装配"],
环境["作者"],
f"bad-budget-{uuid4()}",
环境["规划请求"],
"细化本章",
配置ID="plan-config",
预算=预算,
)
with 环境["库"].连接(只读=True) as 连:
assert 连.execute("SELECT COUNT(*) FROM muse_task").fetchone()[0] == 前
@pytest.mark.case_id(
"NC-w20-20c03c",
environment="隔离数据库",
given="无正式规划的合法作品、隔离PG与受控planner",
when="从本书目录发起首次规划并执行完整任务",
then=["只使用本书目录与授权输入;有模型来源的候选形成,正式规划仍为空"],
contract="docs/系统架构/新版设计/模块设计/B01-作品规划.md",
)
def test_首次规划使用本书目录且不借用其他作品__20c03c(规划环境):
环境 = 规划环境
assert 环境["规划"].列出规划(环境["作者"], "other-book") == []
环境["规划请求"] = 规划保存(
"other-book",
0,
环境["规划"].新规划结构(环境["作者"], "other-book", "outline", 1),
{},
)
task_id, 终态 = _运行(环境, 内容={"名称": "隔岸来信"})
assert 终态.状态 == 任务状态.已完成
载荷 = json.loads(环境["收到"][-1]["input"])
assert "gen-work" not in 载荷["装配材料"] and "other-book" in 载荷["装配材料"]
assert 环境["规划"].列出规划(环境["作者"], "other-book") == []
候选 = 环境["规划"].列出候选(环境["作者"], "other-book")
assert len(候选) == 1 and 候选[0]["content"]["名称"] == "隔岸来信"
assert 候选[0]["model_source"]["task_id"] == task_id
@pytest.mark.case_id(
"NC-w20-20c05e",
environment="隔离PostgreSQL、合成宿主和并发事务",
given="真实规划快照与方法绑定",
when="S02初次核验后、发送事务前停用方法",
then=["不外发,不产生已发送预算或派发授权回执,不登记消费"],
contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md",
)
def test_初次复检后停用仍在发送事务被拦__20c05e(规划环境, monkeypatch):
环境 = 规划环境
method_id, _ = _历史方法(环境)
原预留 = 预算管理.预留
def 预留后停用(self, *args, **kwargs):
结果 = 原预留(self, *args, **kwargs)
with 环境["库"].连接() as 连:
连.execute("UPDATE muse_method SET state='disabled' WHERE method_id=%s", (method_id,))
return 结果
monkeypatch.setattr(预算管理, "预留", 预留后停用)
with pytest.raises(Muse错误, match="变化或停用"):
_运行(环境)
assert 环境["收到"] == []
任务 = 环境["装配"].任务运行.读取命令任务(环境["作者"].作者, 环境["最后命令"])
assert 任务 is not None and 任务.状态 == 任务状态.已失败
with 环境["库"].连接(只读=True) as 连:
assert (
连.execute(
"SELECT COUNT(*) FROM muse_runtime_evidence "
"WHERE task_id=%s AND kind='model_dispatch'",
(任务.任务ID,),
).fetchone()[0]
== 0
)
assert (
连.execute(
"SELECT COUNT(*) FROM muse_budget_reservation "
"WHERE task_id=%s AND sent_at IS NOT NULL",
(任务.任务ID,),
).fetchone()[0]
== 0
)
assert (
连.execute(
"SELECT COUNT(*) FROM muse_method_usage WHERE task_id=%s", (任务.任务ID,)
).fetchone()[0]
== 0
)
@pytest.mark.case_id(
"NC-w20-20c06f",
environment="隔离PostgreSQL、合成宿主和并发事务",
given="合法派发回执和同一方法/绑定/来源",
when="另一事务在发送保护期间停用、释放、撤权或经S04收紧字段策略",
then=["实际被保护事务阻塞;提交后可改变;旧范围不再可发送;已登记历史消费保留"],
contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md",
)
@pytest.mark.parametrize("变化", ["停用", "释放", "撤权", "策略"])
def test_发送保护锁持续到短事务提交而不覆盖历史消费__20c06f(规划环境, 变化):
from concurrent.futures import ThreadPoolExecutor
from dataclasses import asdict
from queue import Queue
from time import monotonic, sleep
from muse.上下文.接口 import 保护模型发送
from muse.元数据.接口 import 元数据服务, 启用命令, 字段限制, 定义哈希
from muse.共享.调用身份 import 用途
环境 = 规划环境
method_id, version_id = _历史方法(环境)
task_id, 终态 = _运行(环境)
assert 终态.状态 == 任务状态.已完成
输入 = 环境["收到"][0]["input"]
快照ID = json.loads(输入)["上下文快照ID"]
库 = 环境["库"]
with 库.连接(只读=True) as 连:
派发 = 连.execute(
"SELECT metadata FROM muse_runtime_evidence WHERE task_id=%s AND kind='model_dispatch'",
(task_id,),
).fetchone()[0]
assert 派发["context_snapshot_id"] == 快照ID and len(派发["context_hash"]) == 64
队列: Queue[int] = Queue()
def 修改依据():
变更库 = 规划环境["库组"][用途.维护] if 变化 == "策略" else 库
with 变更库.连接() as 连:
连.execute("SET statement_timeout='5s'")
队列.put(连.info.backend_pid)
if 变化 == "停用":
连.execute(
"UPDATE muse_method SET state='disabled' WHERE method_id=%s", (method_id,)
)
elif 变化 == "释放":
连.execute(
"UPDATE muse_method_binding SET released_at=clock_timestamp() "
"WHERE method_id=%s",
(method_id,),
)
elif 变化 == "撤权":
连.execute("UPDATE muse_source SET authorized_uses='[]'::jsonb")
else:
元数据 = 元数据服务(连)
字段 = next(f for f in 元数据.读取有效结构("craft", 1).字段 if f.key == "原理")
当前 = 元数据.当前策略("craft")
新 = replace(
当前,
version=当前.version + 1,
限制=(*当前.限制, 字段限制(字段.field_id, ("aiContext:planning",))),
)
元数据.更新策略(
新,
启用命令(
"dispatch-policy",
定义哈希(asdict(新)),
当前.version,
环境["作者"].作者,
"type:craft",
),
)
with ThreadPoolExecutor(max_workers=1) as 池:
with 库.连接() as 连, 连.transaction():
依据 = 保护模型发送(连, 库.用途, 环境["作者"].作者, task_id, 快照ID, 输入)
assert 依据["context_hash"] == 派发["context_hash"]
作业 = 池.submit(修改依据)
后端 = 队列.get(timeout=2)
截止 = monotonic() + 3
while True:
with 库.连接(只读=True) as 观察:
阻塞者 = 观察.execute("SELECT pg_blocking_pids(%s)", (后端,)).fetchone()[0]
if 连.info.backend_pid in 阻塞者:
break
assert monotonic() < 截止 and not 作业.done(), "必须由当前保护事务实际阻塞变更"
sleep(0.01)
作业.result(timeout=3)
with 库.连接() as 连, 连.transaction(), pytest.raises(Muse错误):
保护模型发送(连, 库.用途, 环境["作者"].作者, task_id, 快照ID, 输入)
assert len(环境["装配"].要求知识方法().列出消费(环境["作者"], version_id)) == 1
@pytest.mark.case_id(
"NC-w20-20c07a",
environment="隔离PostgreSQL、合成宿主和并发事务",
given="真实S02合法派发和有效完整交付",
when="完整交付后、登记消费前停用方法",
then=["历史消费登记成功;保存候选与恢复被当前依赖拒绝;不抹历史、不再次外发"],
contract="docs/系统架构/新版设计/模块设计/B04-知识方法.md",
)
def test_交付后停用仍记历史消费但不推进候选与恢复__20c07a(规划环境, monkeypatch):
from muse.任务运行.接口 import 模型执行器
环境 = 规划环境
method_id, version_id = _历史方法(环境)
原确认 = 模型执行器._确认保存
def 交付后停用(self, *args, **kwargs):
原确认(self, *args, **kwargs)
with 环境["库"].连接() as 连:
连.execute("UPDATE muse_method SET state='disabled' WHERE method_id=%s", (method_id,))
monkeypatch.setattr(模型执行器, "_确认保存", 交付后停用)
with pytest.raises(Muse错误):
_运行(环境)
运行 = 环境["装配"].任务运行
任务 = 运行.读取命令任务(环境["作者"].作者, 环境["最后命令"])
assert 任务 is not None and 任务.状态 == 任务状态.已失败
状态 = {s["step_id"]: s["state"] for s in 任务.步骤}
assert 状态["模型规划"] == "completed" and 状态["保存规划候选"] == "failed"
消费 = 环境["装配"].要求知识方法().列出消费(环境["作者"], version_id)
assert len(消费) == 1 and str(消费[0]["task_id"]) == 任务.任务ID
assert len(环境["收到"]) == 1
assert not any(c["origin"] == "model" for c in 环境["规划"].列出候选(环境["作者"], "gen-work"))
with pytest.raises(Muse错误):
运行.控制任务(
任务.任务ID, 环境["作者"].作者, 任务状态.已失败, "恢复", 命令ID="stale-resume"
)
assert len(环境["收到"]) == 1, "旧依赖不能恢复后再外发"
assert len(环境["装配"].要求知识方法().列出消费(环境["作者"], version_id)) == 1
@pytest.mark.case_id(
"NC-w20-20c08b",
environment="隔离PostgreSQL、合成宿主和并发事务",
given="同一冻结快照与同一步骤的两个合法调用",
when="真实派发并完成两次交付,再重复登记并破坏派发证据",
then=["不同调用分别留痕,同调用幂等;缺派发或错快照指纹不能冒充幂等回执"],
contract="docs/系统架构/新版设计/模块设计/B04-知识方法.md",
)
def test_同快照不同合法调用分别留痕且缺派发依据拒绝登记__20c08b(规划环境):
from muse.任务运行.接口 import 执行上下文
from muse.知识方法.接口 import 消费输入, 记录消费
环境 = 规划环境
_, version_id = _历史方法(环境)
task_id, 运行 = _准备运行(环境)
冻结 = 运行.领取步骤("planner-two-calls", list(处理器版本集))
assert 冻结 is not None and 冻结.步骤ID == "冻结规划"
运行.执行一步(冻结)
领取 = 运行.领取步骤("planner-two-calls", list(处理器版本集))
assert 领取 is not None and 领取.步骤ID == "模型规划"
处理器 = 运行.处理器.获取(领取.处理器ID, 领取.处理器版本)
结果 = None
for _ in range(2):
环境["剧本"].append({"类型": "文本", "文本": 环境["规划响应"]})
# 两次均走真实 S02 领取、配置、预算、派发和交付;测试控制步骤完成时机。
结果 = 处理器.执行(执行上下文(领取, 运行.读取任务(task_id)))
assert 结果 is not None
运行.完成步骤(领取, 结果)
assert len(环境["收到"]) == 2
assert 环境["收到"][0]["input"] == 环境["收到"][1]["input"]
服务 = 环境["装配"].要求知识方法()
消费 = 服务.列出消费(环境["作者"], version_id)
assert len(消费) == 2 and len({c["context_ref"] for c in 消费}) == 1
assert len({c["result_ref"] for c in 消费}) == 2
调用ID = 结果.检查点["调用ID"]
行 = next(c for c in 消费 if c["result_ref"] == "model-call:" + 调用ID)
输入 = 消费输入(
version_id,
"planning",
str(行["context_ref"]),
行["fragment_hash"],
"model-call:" + 调用ID,
task_id,
)
模型输入 = 环境["收到"][-1]["input"]
assert 服务.记录消费(环境["作者"], 输入, 模型输入=模型输入)["幂等"]
for 变异 in ("缺派发", "错指纹"):
with 环境["库"].连接() as 连, 连.transaction(force_rollback=True):
if 变异 == "缺派发":
连.execute(
"DELETE FROM muse_runtime_evidence WHERE task_id=%s AND kind='model_dispatch'",
(task_id,),
)
else:
连.execute(
"UPDATE muse_runtime_evidence SET metadata=jsonb_set(metadata, "
"'{context_hash}',to_jsonb(%s::text)) "
"WHERE task_id=%s AND kind='model_dispatch'",
("0" * 64, task_id),
)
with pytest.raises(Muse错误):
记录消费(连, 环境["作者"].作者, 输入, 模型输入=模型输入)
assert len(服务.列出消费(环境["作者"], version_id)) == 2
@pytest.mark.case_id(
"NC-w20-20c04d",
environment="隔离PG、HTTP、真实浏览器和合成模型宿主",
given="已授权隔离环境及预先完成的受控规划交付",
when="浏览器创建新任务并确认已有模型候选",
then=["浏览器真实确认后,PG正式版本与方法消费可回查;仅创建的新任务不产生消费"],
contract="docs/系统架构/新版设计/模块设计/B01-作品规划.md",
)
@pytest.mark.浏览器
def test_规划网页与真实后端往返__20c04d(规划环境, tmp_path):
import os
import secrets
import socket
import subprocess
from contextlib import asynccontextmanager
from pathlib import Path
from threading import Event, Thread
import uvicorn
from muse.接入.http.应用 import 创建应用
from muse.配置 import 服务配置
if os.environ.get("MUSE_RUN_BROWSER") != "1":
pytest.skip("浏览器驱动需显式启用;不将未执行记为通过")
环境 = 规划环境
_, method_version = _历史方法(环境)
task_id, 终态 = _运行(环境)
assert 终态.状态 == 任务状态.已完成
口令 = tmp_path / "browser-password"
口令.write_text(secrets.token_hex(24))
口令.chmod(0o600)
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
base = f"http://127.0.0.1:{port}"
配置 = replace(
环境["装配"].配置,
HTTP=服务配置(
str(口令),
作者ID="gen-author",
端口=port,
公开地址=base,
允许来源=(base,),
),
)
app = 创建应用(配置)
原生命周期 = app.router.lifespan_context
就绪 = Event()
@asynccontextmanager
async def 生命周期(app_):
async with 原生命周期(app_):
app_.state.装配 = 环境["装配"]
就绪.set()
yield
app.router.lifespan_context = 生命周期
server = uvicorn.Server(
uvicorn.Config(app, log_level="warning", access_log=False, timeout_graceful_shutdown=3)
)
thread = Thread(target=server.run, kwargs={"sockets": [sock]}, daemon=True)
root = Path(__file__).resolve().parents[2]
输出 = Path(os.environ.get("MUSE_BROWSER_ARTIFACTS", str(tmp_path / "browser")))
输出.mkdir(parents=True, exist_ok=True)
env = {**os.environ, "MUSE_WORKBENCH_URL": base, "MUSE_AUTHOR_PASSWORD_FILE": str(口令)}
chrome = Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
if chrome.exists():
env.setdefault("MUSE_BROWSER_EXECUTABLE", str(chrome))
thread.start()
try:
assert 就绪.wait(10), "隔离 HTTP 服务未就绪"
报告路径 = 输出.parent / "受控规划-浏览器执行.json"
with 报告路径.open("w") as 报告:
过程 = subprocess.run(
[
"pnpm",
"exec",
"playwright",
"test",
"规划生成.spec.ts",
"--reporter=json",
"--output",
str(输出),
],
cwd=root / "web",
env=env,
stdout=报告,
stderr=subprocess.STDOUT,
timeout=90,
)
assert 过程.returncode == 0, f"浏览器失败,见 {报告路径}"
文件 = list(输出.rglob("规划回执.json"))
assert len(文件) == 1
新任务 = json.loads(文件[0].read_text())["created_task_id"]
assert 新任务 != task_id
with 环境["库"].连接(只读=True) as 连:
assert (
连.execute("SELECT COUNT(*) FROM muse_task WHERE task_id=%s", (新任务,)).fetchone()[
0
]
== 1
)
assert (
连.execute(
"SELECT COUNT(*) FROM muse_method_usage WHERE task_id=%s", (新任务,)
).fetchone()[0]
== 0
)
正式 = 环境["规划"].读取可用细纲(环境["作者"], "gen-work", "ch-3")
assert 正式["revision"] == 2
assert 正式["content"]["chapterEndHook"] == 环境["规划响应"]["chapterEndHook"]
消费 = 环境["装配"].要求知识方法().列出消费(环境["作者"], method_version)
assert len(消费) == 1 and str(消费[0]["task_id"]) == task_id
finally:
server.should_exit = True
thread.join(10)
sock.close()
assert not thread.is_alive(), "测试 HTTP 服务未关闭"