"""探索方向的真实作者动作与PG回执;选择方向不确认正式规划。""" import json from dataclasses import replace import pytest from muse.作品规划.接口 import 作品错误, 探索选择, 档案保存 from muse.元数据.接口 import 元数据服务, 导入内置结构 from muse.共享.调用身份 import 内容用途, 用途, 调用身份 from muse.启动 import 构建 from muse.资源加载 import 打开资源 from muse.配置 import 应用配置 pytestmark = pytest.mark.数据库 @pytest.fixture def 交接环境(应用测试库, tmp_path): with 应用测试库[用途.维护].连接() as 连, 连.transaction(): 导入内置结构(元数据服务(连)) app = 构建( 应用配置(应用测试库[用途.生产].引用, "synthetic", 探索草稿=str(tmp_path / "私人探索")) ) actor = 调用身份("author", None, 用途.生产, 内容用途.规划) profile = app.要求作品().新档案结构(actor, "selection-work", "work_core", 1) app.要求作品().保存档案( actor, "create-work", 档案保存("selection-work", 0, {"名称": "合成作品"}, profile) ) tree = json.loads(打开资源("规划/探索交付结构.json")) draft = "# 浮岛|作品探索\n\n" + "\n\n".join( "## " + h + "\n未探索:先保留空中城镇停电的画面。" for h in ["根设定", *[c["heading"] for c in tree["chapters"]], "待决与取舍"] ) saved = app.要求探索().保存(actor, "island", "浮岛", draft, None) return app, actor, saved, 应用测试库 def test_选定稿冻结来源且重复请求回原回执__ad1201(交接环境): app, actor, draft, _ = 交接环境 works = app.要求作品() assert works.读取探索选定(actor, "selection-work")["revision"] == 0 req = 探索选择("selection-work", "island", draft["draft_hash"], 0) receipt = works.选定探索稿(actor, "select-direction", req) chosen = works.读取探索选定(actor, "selection-work") assert chosen["revision"] == 1 and chosen["selection"]["snapshot"] == draft assert chosen["selection"]["selected_by"] == "author" app.要求探索().保存( actor, "island", "浮岛", draft["content"] + "\n新的分歧,暂不选。", draft["draft_hash"] ) assert works.读取探索选定(actor, "selection-work") == chosen assert works.选定探索稿(actor, "select-direction", req) == receipt with pytest.raises(作品错误) as 错: works.选定探索稿(actor, "select-stale", replace(req, expected_revision=1)) assert 错.value.错误码 == "REVISION_CONFLICT" def test_选定失败完整回滚且私人稿保留__ad1202(交接环境): import psycopg app, actor, draft, db = 交接环境 with db[用途.维护].连接() as 连, 连.transaction(): 连.execute( "CREATE FUNCTION reject_selection_receipt() RETURNS trigger LANGUAGE plpgsql AS $$ " "BEGIN RAISE EXCEPTION 'synthetic receipt failure'; END $$" ) 连.execute( "CREATE TRIGGER reject_selection BEFORE UPDATE OF receipt ON muse_change_command " "FOR EACH ROW EXECUTE FUNCTION reject_selection_receipt()" ) request = 探索选择("selection-work", "island", draft["draft_hash"], 0) with pytest.raises(psycopg.Error): app.要求作品().选定探索稿(actor, "retry-selection", request) assert app.要求作品().读取探索选定(actor, "selection-work")["revision"] == 0 assert app.要求探索().读取(actor, "island") == draft with db[用途.维护].连接() as 连, 连.transaction(): 连.execute("DROP TRIGGER reject_selection ON muse_change_command") assert ( app.要求作品().选定探索稿(actor, "retry-selection", request)["results"][0]["revision"] == 1 ) def test_作者实际选定经HTTP与CLI回查来源__bfcbc2(交接环境, tmp_path): import subprocess import sys from datetime import datetime from fastapi.testclient import TestClient from muse.接入.http.应用 import 创建应用 from muse.配置 import 读取配置 app, actor, draft, db = 交接环境 password = tmp_path / "口令.txt" password.write_text("synthetic-selection-password") password.chmod(0o600) cfg = tmp_path / "选定.toml" cfg.write_text( '["数据库"]\n"取值方式"="受控存储"\n"位置"=' + json.dumps(db[用途.生产].引用.位置) + '\n["资源"]\n"发布身份"="synthetic"\n[HTTP]\n"口令文件"="口令.txt"\n"作者ID"="author"\n"公开地址"="http://testserver"\n"允许来源"=["http://testserver"]\n["文件"]\n"探索草稿"=' + json.dumps(app.配置.探索草稿) + "\n" ) path = "/api/v1/works/selection-work/exploration-selection" with TestClient(创建应用(读取配置(cfg)), headers={"origin": "http://testserver"}) as client: assert client.get(path).status_code == 401 client.post("/api/v1/session", json={"password": "synthetic-selection-password"}) request = { "command_id": "http-select", "selection": { "work_id": "selection-work", "draft_id": "island", "expected_hash": draft["draft_hash"], "expected_revision": 0, }, } assert ( client.post( path, json={ **request, "confirmedByUser": True, "confirmedAt": "2026-08-20T12:00:00+08:00", }, ).status_code == 422 ) result = client.post(path, json=request) assert result.status_code == 200, result.text assert client.post(path, json=request).json() == result.json() selected = client.get(path).json() assert selected["selection"]["snapshot"] == draft assert selected["selection"]["selected_by"] == "author" assert datetime.fromisoformat(selected["selection"]["selected_at"]).tzinfo is not None probe = subprocess.run( [sys.executable, "-m", "muse", "探索", str(cfg), "查看选定", "selection-work"], text=True, capture_output=True, ) assert probe.returncode == 0, probe.stderr assert json.loads(probe.stdout) == selected with pytest.raises(作品错误) as 错: app.要求作品().读取探索选定(replace(actor, 作者="another-author"), "selection-work") assert 错.value.错误码 == "SCOPE_DENIED" def test_不同方向同时交接只推进一个选定版本__ad1203(交接环境): from concurrent.futures import ThreadPoolExecutor from threading import Barrier from muse.共享.错误 import Muse错误 app, actor, one, _ = 交接环境 two = app.要求探索().保存( actor, "village", "山村", one["content"].replace("空中城镇停电", "山村渡口停航"), None ) start = Barrier(2) def choose(draft): start.wait(3) try: return app.要求作品().选定探索稿( actor, "choose-" + draft["draft_id"], 探索选择("selection-work", draft["draft_id"], draft["draft_hash"], 0), ) except Muse错误 as exc: return exc.错误码 with ThreadPoolExecutor(2) as pool: results = list(pool.map(choose, [one, two])) assert sum(isinstance(r, dict) for r in results) == 1 # 预读时已变化为REVISION_CONFLICT;等待S01依赖锁后变化为SOURCE_STALE。 assert any(r in {"REVISION_CONFLICT", "SOURCE_STALE"} for r in results if isinstance(r, str)) assert app.要求作品().读取探索选定(actor, "selection-work")["revision"] == 1