muse-agent-example/tests/集成/test_正式变更事务.py
zizi 713cc45c63 重构(数据库): 迁移链压扁为单基线并精简测试至保留集
- 51 个增量迁移文件压扁为 V0001__基线.sql 完整快照(结构+种子+授权),
  等价门禁:旧链全量执行库与基线库 pg_dump 逐字节一致
- 剔除 pg_dump 固化的 public schema 超级用户归属断言(muse_maint 无权执行)
- 测试删减至保留集:备份往返 2 + 预算 2 + 租约 2 + 基线建库 1
- 租约/预算夹具改共享库,消除按例克隆建库
- 修复共享库三类既有污染:账本注入残留(系统管理)、失败触发器残留(评测)、
  建表残留(正式变更事务),发布包迁移文件名硬编码改动态核对
- 全量数据库验收 722 passed / 0 failed / 0 errors(main 基线为 24F+291E)
- 运行手册登记基线模式改表流程与账本校验和同步
2026-09-22 10:18:51 +08:00

424 lines
18 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.

"""S01共用机制的真实PG验收;参与者使用合成表,不冒充正文/事实/章后主链。"""
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, replace
from threading import Barrier
import pytest
from muse.共享.标识 import 任务标识
from muse.共享.调用身份 import 内容用途, 用途, 调用身份
from muse.正式变更.接口 import (
作者动作,
依赖引用,
修改影响,
参与操作,
参与者目录,
变更命令,
变更错误,
固定哈希,
审阅内容,
提交计划,
正式变更服务,
)
pytestmark = pytest.mark.数据库
@dataclass(frozen=True)
class 合成编辑:
内容: str
写后失败: bool = False
class 合成入口:
身份 = "synthetic-edit"
请求类型 = 合成编辑
def 查看(self, 连, 身份, 命令):
目标 = 连.execute(
"SELECT owner,revision,content FROM s01_test_document WHERE id=%s", (命令.目标,)
).fetchone()
if 目标 is None or 目标[0] != 身份.作者:
raise 变更错误("SCOPE_DENIED", "合成目标不属于当前作者")
候选 = 连.execute(
"SELECT revision,content,check_state FROM s01_test_candidate WHERE id=%s",
("candidate-1",),
).fetchone()
来源 = 连.execute(
"SELECT revision FROM s01_test_source WHERE id=%s", ("source-1",)
).fetchone()[0]
有候选 = 命令.动作 is not 作者动作.人工保存
依赖 = [
依赖引用("target", 命令.目标, str(目标[1])),
依赖引用("source", "source-1", str(来源)),
]
if 有候选:
依赖.append(依赖引用("candidate", "candidate-1", str(候选[0])))
return 审阅内容(
命令.目标,
目标[1],
"candidate-1" if 有候选 else None,
候选[0] if 有候选 else None,
固定哈希(候选[1]) if 有候选 else None,
固定哈希([目标[2], 命令.业务请求.内容]),
None,
None,
固定哈希(来源),
("replace-content",),
tuple(依赖),
)
def 准备(self, 连, 身份, 命令, 当前):
if 命令.动作 in {作者动作.拒绝, 作者动作.暂缓}:
return 提交计划(())
if 命令.动作 is 作者动作.采纳:
行 = 连.execute(
"SELECT content,check_state FROM s01_test_candidate WHERE id='candidate-1'"
).fetchone()
if 行[1] != "ready":
raise 变更错误("PRECONDITION_PENDING", "候选存在未处理检查问题")
if 行[0] != 命令.业务请求.内容:
raise 变更错误("REVIEW_MISMATCH", "只能采纳被审候选")
return 提交计划(
(参与操作("write", 命令.业务请求),),
(修改影响(命令.目标, "document_changed", str(当前.数据版本), str(当前.数据版本 + 1)),),
)
class 合成写入:
身份 = "write"
请求类型 = 合成编辑
def 提交(self, 连, 请求):
行 = 连.execute(
"UPDATE s01_test_document SET revision=revision+1,content=%s "
"WHERE id='document-1' RETURNING revision",
(请求.内容,),
).fetchone()
连.execute("INSERT INTO s01_test_audit (revision) VALUES (%s)", (行[0],))
if 请求.写后失败:
raise RuntimeError("合成参与者在业务写入后失败")
return {"revision": 行[0]}
class 合成依赖:
def __init__(self, 种类):
self.种类 = 种类
def 锁定当前(self, 连, 对象ID):
if self.种类 == "target":
行 = 连.execute(
"SELECT revision FROM s01_test_document WHERE id=%s FOR UPDATE", (对象ID,)
).fetchone()
elif self.种类 == "candidate":
行 = 连.execute(
"SELECT revision FROM s01_test_candidate WHERE id=%s FOR UPDATE", (对象ID,)
).fetchone()
else:
行 = 连.execute(
"SELECT revision FROM s01_test_source WHERE id=%s FOR SHARE", (对象ID,)
).fetchone()
if 行 is None:
raise 变更错误("SOURCE_STALE", "合成当前指针已删除")
return str(行[0])
@pytest.fixture
def 变更环境(应用测试库):
with 应用测试库[用途.维护].连接() as 连:
# 共享库模式下例间残留同名表;先幂等清理再建。
连.execute("""DROP TABLE IF EXISTS s01_test_document, s01_test_candidate,
s01_test_source, s01_test_audit CASCADE;
CREATE TABLE s01_test_document(
id text PRIMARY KEY, owner text,revision int,content text);
CREATE TABLE s01_test_candidate(
id text PRIMARY KEY,revision int,content text,check_state text);
CREATE TABLE s01_test_source(id text PRIMARY KEY,revision int);
CREATE TABLE s01_test_audit(revision int);
INSERT INTO s01_test_document VALUES ('document-1','author',1,'原内容');
INSERT INTO s01_test_candidate VALUES ('candidate-1',1,'候选内容','ready');
INSERT INTO s01_test_source VALUES ('source-1',1);""")
目录 = 参与者目录()
目录.登记入口(合成入口())
目录.登记参与者(合成写入())
for kind in ("target", "candidate", "source"):
目录.登记依赖(合成依赖(kind))
服务 = 正式变更服务(应用测试库[用途.生产], 目录)
身份 = 调用身份("author", 任务标识("author-operation"), 用途.生产, 内容用途.生成)
命令 = 变更命令(
"command-1", "synthetic-edit", "document-1", 作者动作.采纳, 1, 合成编辑("候选内容")
)
审阅 = 服务.打开审阅(身份, 命令)
命令 = replace(
命令, 审阅ID=审阅["review_id"], 审阅哈希=审阅["review_hash"], 批准变更=("replace-content",)
)
return 服务, 身份, 命令, 应用测试库
@pytest.mark.case_id(
"NC-change-review-idempotent",
environment="隔离 PostgreSQL、独立连接、合成业务参与者",
given="真实PG合成候选与已打开审阅",
when="提交、重试并改变同键参数",
then=["打开审阅不批准;同参数回执相同、异参数拒绝,业务/决定/影响只写一次"],
contract="docs/系统架构/新版设计/模块设计/S01-正式变更.md",
)
def test_审阅不是批准且提交回执幂等__a91001(变更环境):
服务, 身份, 命令, 库 = 变更环境
with 库[用途.生产].连接() as 连:
assert 连.execute("SELECT count(*) FROM muse_candidate_decision").fetchone()[0] == 0
assert 连.execute("SELECT revision FROM s01_test_document").fetchone()[0] == 1
回执 = 服务.提交变更(身份, 命令)
assert 回执["author_review_id"] == 命令.审阅ID
assert 回执["basis"]["候选版本"] == 1 and 回执["basis"]["数据版本"] == 1
assert 回执 == 服务.提交变更(身份, 命令) == 服务.读取回执(身份, 命令.命令ID)
with pytest.raises(变更错误) as 捕获:
服务.提交变更(身份, replace(命令, 批准变更=()))
assert 捕获.value.错误码 == "COMMAND_PAYLOAD_MISMATCH"
with 库[用途.生产].连接() as 连:
assert 连.execute("SELECT revision,content FROM s01_test_document").fetchone() == (
2,
"候选内容",
)
assert 连.execute("SELECT count(*) FROM s01_test_audit").fetchone()[0] == 1
assert 连.execute("SELECT count(*) FROM muse_change_impact").fetchone()[0] == 1
@pytest.mark.case_id(
"NC-change-all-rollback",
environment="隔离 PostgreSQL、独立连接、合成业务参与者",
given="已审候选与合成业务参与者",
when="参与者写业务与审计后抛异常",
then=["业务、决定、影响和命令整体回滚;相同命令修复后可执行"],
contract="docs/系统架构/新版设计/模块设计/S01-正式变更.md",
)
def test_参与者失败回滚业务决定影响与命令__a91002(变更环境):
服务, 身份, 命令, 库 = 变更环境
with pytest.raises(RuntimeError, match="合成参与者"):
服务.提交变更(身份, replace(命令, 业务请求=合成编辑("候选内容", True)))
with 库[用途.生产].连接() as 连:
assert 连.execute("SELECT revision FROM s01_test_document").fetchone()[0] == 1
assert 连.execute("SELECT count(*) FROM s01_test_audit").fetchone()[0] == 0
assert 连.execute("SELECT count(*) FROM muse_candidate_decision").fetchone()[0] == 0
assert 连.execute("SELECT count(*) FROM muse_change_command").fetchone()[0] == 0
assert 连.execute("SELECT count(*) FROM muse_change_impact").fetchone()[0] == 0
assert 服务.提交变更(身份, 命令)["results"] == [{"revision": 2}]
@pytest.mark.case_id(
"NC-change-concurrent-cas",
environment="隔离 PostgreSQL、独立连接、合成业务参与者",
given="相同目标基线和候选版本的两份已审命令",
when="独立连接同时采纳",
then=["一个赢家,另一个明确冲突,当前内容只前进一个版本"],
contract="docs/系统架构/新版设计/模块设计/S01-正式变更.md",
)
def test_两份命令并发采纳只有一份当前写入__a91003(变更环境):
服务, 身份, 命令, 库 = 变更环境
栅栏 = Barrier(2)
def 竞争(id_):
栅栏.wait()
try:
return 服务.提交变更(身份, replace(命令, 命令ID=id_))
except 变更错误 as exc:
return exc.错误码
with ThreadPoolExecutor(max_workers=2) as pool:
结果 = list(pool.map(竞争, ["command-A", "command-B"]))
assert sum(isinstance(v, dict) for v in 结果) == 1
assert next(v for v in 结果 if isinstance(v, str)) in {"SOURCE_STALE", "REVISION_CONFLICT"}
with 库[用途.生产].连接() as 连:
assert 连.execute("SELECT count(*) FROM s01_test_audit").fetchone()[0] == 1
assert 连.execute("SELECT revision FROM s01_test_document").fetchone()[0] == 2
@pytest.mark.case_id(
"NC-change-source-stale",
environment="隔离 PostgreSQL、独立连接、合成业务参与者",
given="来源版本1的作者审阅",
when="来源更新到2后提交旧审阅",
then=["SOURCE_STALE且没有业务写入或伪成功命令"],
contract="docs/系统架构/新版设计/模块设计/S01-正式变更.md",
)
def test_来源改变拒绝旧审阅且零业务写入__a91004(变更环境):
服务, 身份, 命令, 库 = 变更环境
with 库[用途.维护].连接() as 连:
连.execute("UPDATE s01_test_source SET revision=2")
with pytest.raises(变更错误) as 捕获:
服务.提交变更(身份, 命令)
assert 捕获.value.错误码 == "SOURCE_STALE"
with 库[用途.生产].连接() as 连:
assert 连.execute("SELECT revision FROM s01_test_document").fetchone()[0] == 1
assert 连.execute("SELECT count(*) FROM muse_change_command").fetchone()[0] == 0
@pytest.mark.case_id(
"NC-change-decision-axis",
environment="隔离 PostgreSQL、独立连接、合成业务参与者",
given="候选检查存在问题",
when="分别采纳、暂缓、拒绝",
then=["采纳由owner阻止;作者仍能暂缓和拒绝,不改当前正文"],
contract="docs/系统架构/新版设计/模块设计/S01-正式变更.md",
)
def test_检查未过仍可拒绝或暂缓且不改正文__a91005(变更环境):
服务, 身份, 命令, 库 = 变更环境
with 库[用途.维护].连接() as 连:
连.execute("UPDATE s01_test_candidate SET check_state='issues'")
with pytest.raises(变更错误) as 捕获:
服务.提交变更(身份, 命令)
assert 捕获.value.错误码 == "PRECONDITION_PENDING"
for action in [作者动作.暂缓, 作者动作.拒绝]:
回执 = 服务.提交变更(身份, replace(命令, 命令ID=action.value, 动作=action, 批准变更=()))
assert 回执["action"] == action.value and 回执["results"] == []
with 库[用途.生产].连接() as 连:
assert 连.execute("SELECT revision FROM s01_test_document").fetchone()[0] == 1
assert 连.execute("SELECT decision FROM muse_candidate_decision").fetchone()[0] == "reject"
@pytest.mark.case_id(
"NC-change-author-and-candidate",
environment="隔离 PostgreSQL、独立连接、合成业务参与者",
given="生产已审候选",
when="无作者、其他作者、评测用途或变更后的候选版本提交",
then=["越权拒绝,旧候选版本返回REVISION_CONFLICT"],
contract="docs/系统架构/新版设计/模块设计/S01-正式变更.md",
)
def test_非作者评测用途和旧候选版本拒绝__a91006(变更环境):
服务, 身份, 命令, 库 = 变更环境
for bad in [
replace(身份, 作者=None),
replace(身份, 作者="other"),
replace(身份, 用途=用途.评测),
]:
with pytest.raises(变更错误) as 捕获:
服务.提交变更(bad, 命令)
assert 捕获.value.错误码 == "SCOPE_DENIED"
with 库[用途.维护].连接() as 连:
连.execute("UPDATE s01_test_candidate SET revision=2,content='新版候选'")
with pytest.raises(变更错误) as 捕获:
服务.提交变更(身份, 命令)
assert 捕获.value.错误码 == "REVISION_CONFLICT"
@pytest.mark.case_id(
"NC-change-manual-save",
environment="隔离 PostgreSQL、独立连接、合成业务参与者",
given="候选检查未通过",
when="作者人工保存新文本",
then=["按当前版本保存,不要求AI评分或写候选决定"],
contract="docs/系统架构/新版设计/模块设计/S01-正式变更.md",
)
def test_人工保存不要求候选检查或审阅__a91007(变更环境):
服务, 身份, 命令, 库 = 变更环境
with 库[用途.维护].连接() as 连:
连.execute("UPDATE s01_test_candidate SET check_state='issues'")
手写 = replace(
命令,
动作=作者动作.人工保存,
业务请求=合成编辑("作者亲自写的内容"),
审阅ID=None,
审阅哈希=None,
批准变更=(),
)
assert 服务.提交变更(身份, 手写)["results"] == [{"revision": 2}]
with 库[用途.生产].连接() as 连:
assert (
连.execute("SELECT content FROM s01_test_document").fetchone()[0] == "作者亲自写的内容"
)
assert 连.execute("SELECT count(*) FROM muse_candidate_decision").fetchone()[0] == 0
@pytest.mark.case_id(
"NC-change-preview",
environment="隔离 PostgreSQL、独立连接、合成业务参与者",
given="类型化正式变更请求",
when="只读准备变更",
then=["返回参与者与具体影响,不批准、不写业务、命令或决定"],
contract="docs/系统架构/新版设计/模块设计/S01-正式变更.md",
)
def test_预览返回具体影响而不记录批准或写入__a91008(变更环境):
服务, 身份, 命令, 库 = 变更环境
预览 = 服务.准备变更(身份, 命令)
assert 预览["participants"] == ["write"]
assert 预览["changes"] == ["replace-content"]
assert 预览["impacts"][0]["新版本"] == "2"
with 库[用途.生产].连接() as 连:
assert 连.execute("SELECT count(*) FROM muse_change_command").fetchone()[0] == 0
assert 连.execute("SELECT revision FROM s01_test_document").fetchone()[0] == 1
@pytest.mark.case_id(
"NC-change-source-lock",
environment="隔离 PostgreSQL、独立连接、合成业务参与者",
given="提交已锁定当前来源,参与者尚未结束",
when="另一连接同时更新来源",
then=["PG锁证明确被阻塞;提交完成后来源更新才能生效"],
contract="docs/系统架构/新版设计/模块设计/S01-正式变更.md",
)
def test_来源当前指针在正式提交期间不可改变__a91009(变更环境):
from threading import Event
from time import monotonic
服务, 身份, 命令, 库 = 变更环境
已准备, 放行, 更新开始 = Event(), Event(), Event()
pid = []
class 等待参与者(合成写入):
def 提交(self, 连, 请求):
已准备.set()
assert 放行.wait(5)
return super().提交(连, 请求)
# 仅替换本例已登记的合成参与者;不向产品接入开放运行时回调。
服务.目录.参与者["write"] = 等待参与者()
def 更新来源():
with 库[用途.生产].连接() as 连:
pid.append(连.execute("SELECT pg_backend_pid()").fetchone()[0])
更新开始.set()
连.execute("UPDATE s01_test_source SET revision=2")
with ThreadPoolExecutor(max_workers=2) as pool:
提交 = pool.submit(服务.提交变更, 身份, 命令)
try:
assert 已准备.wait(3)
更新 = pool.submit(更新来源)
assert 更新开始.wait(2)
截止 = monotonic() + 2
阻塞 = False
with 库[用途.生产].连接() as 连:
while monotonic() < 截止:
阻塞 = 连.execute(
"SELECT EXISTS(SELECT 1 FROM pg_locks WHERE pid=%s AND NOT granted)",
(pid[0],),
).fetchone()[0]
if 阻塞:
break
Event().wait(0.01)
assert 阻塞, "来源更新没有被当前指针锁阻止"
assert 连.execute("SELECT revision FROM s01_test_source").fetchone()[0] == 1
finally:
放行.set()
assert 提交.result(timeout=3)["results"] == [{"revision": 2}]
更新.result(timeout=3)
with 库[用途.生产].连接() as 连:
assert 连.execute("SELECT revision FROM s01_test_source").fetchone()[0] == 2
@pytest.mark.case_id("NC-O09-PG-DECISIONS")
def test_批量决定不返回其他作者和其他版本(变更环境):
from muse.正式变更.接口 import 读取作者决定集
服务, 身份, 命令, 库 = 变更环境
服务.提交变更(身份, 命令)
版本 = [("candidate-1", 1), ("candidate-1", 2), ("missing", 1)]
with 库[用途.生产].连接() as 连:
assert 读取作者决定集(连, "author", 版本) == {
("candidate-1", 1): "adopt",
("candidate-1", 2): "undecided",
("missing", 1): "undecided",
}
assert set(读取作者决定集(连, "other", 版本).values()) == {"undecided"}