muse-agent-example/tests/集成/test_原文归档与恢复.py
zizi d82581daee W08 原文证据、临时租约与归档:原文证据生命周期、临时租约、调用授权与归档恢复。
按 R2 串行阶段整理提交;包内文件为该阶段交付(含后续小增量),状态以工作包清单为准。
2026-09-10 19:25:41 +08:00

591 lines
25 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 hashlib
from datetime import UTC, datetime, timedelta
import pytest
from muse.任务运行.接口 import (
任务服务,
任务请求,
原文服务,
原文错误,
步骤处理器,
步骤结果,
步骤计划,
证据服务,
)
from muse.共享.时间 import 假时钟
from muse.共享.调用身份 import 内容用途, 用途
from muse.基础设施.受控文件 import 受控文件, 受控文件错误
from muse.编排.接口 import 流程定义, 流程服务, 流程登记
pytestmark = pytest.mark.数据库
def test_raw_orphan_scope__a82112(原文环境, tmp_path):
"""保留LC-d6e3f58513a1;租约替身升级为真实PG,原占位孤儿改用规范UUID。"""
import json
import subprocess
import sys
import uuid
服务, 任务, 授权, 数据, 哈希, 时钟, 库 = 原文环境
租约 = 服务.创建租约(任务, 授权, "kept", 时钟.现在() + timedelta(hours=1), 最少剩余秒=30)
服务.写入(任务, 租约, 哈希, 数据)
孤儿 = str(uuid.uuid4())
原孤儿字节 = b"secret" # 旧例的合成字节,仅模拟缺租约残留。
孤儿哈希 = hashlib.sha256(原孤儿字节).hexdigest()
服务.文件.写入(孤儿, 孤儿哈希, 原孤儿字节)
外根 = 受控文件(tmp_path / "other-namespace")
外根.绑定命名空间(str(uuid.uuid4()), "production")
外根.写入(孤儿, 孤儿哈希, 原孤儿字节)
未知 = 服务.文件.根 / "合成资料.md"
未知.write_text("不属于原文对象的合成资料")
应用 = tmp_path / "cleanup.toml"
应用.write_text(
'["数据库"]\n"取值方式"="受控存储"\n"位置"='
+ json.dumps(库[用途.生产].引用.位置, ensure_ascii=False)
+ '\n["资源"]\n"发布身份"="test"\n["文件"]\n"原文暂存"='
+ json.dumps(str(服务.文件.根), ensure_ascii=False)
+ "\n"
)
结果 = subprocess.run(
[sys.executable, "-I", "-m", "muse", "管理", str(应用), "清理无租约暂存"],
cwd=tmp_path,
capture_output=True,
text=True,
)
assert 结果.returncode == 0, 结果.stderr
报告 = json.loads(结果.stdout)
assert 报告["cleaned_orphan_ids"] == [孤儿] and 报告["unknown_entries"] == 1
assert not (服务.文件.根 / 孤儿).exists()
assert 服务.读取(任务, 租约, 哈希) == 数据
assert (
外根.读取(孤儿, 孤儿哈希) == 原孤儿字节 and 未知.read_text() == "不属于原文对象的合成资料"
)
回执 = 服务.读取孤儿清理回执(孤儿)
assert 回执["receipt_id"] in 报告["receipts"] and 回执["state"] == "completed"
assert 服务.清理无租约原文()["cleaned_orphan_ids"] == []
assert 服务.读取孤儿清理回执(孤儿) == 回执
with pytest.raises(受控文件错误, match="其他数据库"):
原文服务(库[用途.生产], 外根).清理无租约原文()
assert 外根.读取(孤儿, 孤儿哈希) == 原孤儿字节
def test_孤儿清理先保存意图且确认失败可幂等恢复__a82111(原文环境):
import uuid
服务, 任务, 授权, 数据, 哈希, 时钟, 库 = 原文环境
租约 = 服务.创建租约(
任务, 授权, "kept-for-namespace", 时钟.现在() + timedelta(hours=1), 最少剩余秒=0
)
服务.写入(任务, 租约, 哈希, 数据)
孤儿 = str(uuid.uuid4())
服务.文件.写入(孤儿, 哈希, 数据)
with 库[用途.维护].连接() as 连:
连.execute("""CREATE FUNCTION reject_cleanup() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN RAISE EXCEPTION 'synthetic cleanup confirmation failure'; END $$;
CREATE TRIGGER reject_cleanup BEFORE UPDATE ON muse_raw_orphan_cleanup
FOR EACH ROW EXECUTE FUNCTION reject_cleanup();""")
首次 = 服务.清理无租约原文()
assert 首次["needs_recovery"] == [孤儿] and not (服务.文件.根 / 孤儿).exists()
待续 = 服务.读取孤儿清理回执(孤儿)
assert 待续["state"] == "pending"
with 库[用途.维护].连接() as 连:
连.execute("DROP TRIGGER reject_cleanup ON muse_raw_orphan_cleanup")
再次 = 服务.清理无租约原文()
assert 再次["cleaned_orphan_ids"] == [孤儿]
完成 = 服务.读取孤儿清理回执(孤儿)
assert 完成["receipt_id"] == 待续["receipt_id"] and 完成["state"] == "completed"
assert 完成["removed"] is False # 此次确认已不存在,不补造首次删除成功记录。
assert 服务.读取(任务, 租约, 哈希) == 数据
def 建证据(环境):
原文, 任务, _, 数据, 哈希, _, 库 = 环境
登记 = 流程登记()
登记.登记处理器(步骤处理器("raw-test", "1", lambda _: 步骤结果({}), "1", "1"))
运行 = 任务服务(库[用途.生产], 登记)
领取 = 运行.领取步骤("evidence-worker", ["raw-test"])
assert 领取 is not None and 领取.任务ID == 任务
return 证据服务(库[用途.生产]), 领取
def test_失败任务仍可恢复所以保留有效原文租约__a82113(原文环境):
from muse.任务运行.接口 import 任务状态
服务, 任务, 授权, 数据, 哈希, 时钟, 库 = 原文环境
租约 = 服务.创建租约(
任务, 授权, "recoverable-failure", 时钟.现在() + timedelta(hours=1), 最少剩余秒=30
)
服务.写入(任务, 租约, 哈希, 数据)
登记 = 流程登记()
登记.登记处理器(步骤处理器("raw-test", "1", lambda _: 步骤结果({}), "1", "1"))
运行 = 任务服务(库[用途.生产], 登记)
领取 = 运行.领取步骤("failed-worker", ["raw-test"])
assert 领取 is not None
运行.失败步骤(领取, "synthetic_failure")
assert 运行.读取任务(任务).状态 == 任务状态.已失败
assert 服务.恢复任务原文(任务, "author") == [{"lease_id": 租约, "action": "retained"}]
assert 服务.读取(任务, 租约, 哈希) == 数据
def test_仅哈希补交完整证据保持失败与同一身份__a82007(原文环境) -> None:
原文, 任务, 临时授权, 数据, 哈希, 时钟, 库 = 原文环境
证据, 领取 = 建证据(原文环境)
元信息 = {"call_id": "call-failed", "error_code": "MODEL_INCOMPLETE", "output_tokens": None}
回执 = 证据.保存(任务, 领取.尝试ID, "model_response", 哈希, "failed", 元信息)
assert 回执["retention"] == "hash_only" and 回执["outcome"] == "failed"
with pytest.raises(原文错误):
证据.补交原文(任务, 回执["evidence_id"], 数据, 临时授权)
授权 = 原文.批准保留(
任务,
"author",
"persistent",
来源版本="source-v1",
哈希=(哈希,),
内容用途="detection",
方式="persistent",
有效期=时钟.现在() + timedelta(days=1),
)
with pytest.raises(原文错误):
证据.补交原文(任务, 回执["evidence_id"], 数据 + b"changed", 授权)
补交 = 证据.补交原文(任务, 回执["evidence_id"], 数据, 授权)
assert 补交 == {**回执, "retention": "full", "revision": 2}
assert 证据.补交原文(任务, 回执["evidence_id"], 数据, 授权) == 补交
with 库[用途.生产].连接() as 连:
assert (
连.execute("SELECT count(*) FROM muse_attempt WHERE task_id=%s", (任务,)).fetchone()[0]
== 1
)
assert (
连.execute(
"SELECT content FROM muse_runtime_evidence WHERE evidence_id=%s",
(回执["evidence_id"],),
).fetchone()[0]
== 数据
)
def test_证据不能覆盖结果或写入未登记正文元信息__a82008(原文环境) -> None:
_, 任务, _, _, 哈希, _, _ = 原文环境
证据, 领取 = 建证据(原文环境)
元信息 = {"call_id": "call-partial"}
回执 = 证据.保存(任务, 领取.尝试ID, "model_response", 哈希, "partial", 元信息)
with pytest.raises(原文错误):
证据.保存(任务, 领取.尝试ID, "model_response", 哈希, "completed", 元信息)
with pytest.raises(原文错误):
证据.保存(任务, 领取.尝试ID, "failure", 哈希, "failed", {"source_refs": [{"raw": "正文"}]})
assert 证据.读取回执(任务, 回执["evidence_id"]) == 回执
@pytest.fixture
def 原文环境(应用测试库, tmp_path):
登记 = 流程登记()
登记.登记处理器(步骤处理器("raw-test", "1", lambda _: 步骤结果({}), "1", "1"))
登记.登记类型("raw-test", 必需保护=())
任务 = 任务服务(应用测试库[用途.生产], 登记)
流程 = 流程服务(任务, 登记)
流程.发布(流程定义("raw-test", "1", (步骤计划("raw", "raw-test", "1"),)))
id_ = 流程.发起(
任务请求(
"raw-test",
"cmd",
"author",
用途.生产,
内容用途.检测,
{},
"role-1",
"res-1",
{
"source_scope": {},
"schema_versions": {},
"authorization": "grant",
"budget": {},
"stop_conditions": [],
},
),
"raw-test",
"1",
)
时钟 = 假时钟(datetime.now(UTC))
文件 = 受控文件(tmp_path / "私人原文")
服务 = 原文服务(应用测试库[用途.生产], 文件, 时钟)
数据 = "合成原文,保留每一个字节。".encode()
哈希 = hashlib.sha256(数据).hexdigest()
授权 = 服务.批准保留(
id_,
"author",
"approve",
来源版本="source-v1",
哈希=(哈希,),
内容用途="detection",
方式="temporary",
有效期=时钟.现在() + timedelta(hours=2),
)
return 服务, id_, 授权, 数据, 哈希, 时钟, 应用测试库
def test_租约先提交才写字节且过期仍可清理__a82001(原文环境) -> None:
服务, 任务, 授权, 数据, 哈希, 时钟, 库 = 原文环境
with pytest.raises(原文错误):
服务.创建租约(任务, 授权, "too-long", 时钟.现在() + timedelta(hours=25), 最少剩余秒=60)
assert list(服务.文件.根.iterdir()) == []
租约 = 服务.创建租约(任务, 授权, "lease", 时钟.现在() + timedelta(hours=1), 最少剩余秒=60)
with 库[用途.生产].连接() as 连:
assert (
连.execute("SELECT state FROM muse_raw_lease WHERE lease_id=%s", (租约,)).fetchone()[0]
== "open"
)
assert list(服务.文件.根.iterdir()) == []
服务.写入(任务, 租约, 哈希, 数据)
assert 服务.读取(任务, 租约, 哈希) == 数据
时钟.推进(timedelta(hours=1))
with pytest.raises(原文错误):
服务.读取(任务, 租约, 哈希)
with pytest.raises(原文错误):
服务.写入(任务, 租约, 哈希, 数据)
服务.清理(任务, 租约)
assert 服务.状态(任务, 租约)["state"] == "closed"
服务.清理(任务, 租约)
def test_完整PG归档可重复对账并清理临时副本__a82002(原文环境) -> None:
服务, 任务, 授权, 数据, 哈希, 时钟, 库 = 原文环境
租约 = 服务.创建租约(任务, 授权, "lease", 时钟.现在() + timedelta(hours=1), 最少剩余秒=60)
服务.写入(任务, 租约, 哈希, 数据)
归档授权 = 服务.批准保留(
任务,
"author",
"archive",
来源版本="source-v1",
哈希=(哈希,),
内容用途="detection",
方式="archive",
有效期=时钟.现在() + timedelta(days=1),
)
回执 = 服务.归档(任务, 租约, 归档授权)
assert 回执["entry_count"] == 1 and 回执["total_bytes"] == len(数据)
assert 服务.归档(任务, 租约, 归档授权) == 回执
assert 服务.状态(任务, 租约) == {
"lease_id": 租约,
"state": "migrated",
"archive_id": 回执["archive_id"],
"cleanup_complete": True,
}
assert not (服务.文件.根 / 租约).exists()
assert 服务.读取归档(任务, 回执["archive_id"], 哈希) == 数据
assert 原文服务(库[用途.生产], 时钟实现=时钟).读取归档(任务, 回执["archive_id"], 哈希) == 数据
时钟.推进(timedelta(hours=2))
服务.清理(任务, 租约)
assert 服务.读取归档(任务, 回执["archive_id"], 哈希) == 数据
with 库[用途.生产].连接() as 连:
assert (
连.execute(
"SELECT content FROM muse_raw_archive_item WHERE archive_id=%s",
(回执["archive_id"],),
).fetchone()[0]
== 数据
)
def test_缺字节归档保留migrating不被过期删除__a82003(原文环境) -> None:
服务, 任务, 授权, 数据, 哈希, 时钟, _ = 原文环境
租约 = 服务.创建租约(任务, 授权, "lease", 时钟.现在() + timedelta(hours=1), 最少剩余秒=60)
归档授权 = 服务.批准保留(
任务,
"author",
"archive",
来源版本="source-v1",
哈希=(哈希,),
内容用途="detection",
方式="archive",
有效期=时钟.现在() + timedelta(days=1),
)
with pytest.raises(受控文件错误):
服务.归档(任务, 租约, 归档授权)
assert 服务.状态(任务, 租约)["state"] == "migrating"
assert 服务.恢复任务原文(任务, "author")[0]["action"] == "needs_recovery"
时钟.推进(timedelta(hours=2))
with pytest.raises(原文错误):
服务.清理(任务, 租约)
assert 服务.状态(任务, 租约)["state"] == "migrating"
def test_归档事务失败不留半归档且同ID恢复__a82004(原文环境) -> None:
服务, 任务, 授权, 数据, 哈希, 时钟, 库 = 原文环境
租约 = 服务.创建租约(任务, 授权, "lease", 时钟.现在() + timedelta(hours=1), 最少剩余秒=60)
服务.写入(任务, 租约, 哈希, 数据)
归档授权 = 服务.批准保留(
任务,
"author",
"archive",
来源版本="source-v1",
哈希=(哈希,),
内容用途="detection",
方式="archive",
有效期=时钟.现在() + timedelta(days=1),
)
with 库[用途.维护].连接() as 连:
连.execute("""CREATE FUNCTION reject_raw() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN RAISE EXCEPTION 'synthetic archive failure'; END $$;
CREATE TRIGGER reject_raw BEFORE INSERT ON muse_raw_archive_item
FOR EACH ROW EXECUTE FUNCTION reject_raw();""")
with pytest.raises(原文错误):
服务.归档(任务, 租约, 归档授权)
原状态 = 服务.状态(任务, 租约)
assert 原状态["state"] == "migrating"
assert 服务.文件.读取(租约, 哈希) == 数据
with 库[用途.维护].连接() as 连:
assert 连.execute("SELECT count(*) FROM muse_raw_archive").fetchone()[0] == 0
assert 连.execute("SELECT count(*) FROM muse_raw_archive_item").fetchone()[0] == 0
连.execute("DROP TRIGGER reject_raw ON muse_raw_archive_item")
assert 服务.归档(任务, 租约, 归档授权)["archive_id"] == 原状态["archive_id"]
def test_清理失败不关闭且错误作者不能批准__a82005(原文环境) -> None:
服务, 任务, 授权, 数据, 哈希, 时钟, _ = 原文环境
with pytest.raises(原文错误):
服务.批准保留(
任务,
"创始人",
"forged",
来源版本="source-v1",
哈希=(哈希,),
内容用途="detection",
方式="archive",
有效期=时钟.现在() + timedelta(days=1),
)
租约 = 服务.创建租约(任务, 授权, "lease", 时钟.现在() + timedelta(hours=1), 最少剩余秒=60)
服务.写入(任务, 租约, 哈希, 数据)
目录 = 服务.文件.根 / 租约
目录.chmod(0o755)
try:
with pytest.raises(受控文件错误):
服务.清理(任务, 租约, 作者="author")
assert 服务.状态(任务, 租约)["state"] == "open"
assert not 服务.状态(任务, 租约)["cleanup_complete"]
finally:
目录.chmod(0o700)
服务.清理(任务, 租约, 作者="author")
assert 服务.状态(任务, 租约)["state"] == "closed"
def test_HTTP作者授权到原文写入归档使用真实会话__a82006(原文环境, tmp_path) -> None:
from fastapi.testclient import TestClient
from muse.接入.http.应用 import 创建应用
from muse.配置 import 应用配置, 服务配置
_, 任务, _, 数据, 哈希, _, 库 = 原文环境
口令 = tmp_path / "口令.txt"
口令.write_text("synthetic-raw-author")
配置 = 应用配置(
库[用途.生产].引用,
"test",
HTTP=服务配置(
str(口令),
作者ID="author",
公开地址="http://testserver",
允许来源=("http://testserver",),
),
原文暂存=str(tmp_path / "HTTP原文"),
)
base = f"/api/v1/tasks/{任务}"
body = {
"command_id": "http-approve",
"source_version": "source-v1",
"content_hashes": [哈希],
"content_purpose": "detection",
"retention_mode": "temporary",
"valid_until": (datetime.now(UTC) + timedelta(hours=2)).isoformat(),
}
with TestClient(创建应用(配置)) as client:
client.headers["Origin"] = "http://testserver"
assert client.post(base + "/raw-authorizations", json=body).status_code == 401
assert (
client.post("/api/v1/session", json={"password": "synthetic-raw-author"}).status_code
== 200
)
assert (
client.post(
base + "/raw-authorizations", json={**body, "approved_by": "创始人"}
).status_code
== 422
)
授权 = client.post(base + "/raw-authorizations", json=body)
assert 授权.status_code == 200, 授权.text
租约 = client.post(
base + "/raw-leases",
json={
"authorization_id": 授权.json()["authorization_id"],
"command_id": "http-lease",
"retain_until": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
"min_remaining": 60,
},
)
assert 租约.status_code == 200, 租约.text
lease = 租约.json()["lease_id"]
write = client.put(
base + f"/raw-leases/{lease}/content/{哈希}",
content=数据,
headers={"Content-Type": "application/octet-stream"},
)
assert write.status_code == 200 and write.json()["bytes"] == len(数据)
archive = client.post(
base + "/raw-authorizations",
json={**body, "command_id": "http-archive", "retention_mode": "archive"},
)
result = client.post(
base + f"/raw-leases/{lease}/archive",
json={"authorization_id": archive.json()["authorization_id"]},
)
assert result.status_code == 200 and result.json()["entry_count"] == 1
assert not (tmp_path / "HTTP原文" / lease).exists()
def test_历史归档完整导入PG且保留旧载体__a82009(原文环境, tmp_path):
import json
from muse.基础设施.受控文件 import 受控文件错误
服务, 任务, _, _, _, 时钟, 库 = 原文环境
目录 = tmp_path / "历史归档"
目录.mkdir()
字节 = {
"input.txt": "旧输入。".encode(),
"output.txt": "旧输出。".encode(),
"same-output.txt": "旧输出。".encode(),
}
清单 = []
for name, data in sorted(字节.items()):
(目录 / name).write_bytes(data)
清单.append(
{
"relativePath": name,
"contentSha256": "sha256:" + hashlib.sha256(data).hexdigest(),
"sizeBytes": len(data),
}
)
tree = hashlib.sha256(
json.dumps(清单, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
old = {
"schemaVersion": "raw-vault-migration-receipt-v1",
"archiveId": "legacy-1",
"status": "migrated",
"archiveSha256": "sha256:" + tree,
"fileCount": 3,
"totalBytes": sum(map(len, 字节.values())),
}
(目录 / ".migration-receipt.json").write_text(json.dumps(old, ensure_ascii=False))
哈希 = tuple(sorted({hashlib.sha256(v).hexdigest() for v in 字节.values()}))
授权 = 服务.批准保留(
任务,
"author",
"legacy-approve",
来源版本="legacy-source-1",
哈希=哈希,
内容用途="detection",
方式="archive",
有效期=时钟.现在() + timedelta(days=1),
)
回执 = 服务.导入历史归档(任务, 授权, "legacy-source-1", 目录)
assert 回执 == 服务.导入历史归档(任务, 授权, "legacy-source-1", 目录)
assert 回执["entry_count"] == 2
with 库[用途.生产].连接() as 连:
assert (
连.execute(
"SELECT legacy_manifest FROM muse_raw_archive WHERE archive_id=%s",
(回执["archive_id"],),
).fetchone()[0]["entries"]
== 清单
)
for name, data in 字节.items():
assert (目录 / name).read_bytes() == data
assert 服务.读取归档(任务, 回执["archive_id"], hashlib.sha256(data).hexdigest()) == data
(目录 / "output.txt").write_bytes(b"changed")
with pytest.raises(受控文件错误):
服务.导入历史归档(任务, 授权, "legacy-source-1", 目录)
def test_PG已提交但响应丢失以原归档身份恢复__a82010(原文环境):
import psycopg
服务, 任务, 授权, 数据, 哈希, 时钟, 库 = 原文环境
租约 = 服务.创建租约(任务, 授权, "lease", 时钟.现在() + timedelta(hours=1), 最少剩余秒=60)
服务.写入(任务, 租约, 哈希, 数据)
归档授权 = 服务.批准保留(
任务,
"author",
"archive",
来源版本="source-v1",
哈希=(哈希,),
内容用途="detection",
方式="archive",
有效期=时钟.现在() + timedelta(days=1),
)
原工厂 = 服务.数据库
class 丢失确认工厂:
用途 = 原工厂.用途
次数 = 0
def 连接(self):
self.次数 += 1
次数 = self.次数
实际 = 原工厂.连接()
class 连接上下文:
def __enter__(self):
return 实际.__enter__()
def __exit__(self, *args):
result = 实际.__exit__(*args)
if 次数 == 2 and args[0] is None:
raise psycopg.OperationalError(
"synthetic acknowledgement loss after commit"
)
return result
return 连接上下文()
服务.数据库 = 丢失确认工厂()
try:
with pytest.raises(原文错误):
服务.归档(任务, 租约, 归档授权)
finally:
服务.数据库 = 原工厂
assert 服务.文件.读取(租约, 哈希) == 数据
旧状态 = 服务.状态(任务, 租约)
assert 旧状态["state"] == "migrated" and not 旧状态["cleanup_complete"]
恢复 = 服务.归档(任务, 租约, 归档授权)
assert 恢复["archive_id"] == 旧状态["archive_id"]
assert 服务.状态(任务, 租约)["cleanup_complete"]
with 库[用途.生产].连接() as 连:
assert 连.execute("SELECT count(*) FROM muse_raw_archive").fetchone()[0] == 1
@pytest.mark.parametrize("缺失", [False, True], ids=["retained-bytes", "missing-bytes"])
def test_终止任务恢复清理尚未到期原文__a82011(原文环境, 缺失):
from muse.任务运行.接口 import 任务状态
服务, 任务, 授权, 数据, 哈希, 时钟, 库 = 原文环境
租约 = 服务.创建租约(任务, 授权, "lease", 时钟.现在() + timedelta(hours=1), 最少剩余秒=60)
服务.写入(任务, 租约, 哈希, 数据)
if 缺失:
服务.文件.删除对象(租约)
运行 = 任务服务(库[用途.生产], 流程登记())
运行.控制任务(任务, "author", 任务状态.待运行, "取消", 命令ID="cancel")
assert 服务.恢复任务原文(任务, "author") == [
{"lease_id": 租约, "action": "missing" if 缺失 else "cleaned"}
]
assert 服务.状态(任务, 租约)["state"] == "closed"
assert not (服务.文件.根 / 租约).exists()
with 库[用途.生产].连接() as 连:
失败码 = 连.execute(
"SELECT failure_code FROM muse_raw_lease WHERE lease_id=%s", (租约,)
).fetchone()[0]
assert 失败码 == ("RAW_CONTENT_MISSING" if 缺失 else None)