muse-agent-example/tests/集成/test_探索选定与规划.py
zizi 224df00706 W13 渐进探索、分层规划与近期细化:私人探索、明确选定、分层规划与近期细化;含竞态修复新用例。
按 R2 串行阶段整理提交;包内文件为该阶段交付(含后续小增量),状态以工作包清单为准。
2026-09-10 19:25:41 +08:00

177 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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