177 lines
7.6 KiB
Python
177 lines
7.6 KiB
Python
"""探索方向的真实作者动作与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
|