- 删除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项通过(受控规划/完整文学审校/两阶段写手/写手声音/章后/拆书); 资源包已重建,索引检查通过。
612 lines
26 KiB
Python
612 lines
26 KiB
Python
"""真实 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 服务未关闭"
|