"""真实作者HTTP与CLI对同一返修会话读写,不替代运行装配或真实模型验收。""" import json import os import socket import subprocess import time from dataclasses import asdict, replace from pathlib import Path from threading import Thread import httpx import pytest import test_持久返修会话 as 返修测试 import uvicorn from fastapi.testclient import TestClient from muse.接入.cli.审校命令 import 运行审校命令 from muse.接入.http.应用 import 创建应用 from muse.资源加载 import 当前资源提供器 from muse.配置 import 服务配置 pytestmark = pytest.mark.数据库 生成环境 = 返修测试.生成环境 修订环境 = 返修测试.修订环境 @pytest.mark.case_id( "NC-w23-236011", environment="隔离PG", given="合成资料、真实S02配置与明确作者授权", when="通过实际返修会话和作者入口执行并重新读取", then=["作者HTTP创建返修与CLI读回选择重放同一会话"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) def test_作者HTTP创建返修与CLI读回选择重放同一会话__236011(修订环境, tmp_path): env = 修订环境 secret = tmp_path / "password" secret.write_text("synthetic-session-author") secret.chmod(0o600) config = replace( env["装配"].配置, HTTP=服务配置( str(secret), 作者ID=env["作者"].作者, 公开地址="http://testserver", 允许来源=("http://testserver",), ), ) env["装配"].配置 = config payload = { "command_id": "create-session-http", "request": asdict(env["授权"]), "config_id": "gen-config", "max_rounds": 2, } path = tmp_path / "request.json" path.write_text(json.dumps(payload)) app = 创建应用(config) with TestClient(app, headers={"origin": "http://testserver"}) as client: app.state.装配 = env["装配"] assert client.post("/api/v1/revision-sessions", json=payload).status_code == 401 assert ( client.post("/api/v1/session", json={"password": secret.read_text()}).status_code == 200 ) response = client.post("/api/v1/revision-sessions", json=payload) assert response.status_code == 201, response.text face = response.json() sid = face["session_id"] assert 运行审校命令(env["装配"], "有限返修", str(path)) == face tid = face["rounds"][0]["task_id"] 返修测试.批准轮预算(env, tid) env["剧本"].append({"类型": "文本", "文本": 返修测试.候选输出(env)}) 返修测试.修订测试.跑(env, tid) face = client.get("/api/v1/revision-sessions/" + sid).json() assert face["state"] == "awaiting_author" assert face == 运行审校命令(env["装配"], "返修会话", sid) choice = { "command_id": "keep-original-http", "session_id": sid, "expected_round": 1, "candidate_id": face["rounds"][0]["candidate_id"], "choice": "original", } selected = client.post(f"/api/v1/revision-sessions/{sid}/decisions", json=choice) assert selected.status_code == 200, selected.text path.write_text(json.dumps(choice)) assert 运行审校命令(env["装配"], "返修选择", str(path)) == selected.json() assert selected.json()["state"] == "closed" assert selected.json()["stop_reason"] == "author_selected_original" assert len(env["收到"]) == 1 assert env["正文"].读取正文(env["作者"], "ch-4") == env["原稿"] @pytest.mark.case_id( "NC-w23-236012", environment="隔离PG/显式浏览器", given="合成资料、真实S02配置与明确作者授权", when="通过实际返修会话和作者入口执行并重新读取", then=["返修原文选择经真实浏览器保存刷新及owner复核"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) @pytest.mark.浏览器 def test_返修原文选择经真实浏览器保存刷新及owner复核__236012(修订环境, tmp_path): if os.environ.get("MUSE_RUN_BROWSER") != "1": pytest.skip("需显式启用浏览器") env = 修订环境 face = 返修测试.发起并产出候选(env, command="browser-revision", max_rounds=2) sid = face["session_id"] with socket.socket() as sock: sock.bind(("127.0.0.1", 0)) port = sock.getsockname()[1] base = f"http://127.0.0.1:{port}" secret = tmp_path / "password" secret.write_text("synthetic-browser-revision") secret.chmod(0o600) cfg = replace( env["装配"].配置, HTTP=服务配置( str(secret), 作者ID=env["作者"].作者, 公开地址=base, 允许来源=(base,), ), ) env["装配"].配置 = cfg app = 创建应用(cfg) app.state.装配 = env["装配"] # lifespan 关掉后要自己补上请求中间件要用的资源提供器,否则 /ready 与业务路由都拿不到。 app.state.资源提供器 = 当前资源提供器() # 本例注入合成HTTP/计价的真实业务装配;默认安装装配由独立安装用例验证。 server = uvicorn.Server( uvicorn.Config(app, host="127.0.0.1", port=port, lifespan="off", log_level="warning") ) thread = Thread(target=server.run, daemon=True) thread.start() output = Path(os.environ.get("MUSE_BROWSER_ARTIFACTS", str(tmp_path / "browser"))) output.mkdir(parents=True, exist_ok=True) try: for _ in range(100): if server.started: break time.sleep(0.05) assert server.started assert httpx.get(base + "/ready", trust_env=False).status_code == 200 target = ( f"/works/gen-work/review?chapter=ch-4&diagnosis={env['授权'].diagnosis_id}" f"&revision_session={sid}" ) environment = { **os.environ, "MUSE_WORKBENCH_URL": base, "MUSE_AUTHOR_PASSWORD_FILE": str(secret), "MUSE_REVISION_SESSION_URL": target, "MUSE_BROWSER_EXECUTABLE": ( "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ), } with (output / "浏览器.json").open("w") as report: result = subprocess.run( [ "pnpm", "exec", "playwright", "test", "有限返修.spec.ts", "--reporter=json", "--output", str(output / "产物"), ], cwd=Path(__file__).resolve().parents[2] / "web", env=environment, stdout=report, stderr=subprocess.STDOUT, timeout=90, ) assert result.returncode == 0, "浏览器未通过,见浏览器.json" current = env["装配"].要求审校().读取返修会话(env["作者"], env["装配"].任务运行, sid) assert current["state"] == "closed" and current["author_choice"] == "original" assert current["current_round"] == 1 and len(env["收到"]) == 1 assert env["正文"].读取正文(env["作者"], "ch-4") == env["原稿"] finally: server.should_exit = True thread.join(10) assert not thread.is_alive()