实现侧: - 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。 - 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。 - 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。 - 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。 - 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。 - 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。 - 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。 - 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。 用例侧: - 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存; - 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
248 lines
9.9 KiB
Python
248 lines
9.9 KiB
Python
"""审校、规则与声音的真实作者浏览器旅程,使用隔离PG和合成模型。"""
|
||
|
||
import json
|
||
from dataclasses import replace
|
||
|
||
import pytest
|
||
import test_完整文学审校 as 文学测试
|
||
|
||
from muse.任务运行.接口 import 任务状态
|
||
|
||
pytestmark = pytest.mark.数据库
|
||
文学环境 = 文学测试.文学环境
|
||
生成环境 = 文学测试.生成环境
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-w22-229004",
|
||
environment="隔离PG与合成提供方",
|
||
given="作者审校与声音浏览器往返",
|
||
when="经B06公共用例及真实声明环境执行",
|
||
then=["作者审校与声音浏览器往返"],
|
||
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md",
|
||
)
|
||
@pytest.mark.浏览器
|
||
@pytest.mark.timeout(240)
|
||
def test_作者审校与声音浏览器往返__229004(文学环境, 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.启动 import 装配审校修订
|
||
from muse.接入.http.应用 import 创建应用
|
||
from muse.配置 import 服务配置
|
||
|
||
if os.environ.get("MUSE_RUN_BROWSER") != "1":
|
||
pytest.skip("浏览器驱动需显式启用;不将未执行记为通过")
|
||
环境 = 文学环境
|
||
task_id, pieces = 文学测试.发起(环境)
|
||
环境["剧本"].extend({"类型": "文本", "文本": 文学测试.片结果(p, 有声音=False)} for p in pieces)
|
||
assert 文学测试.跑(环境, task_id).状态 == 任务状态.已完成
|
||
from muse.正文写作.接口 import 文本节点, 正文草稿, 段落
|
||
|
||
环境["正文"].保存人工(
|
||
环境["作者"],
|
||
"review-patch-seed",
|
||
"ch-4",
|
||
0,
|
||
正文草稿((段落("patch-p1", (文本节点("值得注意的是,雨已经停了。"),)),)),
|
||
)
|
||
口令 = 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]
|
||
装配审校修订(环境["装配"], 文学测试.写手测试.合成计价(), root / "配置/流程模板/审校修订.yaml")
|
||
worker_stop = Event()
|
||
worker_errors = []
|
||
|
||
def 运行合成修订():
|
||
from datetime import UTC, datetime, timedelta
|
||
from decimal import Decimal
|
||
|
||
from muse.任务运行.接口 import 任务预算计划, 角色预算, 预算管理, 额度策略
|
||
|
||
run = 环境["装配"].任务运行
|
||
done = set()
|
||
try:
|
||
while not worker_stop.wait(0.1):
|
||
for task in run.列出任务(环境["作者"].作者, 作品ID="gen-work"):
|
||
if task.流程.流程ID != "审校修订" or task.任务ID in done:
|
||
continue
|
||
done.add(task.任务ID)
|
||
budget = 预算管理(环境["库"], "synthetic")
|
||
budget.登记策略(额度策略("synthetic", "1", Decimal("20"), 40))
|
||
budget.登记任务预算(
|
||
task.任务ID,
|
||
任务预算计划(
|
||
Decimal("4"),
|
||
(角色预算("writer", 2, 2, Decimal("1")),),
|
||
"synthetic-browser-approval",
|
||
datetime.now(UTC) + timedelta(minutes=5),
|
||
),
|
||
)
|
||
auth = task.冻结输入["输入"]["修订授权"]
|
||
if task.冻结输入["输入"].get("source_kind") == "selection":
|
||
from pydantic import TypeAdapter
|
||
|
||
from muse.正文写作.接口 import 选段写作授权
|
||
|
||
selected = TypeAdapter(选段写作授权).validate_python(auth)
|
||
edits = [
|
||
{
|
||
"selection_id": s.selection_id,
|
||
"action": "replace",
|
||
"replacement": "细雨已经停了。",
|
||
"reason": "合成通用润色",
|
||
}
|
||
for s in selected.selections
|
||
]
|
||
else:
|
||
edits = [
|
||
{
|
||
"finding_id": auth["approved_findings"][0],
|
||
"action": "replace",
|
||
"replacement": "",
|
||
"reason": "合成删除元话语",
|
||
}
|
||
]
|
||
环境["剧本"].append(
|
||
{
|
||
"类型": "文本",
|
||
"文本": {"edits": edits},
|
||
}
|
||
)
|
||
while run.读取任务(task.任务ID).状态 != 任务状态.已完成:
|
||
claim = run.领取步骤(
|
||
"browser-revision", ["revision.freeze", "revision.generate"]
|
||
)
|
||
assert claim is not None
|
||
run.执行一步(claim)
|
||
except Exception as exc:
|
||
worker_errors.append(exc)
|
||
|
||
worker = Thread(target=运行合成修订, daemon=True)
|
||
输出 = Path(os.environ.get("MUSE_BROWSER_ARTIFACTS", str(tmp_path / "browser"))).resolve()
|
||
输出.mkdir(parents=True, exist_ok=True)
|
||
env = {
|
||
**os.environ,
|
||
"MUSE_WORKBENCH_URL": base,
|
||
"MUSE_AUTHOR_PASSWORD_FILE": str(口令),
|
||
"MUSE_REVIEW_TASK_ID": task_id,
|
||
}
|
||
chrome = Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
|
||
if chrome.exists():
|
||
env.setdefault("MUSE_BROWSER_EXECUTABLE", str(chrome))
|
||
thread.start()
|
||
worker.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")
|
||
assert 声音["revision"] == 1
|
||
assert 声音["narrator"]["句式画像"] == "短句推进,保留动作间停顿"
|
||
assert 环境["正文"].读取正文(环境["审校作者"], "ch-2") == 环境["审校正文"]
|
||
修订候选 = 环境["正文"].列出候选(环境["作者"], "ch-4")
|
||
assert not worker_errors
|
||
assert len(修订候选) == 4 and {c["origin"] for c in 修订候选} == {"author", "model"}
|
||
assert all(c["decision"] == "undecided" for c in 修订候选)
|
||
assert sorted(c["visible_text"] for c in 修订候选) == sorted(
|
||
[
|
||
"雨已经停了。",
|
||
"雨已经停了。",
|
||
"值得注意的是,雨已经停了。屋檐开始滴水。",
|
||
"值得注意的是,细雨已经停了。",
|
||
]
|
||
)
|
||
assert 环境["正文"].读取正文(环境["作者"], "ch-4", 分支="雨后试写")["revision"] == 2
|
||
assert (
|
||
环境["正文"].读取正文(环境["作者"], "ch-4")["visible_text"]
|
||
== "值得注意的是,雨已经停了。"
|
||
)
|
||
if os.environ.get("MUSE_BROWSER_INSPECT") == "1":
|
||
(输出 / "检查入口.json").write_text(json.dumps({"origin": base, "task_id": task_id}))
|
||
import time
|
||
|
||
deadline = time.monotonic() + 150
|
||
while not (输出 / "检查结束").exists() and time.monotonic() < deadline:
|
||
Event().wait(0.25)
|
||
finally:
|
||
worker_stop.set()
|
||
worker.join(10)
|
||
assert not worker.is_alive(), "测试修订worker未停止"
|
||
server.should_exit = True
|
||
thread.join(10)
|
||
sock.close()
|
||
assert not thread.is_alive(), "测试 HTTP 服务未关闭"
|