实现侧: - 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。 - 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。 - 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。 - 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。 - 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。 - 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。 - 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。 - 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。 用例侧: - 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存; - 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
683 lines
25 KiB
Python
683 lines
25 KiB
Python
"""原生 pytest 的用例身份、进程隔离与共享夹具。
|
||
|
||
默认使用 OS 防护覆盖收集、驱动及子进程。外部环境必须显式选择;
|
||
完整 ID 来自测试源码 marker,参数行和 JUnit 保留可追溯身份。
|
||
临时项目连同工具/用例身份.py和数据库支持插件验证同一入口。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import ctypes
|
||
import ctypes.util
|
||
import errno
|
||
import json
|
||
import socket
|
||
import sys
|
||
from decimal import Decimal
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
import pytest
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
pytest_plugins = ["支持.数据库夹具"]
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 标记与用例选择
|
||
# ---------------------------------------------------------------------------
|
||
|
||
外部标记 = ("数据库", "网络", "真实模型", "浏览器", "宿主")
|
||
|
||
# 执行计数:供会话结束守卫判断“无实际执行”
|
||
_执行计数 = {"passed": 0, "skipped": 0, "failed": 0}
|
||
|
||
# 验证闸门:会话持共享锁并在起止各核一次源码身份,防止“跑着的时候改源码”
|
||
_验证闸门: dict[str, Any] = {"栈": None, "开始哈希": None}
|
||
|
||
|
||
def 准备验证闸门(session: pytest.Session) -> None:
|
||
"""采集前持锁;外部环境层再核对源码身份,不一致立即失败,不等到几百个用例报假错。"""
|
||
from contextlib import ExitStack
|
||
|
||
from 验证锁 import 持有, 源码身份核对, 登记说明
|
||
|
||
# 离线层按用例显式注入源码资源,不依赖已安装资源身份;只有外部环境层才需要同身份。
|
||
if session.config.getoption("--外部环境"):
|
||
问题 = 源码身份核对()
|
||
if 问题:
|
||
raise pytest.UsageError(问题)
|
||
from muse.基础设施.系统运行 import 运行代码哈希
|
||
|
||
_验证闸门["开始哈希"] = 运行代码哈希()
|
||
_验证闸门["栈"] = ExitStack()
|
||
import os
|
||
|
||
说明 = f"pytest {' '.join(session.config.args) or '全部'}(pid {os.getpid()})"
|
||
登记说明(说明)
|
||
_验证闸门["栈"].enter_context(持有(说明))
|
||
|
||
|
||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||
组 = parser.getgroup("muse", "Muse 用例选择")
|
||
组.addoption("--导出用例", default=None, help="将本次收集的用例身份导出 JSON")
|
||
组.addoption(
|
||
"--外部环境", action="store_true", help="显式允许外部用例;仍按标记选择并使用隔离配置"
|
||
)
|
||
组.addoption(
|
||
"--case",
|
||
action="append",
|
||
default=None,
|
||
metavar="CASE_ID",
|
||
help="按稳定用例 ID(TC-/NC- 前缀)选择;可多次给出,语义为并集",
|
||
)
|
||
|
||
|
||
def pytest_configure(config: pytest.Config) -> None:
|
||
config.addinivalue_line("markers", "安装包: 真实发布产物验收")
|
||
config.addinivalue_line("markers", "case_id(id): 稳定用例身份")
|
||
for 名 in 外部标记:
|
||
config.addinivalue_line("markers", f"{名}: 外部环境用例;见 tests/conftest.py")
|
||
|
||
|
||
@pytest.hookimpl(tryfirst=True)
|
||
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
|
||
工具根 = config.rootpath / "工具"
|
||
sys.path.insert(0, str(工具根))
|
||
from 用例身份 import 导出用例, 选择用例
|
||
|
||
导出 = config.getoption("--导出用例")
|
||
if 导出:
|
||
Path(导出).write_text(
|
||
json.dumps(
|
||
{"schema_version": 2, "cases": 导出用例(items, config.rootpath)},
|
||
ensure_ascii=False,
|
||
indent=2,
|
||
)
|
||
+ "\n",
|
||
encoding="utf-8",
|
||
)
|
||
选择用例(config, items)
|
||
|
||
|
||
def pytest_sessionstart(session: pytest.Session) -> None:
|
||
"""整个离线进程使用 OS 防护,收集、libpq 与子进程共用相同边界。"""
|
||
准备验证闸门(session)
|
||
if session.config.getoption("--外部环境"):
|
||
return
|
||
if sys.platform == "darwin":
|
||
库 = ctypes.CDLL("/usr/lib/libsandbox.dylib")
|
||
初始化 = 库.sandbox_init
|
||
初始化.argtypes = [ctypes.c_char_p, ctypes.c_uint64, ctypes.POINTER(ctypes.c_char_p)]
|
||
初始化.restype = ctypes.c_int
|
||
错误 = ctypes.c_char_p()
|
||
if 初始化(b"(version 1)(allow default)(deny network*)", 0, ctypes.byref(错误)):
|
||
raise pytest.UsageError("操作系统离线隔离初始化失败")
|
||
elif sys.platform.startswith("linux"):
|
||
路径 = ctypes.util.find_library("seccomp")
|
||
if not 路径:
|
||
raise pytest.UsageError("离线测试需要系统 libseccomp;不能无防护运行")
|
||
库 = ctypes.CDLL(路径)
|
||
库.seccomp_init.argtypes = [ctypes.c_uint32]
|
||
库.seccomp_init.restype = ctypes.c_void_p
|
||
库.seccomp_syscall_resolve_name.argtypes = [ctypes.c_char_p]
|
||
库.seccomp_rule_add.argtypes = [
|
||
ctypes.c_void_p,
|
||
ctypes.c_uint32,
|
||
ctypes.c_int,
|
||
ctypes.c_uint,
|
||
]
|
||
库.seccomp_load.argtypes = [ctypes.c_void_p]
|
||
库.seccomp_release.argtypes = [ctypes.c_void_p]
|
||
上下文 = 库.seccomp_init(0x7FFF0000)
|
||
if not 上下文:
|
||
raise pytest.UsageError("无法创建离线系统调用规则")
|
||
try:
|
||
# 禁止建立网络 socket 和所有外部 connect;保留事件循环的内部 socketpair。
|
||
class 参数条件(ctypes.Structure):
|
||
_fields_ = [
|
||
("arg", ctypes.c_uint),
|
||
("op", ctypes.c_int),
|
||
("datum_a", ctypes.c_uint64),
|
||
("datum_b", ctypes.c_uint64),
|
||
]
|
||
|
||
库.seccomp_rule_add_array.argtypes = [
|
||
ctypes.c_void_p,
|
||
ctypes.c_uint32,
|
||
ctypes.c_int,
|
||
ctypes.c_uint,
|
||
ctypes.POINTER(参数条件),
|
||
]
|
||
拒绝 = 0x00050000 | errno.EPERM
|
||
connect = 库.seccomp_syscall_resolve_name(b"connect")
|
||
socket_call = 库.seccomp_syscall_resolve_name(b"socket")
|
||
if 库.seccomp_rule_add(上下文, 拒绝, connect, 0):
|
||
raise pytest.UsageError("无法登记离线连接规则")
|
||
for family in (socket.AF_INET, socket.AF_INET6):
|
||
条件 = 参数条件(0, 4, family, 0) # SCMP_CMP_EQ
|
||
if 库.seccomp_rule_add_array(上下文, 拒绝, socket_call, 1, ctypes.byref(条件)):
|
||
raise pytest.UsageError("无法登记离线网络规则")
|
||
if 库.seccomp_load(上下文):
|
||
raise pytest.UsageError("操作系统离线隔离初始化失败")
|
||
finally:
|
||
库.seccomp_release(上下文)
|
||
else:
|
||
raise pytest.UsageError("当前系统尚无离线进程隔离实现;使用受支持的 macOS 或 Linux 环境")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 离线防护:未显式选择外部环境的测试禁止一切 socket 连接
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class 离线连接被拒(RuntimeError):
|
||
"""离线测试尝试建立网络或数据库连接。"""
|
||
|
||
|
||
原始connect: Any = socket.socket.connect
|
||
原始connect_ex: Any = socket.socket.connect_ex
|
||
|
||
|
||
def _拒绝连接(self: socket.socket, address: Any) -> Any:
|
||
raise 离线连接被拒(
|
||
f"离线测试尝试连接 {address!r};需要外部环境时显式标记 {'/'.join(外部标记)} 并使用对应夹具"
|
||
)
|
||
|
||
|
||
def _安装防护() -> None:
|
||
socket.socket.connect = _拒绝连接 # type: ignore[method-assign]
|
||
socket.socket.connect_ex = _拒绝连接 # type: ignore[method-assign]
|
||
|
||
|
||
def _卸载防护() -> None:
|
||
socket.socket.connect = 原始connect # type: ignore[method-assign]
|
||
socket.socket.connect_ex = 原始connect_ex # type: ignore[method-assign]
|
||
|
||
|
||
def _需要外部环境(项: pytest.Item) -> bool:
|
||
标记 = {m.name for m in 项.iter_markers()}
|
||
return any(名 in 标记 for 名 in 外部标记)
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def 离线防护(request: pytest.FixtureRequest):
|
||
"""默认给所有未选择外部环境的用例安装 socket 防护;用例结束后恢复。"""
|
||
if _需要外部环境(request.node):
|
||
yield
|
||
return
|
||
_安装防护()
|
||
try:
|
||
yield
|
||
finally:
|
||
_卸载防护()
|
||
|
||
|
||
def pytest_runtest_logreport(report: pytest.TestReport) -> None:
|
||
if report.when == "call":
|
||
if report.passed:
|
||
_执行计数["passed"] += 1
|
||
elif report.failed:
|
||
_执行计数["failed"] += 1
|
||
elif report.skipped:
|
||
_执行计数["skipped"] += 1
|
||
elif report.when == "setup" and report.skipped:
|
||
_执行计数["skipped"] += 1
|
||
|
||
|
||
def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
|
||
"""全跳过不算通过;运行期间源码被改写时,本轮结论作废。"""
|
||
if _验证闸门["开始哈希"] is not None:
|
||
from muse.基础设施.系统运行 import 运行代码哈希
|
||
|
||
if 运行代码哈希() != _验证闸门["开始哈希"]:
|
||
session.exitstatus = 3
|
||
print(
|
||
"\n守卫:本轮运行期间源码被改写,验证结论作废(退出码 3);"
|
||
"改完请重新 make 生成 并整轮重跑",
|
||
file=sys.stderr,
|
||
)
|
||
if _验证闸门["栈"] is not None:
|
||
_验证闸门["栈"].close()
|
||
_验证闸门["栈"] = None
|
||
if exitstatus == 0 and _执行计数["passed"] == 0 and _执行计数["skipped"] > 0:
|
||
session.exitstatus = 5
|
||
print(
|
||
f"\n守卫:{_执行计数['skipped']} 个用例全部跳过、零实际执行;不计通过(退出码 5)",
|
||
file=sys.stderr,
|
||
)
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def 源码资源():
|
||
"""开发层只编译登记源到内存;不会调用生成入口或写入发布产物。"""
|
||
工具根 = Path(__file__).resolve().parents[1] / "工具"
|
||
if not (工具根 / "源码资源.py").is_file():
|
||
return None # 独立临时pytest项目没有应用资源
|
||
sys.path.insert(0, str(工具根))
|
||
from 源码资源 import 源码资源提供器
|
||
|
||
return 源码资源提供器(工具根.parent)
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def 测试资源接缝(request, 源码资源):
|
||
"""源码测试显式使用开发视图;安装包层始终核验真正安装产物。"""
|
||
if 源码资源 is None or request.node.get_closest_marker("安装包") or _需要外部环境(request.node):
|
||
yield
|
||
return
|
||
from muse.资源加载 import 使用资源提供器
|
||
|
||
with 使用资源提供器(源码资源):
|
||
yield
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 外部环境夹具
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.fixture
|
||
def 夹具根() -> Path:
|
||
"""tests/夹具 的绝对路径;供用例读取稳定回归输入。"""
|
||
根 = Path(__file__).resolve().parent / "夹具"
|
||
assert 根.is_dir(), f"夹具目录不存在:{根}"
|
||
return 根
|
||
|
||
|
||
# ---------------------------------------------------------------- 章后派生共享环境
|
||
|
||
|
||
class 合成派生计价:
|
||
版本 = "synthetic-price-1"
|
||
|
||
def 金额(self, 结果):
|
||
return Decimal("0.125") if 结果.用量 else None
|
||
|
||
|
||
class _离线派生配置检查:
|
||
身份 = "synthetic-derivation-validation"
|
||
|
||
def 验证(self, 内容, 执行用途):
|
||
from muse.任务运行.模型 import 内容哈希
|
||
from muse.任务运行.配置版本 import 配置验证证据
|
||
|
||
return 配置验证证据(
|
||
内容哈希(内容.冻结()),
|
||
内容.角色策略版本,
|
||
内容.资源发布身份,
|
||
执行用途,
|
||
("synthetic-derivation-contract",),
|
||
"runtime",
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def 章后环境(内置结构测试库, tmp_path):
|
||
"""作品、章后流程、抽取配置与合成宿主一次装配;抽取剧本由用例注入。"""
|
||
|
||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||
from threading import Thread
|
||
|
||
from muse.任务运行.角色策略 import 角色策略目录
|
||
from muse.任务运行.配置版本 import (
|
||
凭据引用,
|
||
提供方配置,
|
||
运行配置内容,
|
||
配置版本管理,
|
||
)
|
||
from muse.任务运行.预算管理 import 预算管理, 额度策略
|
||
from muse.作品规划.接口 import 档案保存
|
||
from muse.共享.调用身份 import 内容用途, 用途, 调用身份
|
||
from muse.启动 import 构建, 装配章后处理
|
||
from muse.配置 import 应用配置
|
||
|
||
应用测试库 = 内置结构测试库
|
||
库 = 应用测试库[用途.生产]
|
||
策略 = 角色策略目录.从发布包()
|
||
装配 = 构建(
|
||
应用配置(
|
||
库.引用,
|
||
策略.资源发布身份,
|
||
运行用途=库.用途,
|
||
原文暂存=str(tmp_path / "raw"),
|
||
)
|
||
)
|
||
with 装配.生命周期():
|
||
装配章后处理(装配, 合成派生计价(), 抽取配置ID="post-config")
|
||
|
||
收到: list[dict] = []
|
||
剧本: list = []
|
||
|
||
def 回应(数据):
|
||
收到.append(数据)
|
||
动作 = 剧本.pop(0) if 剧本 else {"类型": "空对象"}
|
||
if 动作.get("类型") == "失败":
|
||
raise RuntimeError("合成宿主按剧本失败")
|
||
输出 = [
|
||
{
|
||
"type": "message",
|
||
"content": [
|
||
{
|
||
"type": "output_text",
|
||
"text": json.dumps(动作.get("文本", {}), ensure_ascii=False),
|
||
}
|
||
],
|
||
}
|
||
]
|
||
载荷 = {
|
||
"type": "response.completed",
|
||
"response": {
|
||
"id": f"post-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()
|
||
|
||
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)
|
||
|
||
def log_message(self, *args):
|
||
pass
|
||
|
||
from muse.编排.生成正文 import 处理器版本集 as 生成处理器集
|
||
from muse.编排.章后处理 import 处理器版本集 as 章后处理器集
|
||
|
||
server = ThreadingHTTPServer(("127.0.0.1", 0), 请求处理)
|
||
thread = Thread(target=server.serve_forever, daemon=True)
|
||
thread.start()
|
||
|
||
凭据 = tmp_path / "post-key"
|
||
凭据.write_text("synthetic-derivation-credential")
|
||
原配置 = 运行配置内容(
|
||
"direct",
|
||
"1",
|
||
策略.定义["version"],
|
||
策略.资源发布身份,
|
||
"synthetic",
|
||
{
|
||
"extractor": {
|
||
"provider": "synthetic",
|
||
"model": "claude-opus-4-8[1M]",
|
||
"thinking": "high",
|
||
"stage_tools": {"抽取": []},
|
||
}
|
||
},
|
||
(凭据引用("provider-key", "受控存储", str(凭据)),),
|
||
(
|
||
提供方配置(
|
||
"synthetic",
|
||
"responses",
|
||
f"http://127.0.0.1:{server.server_port}/v1/responses",
|
||
"provider-key",
|
||
),
|
||
),
|
||
合成派生计价().版本,
|
||
)
|
||
管理 = 配置版本管理(库, _离线派生配置检查())
|
||
管理.保存草案("post-config", "1", 原配置)
|
||
启用回执 = 管理.验证版本("post-config", "1")
|
||
管理.启用("post-config", "1", 验证回执=启用回执, 批准引用="synthetic-approval", 预期代次=0)
|
||
预算 = 预算管理(库, "synthetic")
|
||
预算.登记策略(额度策略("synthetic", "1", Decimal("20"), 40))
|
||
|
||
作者 = 调用身份("post-author", None, 库.用途, 内容用途.抽取)
|
||
作品 = 装配.要求作品()
|
||
|
||
def 新作品(work_id: str, 章数: int = 2, 章前缀: str = "ch-") -> None:
|
||
结构选择 = 作品.新档案结构(作者, work_id, "work_core", 1)
|
||
作品.保存档案(
|
||
作者,
|
||
f"create-{work_id}",
|
||
档案保存(
|
||
work_id,
|
||
0,
|
||
{"名称": work_id, "题材定位": "现实", "主题立意": "守约"},
|
||
结构选择,
|
||
),
|
||
)
|
||
for i in range(1, 章数 + 1):
|
||
作品.添加章节(
|
||
作者,
|
||
f"add-{work_id}-{章前缀}{i}",
|
||
work_id,
|
||
f"{章前缀}{i}",
|
||
f"第{i}章",
|
||
预期目录版本=i - 1,
|
||
)
|
||
|
||
新作品("post-work", 3)
|
||
|
||
环境 = {
|
||
"库": 库,
|
||
# 同库三用途工厂:依赖本环境的夹具必须复用同一库,另借新库会读到空作品。
|
||
"库组": 应用测试库,
|
||
"装配": 装配,
|
||
"作者": 作者,
|
||
"剧本": 剧本,
|
||
"收到": 收到,
|
||
"新作品": 新作品,
|
||
"提供方端口": server.server_port,
|
||
"处理器能力": sorted(set(生成处理器集) | set(章后处理器集)),
|
||
}
|
||
yield 环境
|
||
server.shutdown()
|
||
server.server_close()
|
||
thread.join(timeout=5)
|
||
|
||
|
||
@pytest.fixture
|
||
def 章后工具(章后环境):
|
||
"""章后旅程的具名操作:保存一章、驱动任务、构造抽取剧本。"""
|
||
|
||
from types import SimpleNamespace
|
||
from uuid import uuid4
|
||
|
||
from muse.任务运行.接口 import 任务状态
|
||
from muse.正文写作.接口 import 文本节点, 正文草稿, 段落
|
||
|
||
环境 = 章后环境
|
||
|
||
def 保存一章(章: str, 文本: str, 命令: str | None = None, 预期版本: int = 0):
|
||
回执 = (
|
||
环境["装配"]
|
||
.要求正文()
|
||
.保存人工(
|
||
环境["作者"],
|
||
命令 or f"save-{章}-{uuid4().hex[:6]}",
|
||
章,
|
||
预期版本,
|
||
正文草稿((段落(f"{章}-p1", (文本节点(文本),)),)),
|
||
)
|
||
)
|
||
来源 = next(
|
||
(r for r in 回执["results"] if isinstance(r, dict) and r.get("章后任务")),
|
||
None,
|
||
)
|
||
assert 来源 is not None, f"章后任务未登记:{回执['results']}"
|
||
return 来源["章后任务"], 回执
|
||
|
||
def 跑任务(任务ID: str):
|
||
运行 = 环境["装配"].任务运行
|
||
while True:
|
||
快照 = 运行.读取任务(任务ID)
|
||
if 快照.状态 in {任务状态.已完成, 任务状态.已失败, 任务状态.已取消}:
|
||
return 快照
|
||
领取 = 运行.领取步骤("worker", 环境["处理器能力"])
|
||
assert 领取 is not None, "任务未完成却没有可领取步骤"
|
||
运行.执行一步(领取)
|
||
|
||
def 抽取输出(摘要: str, 事实: list[dict]) -> dict:
|
||
return {"类型": "文本", "文本": {"summary": 摘要, "facts": 事实}}
|
||
|
||
return SimpleNamespace(保存一章=保存一章, 跑任务=跑任务, 抽取输出=抽取输出, 环境=环境)
|
||
|
||
|
||
@pytest.fixture
|
||
def 研究环境(章后环境, tmp_path):
|
||
"""研究拆书环境:analyst 配置、研究流程装配与带授权的参考来源。"""
|
||
|
||
from decimal import Decimal
|
||
from types import SimpleNamespace
|
||
from uuid import uuid4
|
||
|
||
from muse.任务运行.配置版本 import 凭据引用, 提供方配置, 运行配置内容, 配置版本管理
|
||
from muse.任务运行.预算管理 import 预算管理, 额度策略
|
||
from muse.启动 import 装配研究拆书
|
||
from muse.编排.研究拆书 import 处理器版本集 as 研究处理器集
|
||
from muse.资料研究.接口 import 资料服务
|
||
from muse.资料研究.模型 import 导入请求
|
||
|
||
环境 = 章后环境
|
||
环境["处理器能力"] = sorted(set(环境["处理器能力"]) | set(研究处理器集))
|
||
库, 装配 = 环境["库"], 环境["装配"]
|
||
策略 = __import__("muse.任务运行.角色策略", fromlist=["角色策略目录"]).角色策略目录.从发布包()
|
||
|
||
装配研究拆书(装配, 合成派生计价(), 分析配置ID="research-config")
|
||
|
||
凭据 = tmp_path / "research-key"
|
||
凭据.write_text("synthetic-research-credential")
|
||
凭据.chmod(0o600)
|
||
研究配置 = 运行配置内容(
|
||
"direct",
|
||
"1",
|
||
策略.定义["version"],
|
||
策略.资源发布身份,
|
||
"synthetic",
|
||
{
|
||
"extractor": {
|
||
"provider": "synthetic",
|
||
"model": "claude-opus-4-8[1M]",
|
||
"thinking": "high",
|
||
"stage_tools": {"抽取": [], "拆书分析": []},
|
||
},
|
||
},
|
||
(凭据引用("provider-key", "受控存储", str(凭据)),),
|
||
(
|
||
提供方配置(
|
||
"synthetic",
|
||
"responses",
|
||
f"http://127.0.0.1:{环境['提供方端口']}/v1/responses",
|
||
"provider-key",
|
||
),
|
||
),
|
||
合成派生计价().版本,
|
||
)
|
||
管理 = 配置版本管理(库, _离线派生配置检查())
|
||
管理.保存草案("research-config", "1", 研究配置)
|
||
启用回执 = 管理.验证版本("research-config", "1")
|
||
管理.启用("research-config", "1", 验证回执=启用回执, 批准引用="synthetic-approval", 预期代次=0)
|
||
预算 = 预算管理(库, "synthetic-research")
|
||
预算.登记策略(额度策略("synthetic-research", "1", Decimal("40"), 80))
|
||
|
||
服务 = 资料服务()
|
||
|
||
def 导入来源(标题: str, 正文: str, 用途集: tuple[str, ...] = ("analysis",)) -> dict:
|
||
with 库.连接() as 连, 连.transaction():
|
||
回执 = 服务.导入(
|
||
连,
|
||
环境["作者"].作者,
|
||
导入请求("reference", 标题, f"file://资料/{标题}.txt", 正文, 用途集),
|
||
)
|
||
return {
|
||
"source_id": 回执.source_id,
|
||
"revision": 回执.revision,
|
||
"content_hash": 回执.content_hash,
|
||
}
|
||
|
||
def 发起(
|
||
source_id: str, *, 配置ID: str = "research-config", 重跑窗=None, 理由=None, 窗口章数=None
|
||
):
|
||
from muse.编排.研究拆书 import 发起研究拆书
|
||
|
||
样本 = {"理由": 理由} if 理由 else None
|
||
return 发起研究拆书(
|
||
装配,
|
||
环境["作者"],
|
||
"research-" + uuid4().hex[:8],
|
||
source_id=source_id,
|
||
配置ID=配置ID,
|
||
重跑窗=重跑窗,
|
||
样本依据=样本,
|
||
窗口章数=窗口章数,
|
||
)
|
||
|
||
def 跑任务(任务ID: str):
|
||
from muse.任务运行.接口 import 任务状态
|
||
|
||
运行 = 装配.任务运行
|
||
能力 = sorted(set(环境["处理器能力"]) | set(研究处理器集))
|
||
while True:
|
||
快照 = 运行.读取任务(任务ID)
|
||
if 快照.状态 in {任务状态.已完成, 任务状态.已失败, 任务状态.已取消}:
|
||
return 快照
|
||
领取 = 运行.领取步骤("worker", 能力)
|
||
assert 领取 is not None, "任务未完成却没有可领取步骤"
|
||
运行.执行一步(领取)
|
||
|
||
def 分析输出(实体: list[dict], 写法=("三段式推进",)) -> dict:
|
||
return {
|
||
"类型": "文本",
|
||
"文本": {
|
||
"chapters": [{"title": "章", "beat": "节拍"}],
|
||
"phases": ["推进"],
|
||
"entities": 实体,
|
||
"techniques": list(写法),
|
||
},
|
||
}
|
||
|
||
return SimpleNamespace(
|
||
环境=环境,
|
||
库=库,
|
||
装配=装配,
|
||
剧本=环境["剧本"],
|
||
服务=服务,
|
||
导入来源=导入来源,
|
||
发起=发起,
|
||
跑任务=跑任务,
|
||
分析输出=分析输出,
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def 新空目标库(空测试库):
|
||
from types import SimpleNamespace
|
||
|
||
from psycopg.conninfo import conninfo_to_dict
|
||
|
||
from muse.共享.调用身份 import 用途
|
||
from muse.基础设施.数据库.连接 import 解析连接串
|
||
|
||
工厂 = 空测试库[用途.维护]
|
||
库名 = conninfo_to_dict(解析连接串(工厂.引用))["dbname"]
|
||
yield SimpleNamespace(库名=库名, 工厂=工厂)
|
||
|
||
|
||
@pytest.fixture
|
||
def 源码CLI():
|
||
"""子进程运行真实 CLI,入口显式装配同一源码资源视图。"""
|
||
根 = Path(__file__).resolve().parents[1]
|
||
脚本 = (
|
||
"import runpy,sys; from pathlib import Path; "
|
||
f"sys.path.insert(0, {str(根 / '工具')!r}); "
|
||
"from 源码资源 import 源码资源提供器; "
|
||
"from muse.资源加载 import 使用资源提供器; "
|
||
f"provider=源码资源提供器(Path({str(根)!r})); "
|
||
"sys.argv=['muse',*sys.argv[1:]]; "
|
||
"context=使用资源提供器(provider); context.__enter__(); "
|
||
"runpy.run_module('muse',run_name='__main__')"
|
||
)
|
||
return [sys.executable, "-c", 脚本]
|