实现侧: - 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。 - 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。 - 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。 - 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。 - 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。 - 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。 - 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。 - 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。 用例侧: - 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存; - 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
261 lines
9.5 KiB
Python
261 lines
9.5 KiB
Python
"""轮子安装到独立环境,真实服务停机重启后候选和导出任务仍可继续。"""
|
||
|
||
import json
|
||
import os
|
||
import signal
|
||
import socket
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
from dataclasses import asdict
|
||
from pathlib import Path
|
||
from zipfile import ZipFile
|
||
|
||
import httpx
|
||
import pytest
|
||
import test_定稿与导出 as 定稿测试
|
||
|
||
from muse.资源加载 import 加载清单
|
||
|
||
pytestmark = [pytest.mark.数据库, pytest.mark.timeout(120)]
|
||
候选环境 = 定稿测试.候选环境
|
||
交付环境 = 定稿测试.交付环境
|
||
根 = Path(__file__).resolve().parents[2]
|
||
|
||
|
||
def _命令(args, cwd, *, env=None):
|
||
result = subprocess.run(args, cwd=cwd, env=env, capture_output=True, text=True, timeout=60)
|
||
assert result.returncode == 0, result.stderr[-3000:]
|
||
return result.stdout
|
||
|
||
|
||
def _启动(python, config, root, base, environment):
|
||
log = (root / "server.log").open("ab")
|
||
process = subprocess.Popen(
|
||
[str(python), "-I", "-m", "muse", "服务", str(config)],
|
||
cwd=root,
|
||
env=environment,
|
||
stdout=log,
|
||
stderr=subprocess.STDOUT,
|
||
)
|
||
try:
|
||
for _ in range(60):
|
||
if process.poll() is not None:
|
||
pytest.fail("安装包服务提前退出:" + (root / "server.log").read_text()[-2000:])
|
||
try:
|
||
if httpx.get(base + "/ready", timeout=1, trust_env=False).status_code == 200:
|
||
return process, log
|
||
except httpx.TransportError:
|
||
pass
|
||
time.sleep(0.1)
|
||
pytest.fail("安装包服务未就绪")
|
||
except BaseException:
|
||
_停止(process, log)
|
||
raise
|
||
|
||
|
||
def _停止(process, log):
|
||
if process.poll() is None:
|
||
process.terminate()
|
||
try:
|
||
process.wait(10)
|
||
except subprocess.TimeoutExpired:
|
||
process.kill()
|
||
process.wait(5)
|
||
pytest.fail("安装包服务未能优雅停止")
|
||
log.close()
|
||
|
||
|
||
@pytest.fixture
|
||
def 独立安装环境(tmp_path):
|
||
package = tmp_path / "packages"
|
||
_命令(["uv", "build", "--offline", "--wheel", "--out-dir", str(package)], 根)
|
||
wheel = next(package.glob("*.whl"))
|
||
with ZipFile(wheel) as archive:
|
||
names = archive.namelist()
|
||
assert len(names) == len(set(names))
|
||
assert not any(
|
||
part in {".git", "tests", ".agents.local", ".env"}
|
||
for name in names
|
||
for part in Path(name).parts
|
||
)
|
||
migrations = [n for n in names if n.startswith("muse/资源/迁移/") and n.endswith(".sql")]
|
||
assert len(migrations) == len(list((根 / "数据库/迁移").glob("*.sql")))
|
||
assert (
|
||
archive.read("muse/资源/迁移/V0001__共享标识与版本.sql")
|
||
== (根 / "数据库/迁移/V0001__共享标识与版本.sql").read_bytes()
|
||
)
|
||
requirements = tmp_path / "runtime.txt"
|
||
_命令(
|
||
[
|
||
"uv",
|
||
"export",
|
||
"--frozen",
|
||
"--no-dev",
|
||
"--no-emit-project",
|
||
"--output-file",
|
||
str(requirements),
|
||
],
|
||
根,
|
||
)
|
||
venv = tmp_path / "installed"
|
||
_命令(["uv", "venv", "--python", sys.executable, str(venv)], tmp_path)
|
||
python = venv / "bin/python"
|
||
_命令(
|
||
[
|
||
"uv",
|
||
"pip",
|
||
"install",
|
||
"--offline",
|
||
"--python",
|
||
str(python),
|
||
"--require-hashes",
|
||
"-r",
|
||
str(requirements),
|
||
],
|
||
tmp_path,
|
||
)
|
||
_命令(
|
||
["uv", "pip", "install", "--offline", "--no-deps", "--python", str(python), str(wheel)],
|
||
tmp_path,
|
||
)
|
||
environment = {
|
||
k: v for k, v in os.environ.items() if not k.startswith("MUSE_") and k != "PYTHONPATH"
|
||
}
|
||
where = _命令(
|
||
[str(python), "-I", "-c", "import muse; print(muse.__file__)"], tmp_path, env=environment
|
||
).strip()
|
||
assert Path(where).is_relative_to(venv)
|
||
return python, environment
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-w28-28a001",
|
||
environment="隔离PG",
|
||
given="明确隔离配置、发布包和受控测试资料",
|
||
when="经实际入口执行对应操作并读回",
|
||
then=["无Git安装包实际写候选且重启继续原导出任务"],
|
||
contract="docs/系统架构/新版设计/文件设计/工程配置与部署.md",
|
||
)
|
||
def test_无Git安装包实际写候选且重启继续原导出任务__28a001(交付环境, tmp_path, 独立安装环境):
|
||
app, author, pools, request = 交付环境
|
||
python, environment = 独立安装环境
|
||
with socket.socket() as sock:
|
||
sock.bind(("127.0.0.1", 0))
|
||
port = sock.getsockname()[1]
|
||
base = f"http://127.0.0.1:{port}"
|
||
password = tmp_path / "author-secret"
|
||
password.write_text("synthetic-installed-only")
|
||
password.chmod(0o600)
|
||
config = tmp_path / "installed.toml"
|
||
config.write_text(
|
||
'["数据库"]\n"取值方式"="受控存储"\n"位置"='
|
||
+ json.dumps(pools[author.用途].引用.位置)
|
||
+ '\n["资源"]\n"发布身份"='
|
||
+ json.dumps(加载清单()["构建身份"])
|
||
+ '\n["HTTP"]\n"口令文件"='
|
||
+ json.dumps(str(password))
|
||
+ '\n"作者ID"='
|
||
+ json.dumps(author.作者)
|
||
+ '\n"端口"='
|
||
+ str(port)
|
||
+ '\n"公开地址"='
|
||
+ json.dumps(base)
|
||
+ '\n"允许来源"=['
|
||
+ json.dumps(base)
|
||
+ "]\n",
|
||
)
|
||
config.chmod(0o600)
|
||
assert not (tmp_path / ".git").exists()
|
||
process, log = _启动(python, config, tmp_path, base, environment)
|
||
client = httpx.Client(base_url=base, headers={"origin": base}, trust_env=False, timeout=15)
|
||
try:
|
||
assert (
|
||
client.post("/api/v1/session", json={"password": password.read_text()}).status_code
|
||
== 200
|
||
)
|
||
response = client.post(
|
||
"/api/v1/chapters/chapter-candidate/writing-candidates",
|
||
json={
|
||
"command_id": "installed-candidate",
|
||
"chapter_id": "chapter-candidate",
|
||
"branch_id": "main",
|
||
"expected_revision": 1,
|
||
"document": asdict(定稿测试.正文测试.草稿("安装环境的新候选🙂")),
|
||
},
|
||
)
|
||
assert response.status_code == 200, response.text
|
||
cid = response.json()["results"][0]["candidate_id"]
|
||
original = client.get("/api/v1/writing-candidates/" + cid).json()
|
||
frozen = client.post(
|
||
"/api/v1/deliveries",
|
||
json={
|
||
"command_id": "installed-freeze",
|
||
"delivery": asdict(request),
|
||
},
|
||
)
|
||
assert frozen.status_code == 200, frozen.text
|
||
path = "/api/v1/deliveries/" + request.delivery_id
|
||
queued = client.post(
|
||
path + "/exports",
|
||
json={
|
||
"command_id": "installed-export",
|
||
"formats": ["txt", "docx"],
|
||
},
|
||
)
|
||
assert queued.status_code == 200, queued.text
|
||
tid = queued.json()["task_id"]
|
||
_停止(process, log)
|
||
# Uvicorn完成优雅关闭后会重发原SIGTERM,子进程返回-SIGTERM是正常终态。
|
||
assert process.returncode in {0, -signal.SIGTERM}
|
||
assert "Application shutdown complete." in (tmp_path / "server.log").read_text()
|
||
process, log = _启动(python, config, tmp_path, base, environment)
|
||
restored = client.get("/api/v1/writing-candidates/" + cid)
|
||
assert restored.status_code == 200 # 原会话口令和cookie仍然有效。
|
||
assert restored.json()["candidate_hash"] == original["candidate_hash"]
|
||
assert restored.json()["visible_text"] == original["visible_text"]
|
||
assert restored.json()["decision"] == "undecided"
|
||
finished = client.post(path + "/exports/" + tid + "/advance")
|
||
assert finished.status_code == 200, finished.text
|
||
assert finished.json()["state"] == "completed"
|
||
downloaded = client.get(path + "/files/txt")
|
||
assert downloaded.status_code == 200
|
||
assert "原句「保留」" in downloaded.text
|
||
assert "安装环境的新候选" not in downloaded.text
|
||
html = client.get("/")
|
||
assert html.status_code == 200 and "/assets/index-" in html.text
|
||
assert app.要求正文().读取正文(author, "chapter-candidate")["revision"] == 1
|
||
if os.environ.get("MUSE_RUN_BROWSER") == "1":
|
||
output = Path(os.environ.get("MUSE_BROWSER_ARTIFACTS", str(tmp_path / "browser")))
|
||
output.mkdir(parents=True, exist_ok=True)
|
||
browser_env = {
|
||
**os.environ,
|
||
"MUSE_WORKBENCH_URL": base,
|
||
"MUSE_AUTHOR_PASSWORD_FILE": str(password),
|
||
"MUSE_BROWSER_EXECUTABLE": (
|
||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||
),
|
||
}
|
||
with (output / "浏览器.json").open("w") as log_file:
|
||
result = subprocess.run(
|
||
[
|
||
"pnpm",
|
||
"exec",
|
||
"playwright",
|
||
"test",
|
||
"系统管理.spec.ts",
|
||
"--reporter=json",
|
||
"--output",
|
||
str(output / "产物"),
|
||
],
|
||
cwd=根 / "web",
|
||
env=browser_env,
|
||
stdout=log_file,
|
||
stderr=subprocess.STDOUT,
|
||
timeout=90,
|
||
)
|
||
assert result.returncode == 0, str(output / "浏览器.json")
|
||
finally:
|
||
client.close()
|
||
_停止(process, log)
|