"""已授权的合成原文;独立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)