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