muse-agent-example/tests/集成/test_受控规划方法消费.py
zizi faef454ac7 refactor(提示词): 批次2身份与术语统一——身份句唯一归角色层,JF-03销项
- 删除4份登记模板身份句(生成正文/生成作品规划/细化章节计划/创作质量审阅);
  装配后身份句只来自角色层,消除双自我介绍
- 正典名统一规划员:角色合同 displayName 与章节标题两处文字(红线收窄豁免,
  .agent/角色/规划员.md 不动);生成规划.py 错误文案与受控规划测试断言同步
- A3(JF-03,用户已授权):删除未登记孤儿模板 核对实体歧义.md;研究命令与
  「检查与维护拆书结果」技能话术改为"歧义只登记,由作者人工判定",人工核对
  步骤与合同承接进技能;实体归并.py 话术核实已合规无需改
- C4:创作质量审阅英文状态词首现处补中文对照(insufficient_evidence/ask/
  uncalibrated/reviewed/repair/retain/judgment);continuity/craft/voice 已有对照
- D2:生成正文.md"戏剧化叙述"并入可操作判据(用动作、对白与细节呈现事件,
  不复述或概括事件),写手模板净增0行
- docs:B03-资料研究.md 同步(拆书分析改资源模板加载、新增模板行、删孤儿行、
  备注更新);专项检查台账收口节记录 JF-03 处置;历史目标文档按先例保留原时点

证据:全仓 grep 规划师/核对实体歧义 活跃区零残留;离线全量1153项通过;
受影响数据库集成45项通过(受控规划/完整文学审校/两阶段写手/写手声音/章后/拆书);
资源包已重建,索引检查通过。
2026-09-19 22:08:16 +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 服务未关闭"