"""真实写手发送、快照与HTTP决定,验证偏好撤回的后续影响。""" import json import os from dataclasses import asdict, replace from uuid import uuid4 import pytest import test_两阶段写手 as 写手测试 import test_作者偏好与反馈 as 偏好测试 import test_写手声音消费 as 声音测试 import test_受控规划方法消费 as 规划测试 import test_生产评测权限隔离 as 接入测试 from fastapi.testclient import TestClient from muse.上下文.接口 import 核对依赖 from muse.作者经验.接口 import 偏好决定, 偏好范围, 偏好请求 from muse.接入.http.应用 import 创建应用 from muse.编排.生成正文 import 生成正文错误 from muse.配置 import 读取配置 pytestmark = pytest.mark.数据库 生成环境 = 写手测试.生成环境 候选环境 = 偏好测试.候选环境 环境 = 偏好测试.环境 规划环境 = 规划测试.规划环境 def _偏好(env): req = 偏好请求( str(uuid4()), "心理描写", "以动作写心理,不替人物解释。", "以后都按这个偏好写。", 偏好范围("work", "gen-work"), ) saved = env["装配"].要求作者经验().保存偏好(env["作者"], str(uuid4()), req)["results"][0] return 偏好决定(saved["preference_id"], saved["revision"], saved["content_hash"]) @pytest.mark.case_id( "NC-w26-26b001", environment="隔离PG;实际S01/B05/B09/S02与合成HTTP,不证明文学效果", given="真实作者、作品及确切正文反馈版本", when="保存范围偏好、审阅冲突、确认、撤回及重新消费", then=["作者偏好进入实际写手且撤回只影响后续"], contract="docs/系统架构/新版设计/模块设计/B07-作者经验.md", ) def test_作者偏好进入实际写手且撤回只影响后续__26b001(生成环境): env = 生成环境 pref = _偏好(env) _, task = 写手测试.发起并跑完(env, 剧本=写手测试.正常剧本(["林深停在码头。脚边一封湿信。 "])) snapshot = 声音测试.读取材料(env, task) assert "以动作写心理" in json.loads(env["收到"][-1]["input"])["装配材料"] assert snapshot.冻结范围["preferences"][0]["preference_id"] == pref.preference_id env["装配"].要求作者经验().撤回偏好(env["作者"], "withdraw", pref) with env["库"].连接(只读=True) as conn: assert not 核对依赖(conn, snapshot.冻结范围).有效 historical = env["装配"].要求上下文().读取快照(snapshot.快照ID) assert historical.装配["文本"] == snapshot.装配["文本"] _, next_task = 写手测试.发起并跑完(env, 剧本=写手测试.正常剧本(["门扇开了。 "])) assert 声音测试.读取材料(env, next_task).冻结范围["preferences"] == [] assert "以动作写心理" not in json.loads(env["收到"][-1]["input"])["装配材料"] @pytest.mark.case_id( "NC-w26-26b002", environment="隔离PG;实际S01/B05/B09/S02与合成HTTP,不证明文学效果", given="真实作者、作品及确切正文反馈版本", when="保存范围偏好、审阅冲突、确认、撤回及重新消费", then=["冻结后新增或撤回偏好拒绝旧写手发送"], contract="docs/系统架构/新版设计/模块设计/B07-作者经验.md", ) @pytest.mark.parametrize("已有", [False, True]) def test_冻结后新增或撤回偏好拒绝旧写手发送__26b002(生成环境, 已有): env = 生成环境 pref = _偏好(env) if 已有 else None def 改变(tid): if pref: env["装配"].要求作者经验().撤回偏好(env["作者"], "withdraw", pref) else: _偏好(env) with pytest.raises(生成正文错误, match="偏好"): 写手测试.发起并跑完(env, 剧本=写手测试.正常剧本(["林深停在码头。"]), 冻结后=改变) assert len(env["收到"]) == 4 @pytest.mark.case_id( "NC-w26-26b004", environment="隔离PG;实际S01/B05/B09/S02与合成HTTP,不证明文学效果", given="真实作者、作品及确切正文反馈版本", when="保存范围偏好、审阅冲突、确认、撤回及重新消费", then=["同一偏好进入实际规划请求"], contract="docs/系统架构/新版设计/模块设计/B07-作者经验.md", ) def test_同一偏好进入实际规划请求__26b004(规划环境): env = 规划环境 pref = _偏好(env) _, task = 规划测试._运行(env) assert task.状态.value == "completed", task.步骤 assert "以动作写心理" in json.loads(env["收到"][-1]["input"])["装配材料"] assert any( x["preference_id"] == pref.preference_id for x in task.冻结输入["冻结上下文"]["source_scope"]["preferences"] ) @pytest.mark.case_id( "NC-w26-26b003", environment="隔离PG;实际S01/B05/B09/S02与合成HTTP,不证明文学效果", given="真实作者、作品及确切正文反馈版本", when="保存范围偏好、审阅冲突、确认、撤回及重新消费", then=["HTTP反馈到偏好审阅与撤回真实读回"], contract="docs/系统架构/新版设计/模块设计/B07-作者经验.md", ) def test_HTTP反馈到偏好审阅与撤回真实读回__26b003(环境, tmp_path): service, author, pools, feedback = 环境 config = 接入测试._配置文件(pools[author.用途], tmp_path) config.write_text(config.read_text().replace("eval-author", author.作者)) with TestClient(创建应用(读取配置(config)), headers={"origin": "http://testserver"}) as client: assert client.get("/api/v1/preferences").status_code == 401 assert ( client.post( "/api/v1/session", json={"password": "synthetic-evaluation-only"} ).status_code == 200 ) res = client.post( "/api/v1/feedback", json={"command_id": "http-feedback", "feedback": asdict(feedback)} ) assert res.status_code == 200, res.text fid = res.json()["feedback_id"] assert "改过的首句" in client.get("/api/v1/feedback/" + fid).json()["after_text"] req = 偏好测试._请求(origin="inferred", feedback_id=fid, quote=feedback.quote) res = client.post( "/api/v1/preferences", json={"command_id": "save", "preference": asdict(req)} ) assert res.status_code == 200, res.text saved = res.json()["results"][0] point = asdict(偏好测试._定位(saved)) review = client.post("/api/v1/preferences/review", json={"selection": point}) assert review.status_code == 200, review.text res = client.post( "/api/v1/preferences/confirm", json={ "command_id": "confirm", "selection": point, "review_id": review.json()["review_id"], "review_hash": review.json()["review_hash"], }, ) assert res.status_code == 200, res.text active = res.json()["results"][0] assert active["state"] == "active" res = client.post( "/api/v1/preferences/revoke", json={"command_id": "revoke", "selection": asdict(偏好测试._定位(active))}, ) assert res.status_code == 200, res.text assert client.get("/api/v1/preferences").json()[0]["state"] == "withdrawn" if os.environ.get("MUSE_RUN_BROWSER") == "1": _浏览器偏好(读取配置(config), fid, tmp_path) rows = service.列出偏好(author) assert len(rows) == 2 and all(r["state"] == "withdrawn" for r in rows) def _浏览器偏好(config, feedback_id, tmp_path, journey="作者偏好.spec.ts"): import socket import subprocess from contextlib import asynccontextmanager from pathlib import Path from threading import Event, Thread import uvicorn from muse.配置 import 服务配置 password = tmp_path / "preference-browser-key" password.write_text("synthetic-preference-only") password.chmod(0o600) sock = socket.socket() sock.bind(("127.0.0.1", 0)) port = sock.getsockname()[1] base = f"http://127.0.0.1:{port}" app = 创建应用( replace( config, HTTP=服务配置( str(password), 作者ID=config.HTTP.作者ID, 端口=port, 公开地址=base, 允许来源=(base,) ), ) ) ready = Event() previous = app.router.lifespan_context @asynccontextmanager async def lifespan(instance): async with previous(instance): ready.set() yield app.router.lifespan_context = lifespan 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) output = Path(os.environ.get("MUSE_BROWSER_ARTIFACTS", str(tmp_path / "browser"))).resolve() output.mkdir(parents=True, exist_ok=True) env = { **os.environ, "MUSE_WORKBENCH_URL": base, "MUSE_AUTHOR_PASSWORD_FILE": str(password), "MUSE_FEEDBACK_ID": feedback_id, } chrome = Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome") if chrome.exists(): env.setdefault("MUSE_BROWSER_EXECUTABLE", str(chrome)) thread.start() try: assert ready.wait(10) with (output / "浏览器.json").open("w") as log: process = subprocess.run( [ "pnpm", "exec", "playwright", "test", journey, "--reporter=json", "--output", str(output / "产物"), ], cwd=Path(__file__).parents[2] / "web", env=env, stdout=log, stderr=subprocess.STDOUT, timeout=90, ) assert process.returncode == 0, str(output / "浏览器.json") return output finally: server.should_exit = True thread.join(10) sock.close() assert not thread.is_alive()