"""只使用合成临时目录;发布缓存与备份/未知资料不混用。""" import hashlib import json from contextlib import contextmanager import pytest from muse.基础设施 import 保留回收 as 模块 from muse.基础设施.保留回收 import 初始化发布缓存, 发布缓存回收, 回收错误 def 建发布(root, name, build): release = root / name (release / "wheels").mkdir(parents=True) data = b"synthetic-wheel" (release / "wheels/muse.whl").write_bytes(data) manifest = { "schema_version": 1, "version": "1", "build_id": build, "code_hash": "c" * 64, "resource_release": "r" * 64, "files": {"wheels/muse.whl": hashlib.sha256(data).hexdigest()}, } manifest["release_id"] = hashlib.sha256( json.dumps(manifest, ensure_ascii=False, sort_keys=True).encode() ).hexdigest() (release / "发布清单.json").write_text(json.dumps(manifest, ensure_ascii=False)) return release @pytest.fixture def 环境(tmp_path): root = tmp_path / "cache" 初始化发布缓存(root) protection = {"complete": True, "build_ids": ["current"], "generation": "inventory-1"} @contextmanager def 读取保护(): yield protection.copy() return root, protection, 发布缓存回收(root, 读取保护) def 快照(root): return { str(p.relative_to(root)): p.read_bytes() for p in root.rglob("*") if p.is_file() and not p.is_symlink() } @pytest.mark.case_id("NC-release-retention-preview") def test_预览零写入并区分当前构建未知和候选(环境): root, protection, service = 环境 建发布(root, "old", "old") 建发布(root, "live", "current") (root / "backup").mkdir() (root / "backup/manifest.json").write_text('{"backup_id":"retained"}') before = 快照(root) items = {r["entry_id"]: r for r in service.预览()["items"]} assert {k: r["decision"] for k, r in items.items()} == { "old": "candidate", "live": "keep", "backup": "unknown", } assert 快照(root) == before protection["complete"] = False assert {r["decision"] for r in service.预览()["items"]} == {"unknown"} @pytest.mark.case_id("NC-release-retention-idempotent") def test_具名回收和重复执行返回同一回执(环境): root, _, service = 环境 target = 建发布(root, "old", "old") plan = service.预览()["items"][0] first = service.执行(plan) assert first["state"] == "completed" and not target.exists() assert service.执行(plan) == first assert list((root / 模块.暂存目录).iterdir()) == [] @pytest.mark.case_id("NC-release-retention-drift") @pytest.mark.parametrize("drift", ["references", "file", "manifest", "namespace", "unknown-file"]) def test_执行前引用身份或字节变化拒绝删除(环境, drift): root, protection, service = 环境 target = 建发布(root, "old", "old") plan = service.预览()["items"][0] if drift == "references": protection["build_ids"].append("old") if drift == "file": (target / "wheels/muse.whl").write_bytes(b"changed") if drift == "manifest": (target / "发布清单.json").write_text("{}") if drift == "namespace": (root / 模块.标记名).write_text("{}") if drift == "unknown-file": (target / "private.bin").write_bytes(b"private") with pytest.raises(回收错误): service.执行(plan) assert target.exists() @pytest.mark.case_id("NC-release-retention-interruption") def test_删除后回执写入中断可恢复同一具名结果(环境, monkeypatch): root, _, service = 环境 target = 建发布(root, "old", "old") plan = service.预览()["items"][0] write = 模块._写JSON def 中断(fd, name, data): if data.get("state") == "completed": raise OSError("synthetic receipt interruption") write(fd, name, data) monkeypatch.setattr(模块, "_写JSON", 中断) with pytest.raises(OSError): service.执行(plan) assert not target.exists() monkeypatch.setattr(模块, "_写JSON", write) receipt = service.执行(plan) assert receipt["state"] == "completed" and service.执行(plan) == receipt @pytest.mark.case_id("NC-release-retention-pending-unknown") def test_中断后出现未知文件不继续删除(环境, monkeypatch): root, _, service = 环境 建发布(root, "old", "old") plan = service.预览()["items"][0] remove = service._删除已登记 monkeypatch.setattr( service, "_删除已登记", lambda *_: (_ for _ in ()).throw(OSError("synthetic")) ) with pytest.raises(OSError): service.执行(plan) pending = next((root / 模块.暂存目录).iterdir()) unknown = pending / "unexpected.bin" unknown.write_bytes(b"preserve") monkeypatch.setattr(service, "_删除已登记", remove) with pytest.raises(回收错误, match="未知字节"): service.执行(plan) assert unknown.read_bytes() == b"preserve" assert (pending / "wheels/muse.whl").exists() @pytest.mark.case_id("NC-release-retention-no-adoption") def test_已有备份或普通目录不能被接管为缓存(tmp_path): backup = tmp_path / "backup" backup.mkdir() (backup / "manifest.json").write_text("{}") with pytest.raises(FileExistsError): 初始化发布缓存(backup) @contextmanager def 保护(): yield {"complete": True, "build_ids": []} with pytest.raises(FileNotFoundError): 发布缓存回收(backup, 保护).预览() assert (backup / "manifest.json").read_text() == "{}" @pytest.mark.case_id("NC-release-retention-symlink") def test_符号链接始终未知且不影响目标(环境, tmp_path): root, _, service = 环境 outside = tmp_path / "outside" outside.mkdir() (outside / "keep").write_bytes(b"preserve") (root / "linked").symlink_to(outside, target_is_directory=True) assert service.预览()["items"] == [ {"entry_id": "linked", "decision": "unknown", "reason": "unrecognized_or_damaged"} ] assert (outside / "keep").read_bytes() == b"preserve" @pytest.mark.case_id("NC-release-retention-receipt-tamper") @pytest.mark.parametrize("改变", ["files-copy", "completed", "manifest"]) def test_中断回执篡改不能授权删新字节或伪造完成(环境, monkeypatch, 改变): root, _, service = 环境 建发布(root, "old", "old") plan = service.预览()["items"][0] 原删除 = service._删除已登记 monkeypatch.setattr(service, "_删除已登记", lambda *_: (_ for _ in ()).throw(OSError("stop"))) with pytest.raises(OSError): service.执行(plan) pending = next((root / 模块.暂存目录).iterdir()) receipt_path = next((root / 模块.回执目录).iterdir()) receipt = json.loads(receipt_path.read_text()) wheel = pending / "wheels/muse.whl" if 改变 == "completed": receipt["state"] = "completed" else: wheel.write_bytes(b"unapproved-new-bytes") files = {"wheels/muse.whl": hashlib.sha256(wheel.read_bytes()).hexdigest()} if 改变 == "files-copy": receipt["files"] = files else: receipt["release_manifest"]["files"] = files receipt_path.write_text(json.dumps(receipt)) monkeypatch.setattr(service, "_删除已登记", 原删除) with pytest.raises(回收错误, match="回执"): service.执行(plan) assert wheel.exists()