muse-agent-example/tests/集成/test_正文候选与作者决定.py
zizi d909d1bd1b 后端实现与用例身份:19 包集成落地并修复收尾缺陷
实现侧:
- 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。
- 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。
- 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。
- 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。
- 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。
- 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。
- 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。
- 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。

用例侧:
- 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存;
- 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
2026-09-18 01:15:00 +08:00

363 lines
17 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.

"""人工候选走真实B05、S01和PG;生成/事实联合采纳另由W15/W16验收。"""
from concurrent.futures import ThreadPoolExecutor
from dataclasses import replace
from threading import Barrier
import pytest
from muse.作品规划.接口 import 档案保存
from muse.共享.调用身份 import 内容用途, 用途, 调用身份
from muse.共享.错误 import Muse错误
from muse.启动 import 构建
from muse.正式变更.接口 import 作者动作, 变更错误
from muse.正文写作.接口 import 候选定位, 文本节点, 正文草稿, 段落
from muse.配置 import 应用配置
pytestmark = pytest.mark.数据库
def 草稿(首句="原句「保留」e\u0301", 尾句="原来的末段"):
return 正文草稿((段落("p1", (文本节点(首句),)), 段落("p2", ()), 段落("p3", (文本节点(尾句),))))
@pytest.fixture
def 候选环境(内置结构测试库):
应用测试库 = 内置结构测试库
装配 = 构建(应用配置(数据库=应用测试库[用途.生产].引用, 资源发布身份="test"))
with 装配.生命周期():
作者 = 调用身份("author", None, 用途.生产, 内容用途.生成)
作品 = 装配.要求作品()
结构 = 作品.新档案结构(作者, "work-candidate", "work_core", 1)
作品.保存档案(作者, "create", 档案保存("work-candidate", 0, {"名称": "合成候选作品"}, 结构))
作品.添加章节(
作者, "chapter", "work-candidate", "chapter-candidate", "改稿", 预期目录版本=0
)
正文 = 装配.要求正文()
正文.保存人工(作者, "base", "chapter-candidate", 0, 草稿())
yield 正文, 作者, 应用测试库
def 新候选(正文, 作者, 命令="propose"):
回执 = 正文.创建人工候选(
作者, 命令, "chapter-candidate", 1, 草稿("改过的首句。", "改过的末段。")
)
候选 = 正文.读取候选(作者, 回执["results"][0]["candidate_id"])
定位 = 候选定位(
候选["candidate_id"], 候选["revision"], 候选["candidate_hash"], 候选["document_id"], 1
)
return 回执, 候选, 定位
def 决定(正文, 作者, 命令, 定位, 审阅, 动作=作者动作.采纳):
return 正文.决定候选(
作者,
命令,
定位,
动作,
审阅["review_id"],
审阅["review_hash"],
("document",) if 动作 is 作者动作.采纳 else (),
)
@pytest.mark.case_id(
"NC-writing-candidate-partial",
environment="真实隔离PostgreSQL、HTTP TestClient与CLI;没有模型调用",
given="固定中文正文和两段改稿",
when="保存候选、按段选择、重新审阅并采纳",
then=["正文未先写入;候选追加版本并拒绝旧审阅;同一命令回放同一回执"],
contract="docs/系统架构/新版设计/模块设计/B05-正文写作.md",
)
def test_候选部分选择追加版本重新审阅且采纳同一回执__aa1201(候选环境):
正文, 作者, 库 = 候选环境
首次, 候选, 定位 = 新候选(正文, 作者)
assert (
正文.创建人工候选(
作者, "propose", "chapter-candidate", 1, 草稿("改过的首句。", "改过的末段。")
)
== 首次
)
assert 正文.读取正文(作者, "chapter-candidate")["revision"] == 1
assert 候选["decision"] == "undecided" and 候选["origin"] == "author"
assert 候选["checks"]["quality"] == "not_applicable"
assert len(候选["changes"]) == 2
旧审阅 = 正文.打开候选审阅(作者, 定位)
编辑 = 正文.编辑候选(
作者,
"pick-first",
候选["candidate_id"],
1,
候选["candidate_hash"],
选择变更=(候选["changes"][0]["change_id"],),
)
assert 编辑["results"][0]["revision"] == 2
当前 = 正文.读取候选(作者, 候选["candidate_id"])
assert 当前["visible_text"] == "改过的首句。\n\n原来的末段"
assert 当前["candidate_hash"] != 候选["candidate_hash"]
assert 正文.读取候选(作者, 候选["candidate_id"], 版本=1)["visible_text"].endswith(
"改过的末段。"
)
with pytest.raises(变更错误) as 旧:
决定(正文, 作者, "old-review", 定位, 旧审阅)
assert 旧.value.错误码 == "REVISION_CONFLICT"
定位 = replace(定位, candidate_revision=2, candidate_hash=当前["candidate_hash"])
审阅 = 正文.打开候选审阅(作者, 定位)
assert 审阅["before"]["revision"] == 1 and 审阅["candidate"]["revision"] == 2
回执 = 决定(正文, 作者, "adopt", 定位, 审阅)
assert 决定(正文, 作者, "adopt", 定位, 审阅) == 回执
assert 正文.读取正文(作者, "chapter-candidate")["visible_text"] == 当前["visible_text"]
assert 正文.读取候选(作者, 候选["candidate_id"])["decision"] == "adopt"
with 库[用途.生产].连接() as 连:
assert 连.execute("SELECT count(*) FROM muse_document_version").fetchone()[0] == 2
assert 连.execute("SELECT count(*) FROM muse_candidate_decision").fetchone()[0] == 1
@pytest.mark.case_id(
"NC-writing-candidate-stale",
environment="真实隔离PostgreSQL、HTTP TestClient与CLI;没有模型调用",
given="候选创建后作者更新正文",
when="审阅并尝试采纳,随后拒绝候选",
then=["陈旧候选不能覆盖新稿;拒绝可完成;其他作者和评测身份无权操作"],
contract="docs/系统架构/新版设计/模块设计/B05-正文写作.md",
)
def test_正文改变阻止采纳但作者仍可拒绝陈旧候选__aa1202(候选环境):
正文, 作者, _ = 候选环境
_, 候选, 定位 = 新候选(正文, 作者)
正文.保存人工(作者, "new-manual", "chapter-candidate", 1, 草稿("作者后来改的原文"))
定位 = replace(定位, expected_revision=2)
审阅 = 正文.打开候选审阅(作者, 定位)
assert 审阅["candidate"]["stale"] is True
with pytest.raises(变更错误) as 冲突:
决定(正文, 作者, "cannot-adopt", 定位, 审阅)
assert 冲突.value.错误码 == "SOURCE_STALE"
for 无权 in (replace(作者, 作者="another-author"), replace(作者, 用途=用途.评测)):
with pytest.raises(Muse错误) as 越权:
正文.读取候选(无权, 候选["candidate_id"])
assert 越权.value.错误码 == "SCOPE_DENIED"
回执 = 决定(正文, 作者, "reject-stale", 定位, 审阅, 作者动作.拒绝)
assert 回执["results"] == []
assert 正文.读取正文(作者, "chapter-candidate")["revision"] == 2
assert 正文.读取候选(作者, 候选["candidate_id"])["decision"] == "reject"
@pytest.mark.case_id(
"NC-writing-candidate-concurrent",
environment="真实隔离PostgreSQL、HTTP TestClient与CLI;没有模型调用",
given="同一候选版本和审阅",
when="两条真实PG连接同时提交不同采纳命令",
then=["只有一个正文新版本及作者决定,另一条得到冲突"],
contract="docs/系统架构/新版设计/模块设计/B05-正文写作.md",
)
def test_两个真实采纳竞争只产生一个正文与决定__aa1203(候选环境):
正文, 作者, 库 = 候选环境
_, _, 定位 = 新候选(正文, 作者)
审阅 = 正文.打开候选审阅(作者, 定位)
同时 = Barrier(2)
def 提交(序):
同时.wait()
try:
return 决定(正文, 作者, f"race-{序}", 定位, 审阅)
except 变更错误 as exc:
return exc.错误码
with ThreadPoolExecutor(2) as 池:
结果 = list(池.map(提交, (1, 2)))
assert sum(isinstance(v, dict) for v in 结果) == 1
assert any(v in ("REVISION_CONFLICT", "SOURCE_STALE") for v in 结果 if isinstance(v, str))
with 库[用途.生产].连接() as 连:
assert 连.execute("SELECT count(*) FROM muse_document_version").fetchone()[0] == 2
assert 连.execute("SELECT count(*) FROM muse_candidate_decision").fetchone()[0] == 1
@pytest.mark.case_id(
"NC-writing-candidate-rollback",
environment="真实隔离PostgreSQL、HTTP TestClient与CLI;没有模型调用",
given="真实PG在决定写入时注入故障",
when="采纳候选后解除故障并重试",
then=["正文与决定整笔回滚,候选和审阅保留,重试按原基线成功"],
contract="docs/系统架构/新版设计/模块设计/B05-正文写作.md",
)
def test_决定写入失败整笔正文回滚且候选仍可审阅__aa1204(候选环境):
import psycopg
正文, 作者, 库 = 候选环境
_, 候选, 定位 = 新候选(正文, 作者)
审阅 = 正文.打开候选审阅(作者, 定位)
with 库[用途.维护].连接() as 连:
连.execute("""CREATE FUNCTION reject_decision() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN RAISE EXCEPTION 'synthetic decision failure'; END $$;
CREATE TRIGGER reject_decision BEFORE INSERT ON muse_candidate_decision
FOR EACH ROW EXECUTE FUNCTION reject_decision();""")
with pytest.raises(psycopg.Error):
决定(正文, 作者, "rollback", 定位, 审阅)
assert 正文.读取正文(作者, "chapter-candidate")["revision"] == 1
assert 正文.读取候选(作者, 候选["candidate_id"])["decision"] == "undecided"
with 库[用途.维护].连接() as 连:
连.execute("DROP TRIGGER reject_decision ON muse_candidate_decision")
assert 决定(正文, 作者, "rollback", 定位, 审阅)["results"][0]["revision"] == 2
@pytest.mark.case_id(
"NC-writing-candidate-http-cli",
environment="真实隔离PostgreSQL、HTTP TestClient与CLI;没有模型调用",
given="已认证作者和隔离PG中的人工正文",
when="HTTP创建候选、打开审阅并暂缓,CLI按被审版本采纳",
then=["实际路由和CLI同读同写;自报作者拒绝;暂缓不写正文;采纳命令可重放"],
contract="docs/系统架构/新版设计/模块设计/B05-正文写作.md",
)
def test_网页创建审阅暂缓再由CLI按同版本采纳__aa1205(候选环境, tmp_path):
import json
import subprocess
import sys
from dataclasses import asdict
from fastapi.testclient import TestClient
from muse.接入.http.应用 import 创建应用
from muse.配置 import 服务配置
正文, 作者, 库 = 候选环境
口令 = tmp_path / "synthetic-author.txt"
口令.write_text("synthetic-author-access")
口令.chmod(0o600)
配置 = 应用配置(
数据库=库[用途.生产].引用,
资源发布身份="test",
HTTP=服务配置(
str(口令),
作者ID="author",
公开地址="http://testserver",
允许来源=("http://testserver",),
),
)
with TestClient(创建应用(配置), headers={"origin": "http://testserver"}) as 浏览器:
载荷 = {
"command_id": "http-propose",
"chapter_id": "chapter-candidate",
"branch_id": "main",
"expected_revision": 1,
"document": asdict(草稿("另一种开头。")),
}
路径 = "/api/v1/chapters/chapter-candidate/writing-candidates"
assert 浏览器.post(路径, json=载荷).status_code == 401
assert (
浏览器.post("/api/v1/session", json={"password": "synthetic-author-access"}).status_code
== 200
)
回执 = 浏览器.post(路径, json=载荷)
assert 回执.status_code == 200, 回执.text
候选ID = 回执.json()["results"][0]["candidate_id"]
列表 = 浏览器.get(路径).json()
assert len(列表) == 1 and 列表[0]["candidate_id"] == 候选ID
候选 = 浏览器.get(f"/api/v1/writing-candidates/{候选ID}").json()
定位 = {
"candidate_id": 候选ID,
"candidate_revision": 1,
"candidate_hash": 候选["candidate_hash"],
"target_ref": 候选["document_id"],
"expected_revision": 1,
}
审阅 = 浏览器.post("/api/v1/author-reviews", json=定位)
assert 审阅.status_code == 200, 审阅.text
assert 审阅.json()["before"]["visible_text"].startswith("原句")
决定载荷 = {
**定位,
"command_id": "http-defer",
"action": "defer",
"author_review_id": 审阅.json()["review_id"],
"review_hash": 审阅.json()["review_hash"],
"approved_changes": [],
}
决策路径 = f"/api/v1/writing-candidates/{候选ID}/decisions"
assert 浏览器.post(决策路径, json={**决定载荷, "decided_by": "author"}).status_code == 422
暂缓 = 浏览器.post(决策路径, json=决定载荷)
assert 暂缓.status_code == 200 and 暂缓.json()["action"] == "defer"
assert 正文.读取正文(作者, "chapter-candidate")["revision"] == 1
应用文件 = tmp_path / "candidate.toml"
应用文件.write_text(
'["数据库"]\n"取值方式"="受控存储"\n"位置"='
+ json.dumps(库[用途.生产].引用.位置)
+ '\n["资源"]\n"发布身份"="test"\n["HTTP"]\n"作者ID"="author"\n"口令文件"='
+ json.dumps(str(口令))
+ "\n"
)
请求文件 = tmp_path / "adopt.json"
请求文件.write_text(
json.dumps(
{
**决定载荷,
"command_id": "cli-adopt",
"action": "adopt",
"approved_changes": ["document"],
}
)
)
命令 = [sys.executable, "-I", "-m", "muse", "审阅", str(应用文件), "决定", str(请求文件)]
调用 = subprocess.run(命令, cwd=tmp_path, capture_output=True, text=True)
assert 调用.returncode == 0, 调用.stderr
重放 = subprocess.run(命令, cwd=tmp_path, capture_output=True, text=True)
assert 重放.returncode == 0 and json.loads(重放.stdout) == json.loads(调用.stdout)
assert 正文.读取正文(作者, "chapter-candidate")["visible_text"].startswith("另一种开头。")
@pytest.mark.case_id(
"NC-work-progress-from-owners",
environment="真实隔离PostgreSQL;B01/B05/S02公开查询,无模型调用",
given="隔离库中一章有正文,一章只有候选,还有排队任务",
when="通过实际应用装配查询创作进度",
then=["正文、候选、任务分别带真实版本与事件位置;候选和排队任务不增加已保存章数"],
contract="docs/系统架构/新版设计/模块设计/B01-作品规划.md",
)
def test_创作进度只从真实版本候选和任务读取__aa1206(候选环境):
from muse.任务运行.接口 import 任务服务, 任务请求, 步骤处理器, 步骤结果, 步骤计划
from muse.编排.接口 import 流程定义, 流程服务, 流程登记
正文, 作者, 库 = 候选环境
装配 = 构建(应用配置(数据库=库[用途.生产].引用, 资源发布身份="test"))
with 装配.生命周期():
装配.要求作品().添加章节(
作者, "empty-chapter", "work-candidate", "empty-chapter", "尚未落笔", 预期目录版本=1
)
正文.创建人工候选(作者, "empty-proposal", "empty-chapter", 0, 草稿())
登记 = 流程登记()
登记.登记类型("progress-test", 必需保护=("输入校验",))
登记.登记处理器(
步骤处理器(
"progress-test", "1", lambda _: 步骤结果({}), "v1", "v1", 保护职责="输入校验"
)
)
运行 = 任务服务(库[用途.生产], 登记)
流程服务(运行, 登记).发布(
流程定义("progress-test", "1", (步骤计划("inspect", "progress-test", "1"),))
)
任务ID = 运行.创建任务(
任务请求(
"progress-test",
"queued-progress",
"author",
用途.生产,
内容用途.规划,
{},
"v1",
"synthetic",
{
"source_scope": ["work-candidate"],
"schema_versions": {},
"authorization": "synthetic-author",
"budget": {"max_calls": 0},
"stop_conditions": ["no-model-call"],
},
作品ID="work-candidate",
),
"progress-test",
"1",
)
结果 = 装配.要求作品进度().查询(作者, "work-candidate")
assert 结果["directory_revision"] == 2 and 结果["total_chapters"] == 2
assert 结果["saved_chapters"] == 1 and 结果["undecided_candidates"] == 1
assert [c["document_revision"] for c in 结果["chapters"]] == [1, 0]
assert 结果["chapters"][1]["candidates"][0]["revision"] == 1
assert 结果["tasks"] == [{"task_id": 任务ID, "state": "queued", "event_cursor": 1}]