games-development-ai/cheap-worker/tests/test_reference_asset_gate.py
lili 686aaaa421
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(reference-assets): 签认《山海行纪》并闭合可信消费门
冻结地图1二十分钟纵切版的平衡、证据与金标登记。

新增 ReferenceAsset/2 清单、策略、release、受信快照及 CLI/Service/acceptance provenance /4 消费链;保持 survivor live、R1 签名与部署关闭。
2026-07-28 09:11:54 -07:00

1105 lines
46 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.

"""Task 3 参照资产可信消费门的真实文件边界测试。"""
from __future__ import annotations
import hashlib
import importlib
import json
import os
import shutil
import socket
import stat
import sys
import tempfile
import weakref
from pathlib import Path
import pytest
CHEAP_WORKER = Path(__file__).resolve().parents[1]
REPO_ROOT = CHEAP_WORKER.parent
sys.path.insert(0, str(CHEAP_WORKER))
import artifact_snapshot # noqa: E402
RELEASE_REF = "contracts/play-loop/reference-asset-release.initial.json"
REGISTRY_REF = "contracts/play-loop/reference-asset-registry.v2.initial.json"
POLICY_REF = "contracts/play-loop/reference-asset-consumption-policy.initial.json"
MANIFEST_REF = "game-runtime/games/shanhai-xingji/reference/consumption-manifest.json"
BUNDLE_REF = "game-runtime/games/shanhai-xingji/dist/shanhai-bundle.js"
POLICY_ID = "survivor-gold-v1"
EXPECTED_RELEASE_HASH = "5856d607dd973c64dbafc448ccff87cc825c917ede48b6072a52421569e6ccd0"
EXPECTED_REGISTRY_HASH = "57c82adcef617fdb929d056733a395f7c4133636eb16bd38926637857b331b78"
EXPECTED_POLICY_HASH = "695b6143fec3c7b30d7be5f5ce088ccb1c1add97899dae9f167bd3a9524d8bbf"
EXPECTED_MANIFEST_HASH = "0f600598cfb842b213f7d54ae1be1d07fe277a3e66b3e4ab3ad21b73dea320d9"
EXPECTED_BUNDLE_HASH = "1c760811ec435fe0f3b5ba79aa8c4fcc44119e019b3e1240ce2c51e55e25870b"
def _gate():
"""把缺少生产模块转成可记录的 RED,而不是 pytest collection error。"""
try:
return importlib.import_module("reference_asset_gate")
except ModuleNotFoundError as exc:
pytest.fail(f"reference_asset_gate 尚未实现: {exc}")
def _copy_into(root: Path, relative: str) -> None:
"""把仓内冻结输入复制到临时可信仓根,保留相对路径和原始字节。"""
target = root / relative
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(REPO_ROOT / relative, target)
def _fixture_root(tmp_path: Path) -> Path:
"""建立真实 U2 release、registry、policy、bundle、manifest 和 entries 快照。"""
root = tmp_path / "trusted-root"
root.mkdir(parents=True)
for relative in (RELEASE_REF, REGISTRY_REF, POLICY_REF, MANIFEST_REF, BUNDLE_REF):
_copy_into(root, relative)
manifest = json.loads((REPO_ROOT / MANIFEST_REF).read_bytes())
for entry in manifest["entries"]:
_copy_into(root, entry["path"])
return root
def _verify(root: Path, *, declarations=None, mode="frozen_preflight", expected_release_hash=EXPECTED_RELEASE_HASH):
"""按生产入口调用可信消费门。"""
return _gate().verify_policy(
POLICY_ID,
release_ref=RELEASE_REF,
expected_release_hash=expected_release_hash,
trusted_root=root,
mode=mode,
declarations=declarations,
)
def _error_code(callable_):
"""读取专用异常稳定 code,同时禁止测试依赖异常正文。"""
with pytest.raises(Exception) as caught:
callable_()
assert hasattr(caught.value, "code")
return caught.value.code, str(caught.value)
def _canonical(value: object) -> bytes:
"""按消费 manifest 的 canonical JSON 字节口径序列化。"""
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
def _rebind_release(root: Path, *, registry_hash=None, policy_hash=None) -> str:
"""让测试显式信任变异后的上游文件,以便继续覆盖 schema/语义门。"""
path = root / RELEASE_REF
release = json.loads(path.read_bytes())
if registry_hash is not None:
release["registryHash"] = registry_hash
if policy_hash is not None:
release["policyHash"] = policy_hash
raw = _canonical(release)
path.write_bytes(raw)
return hashlib.sha256(raw).hexdigest()
def _rebind_manifest_chain(root: Path) -> str:
"""同步临时 manifest、active record 和 release 的原始字节 hash。"""
manifest_hash = hashlib.sha256((root / MANIFEST_REF).read_bytes()).hexdigest()
registry_path = root / REGISTRY_REF
registry = json.loads(registry_path.read_bytes())
record = next(row for row in registry["records"] if row["recordId"] == POLICY_ID.replace("survivor-gold-v1", "gac-shanhai-xingji"))
record["consumptionManifestHash"] = manifest_hash
registry_raw = _canonical(registry)
registry_path.write_bytes(registry_raw)
return _rebind_release(root, registry_hash=hashlib.sha256(registry_raw).hexdigest())
def _write_sized_file(path: Path, size: int, fill: bytes = b"\x00") -> str:
"""写入测试文件并返回独立计算的 SHA-256;大文件用稀疏零文件控制磁盘占用。"""
path.parent.mkdir(parents=True, exist_ok=True)
if fill == b"\x00":
with path.open("wb") as handle:
handle.truncate(size)
digest = hashlib.sha256()
block = b"\x00" * min(1024 * 1024, size or 1)
remaining = size
while remaining:
chunk = block[:min(len(block), remaining)]
digest.update(chunk)
remaining -= len(chunk)
return digest.hexdigest()
content = (fill * ((size + len(fill) - 1) // len(fill)))[:size]
path.write_bytes(content)
return hashlib.sha256(content).hexdigest()
def _build_batch_policy(
root: Path,
name: str,
entry_specs: list[tuple[str, int, bytes]],
*,
record_id: str | None = None,
) -> dict:
"""构造一套 release/registry/policy,供多记录原子验证真实走完整 fd 链。"""
record_id = record_id or f"record-{name}"
policy_id = f"policy-{name}"
consumer_ref = "batch-test@reference-assets/2"
asset_root = f"assets/{name}"
artifact_ref = f"{asset_root}/bundle.js"
manifest_ref = f"{asset_root}/manifest.json"
registry_ref = f"contracts/{name}-registry.json"
policy_ref = f"contracts/{name}-policy.json"
release_ref = f"contracts/{name}-release.json"
bundle = f"bundle-{name}".encode("utf-8")
(root / artifact_ref).parent.mkdir(parents=True, exist_ok=True)
(root / artifact_ref).write_bytes(bundle)
entries = []
for relative, size, fill in entry_specs:
sha256 = _write_sized_file(root / relative, size, fill)
entries.append({"path": relative, "size": size, "sha256": sha256})
entries.sort(key=lambda entry: entry["path"].encode("utf-8"))
manifest = {
"schemaVersion": "ReferenceAssetConsumptionManifest/1",
"manifestId": f"manifest-{name}",
"canonicalization": "reference-asset-consumption-manifest/1",
"entries": entries,
}
manifest_raw = _canonical(manifest)
(root / manifest_ref).write_bytes(manifest_raw)
record = {
"schemaVersion": "ReferenceAssetRecord/2",
"recordId": record_id,
"role": "generation_exemplar",
"lifecycleStatus": "active",
"assetRef": asset_root,
"assetVersion": "1.0.0",
"artifactHash": hashlib.sha256(bundle).hexdigest(),
"consumerRef": consumer_ref,
"designRef": [
entry["path"] for entry in entries
if not entry["path"].startswith(asset_root + "/")
] or None,
"evidenceRefs": [],
"signedBy": "test",
"signedAt": "2026-07-27",
"artifactRef": artifact_ref,
"consumptionManifestRef": manifest_ref,
"consumptionManifestHash": hashlib.sha256(manifest_raw).hexdigest(),
}
registry = {
"schemaVersion": "ReferenceAssetRegistry/2",
"registryVersion": f"{name}.1",
"sourceOfTruth": "test",
"records": [record],
}
registry_raw = _canonical(registry)
(root / registry_ref).parent.mkdir(parents=True, exist_ok=True)
(root / registry_ref).write_bytes(registry_raw)
policy = {
"schemaVersion": "ReferenceAssetConsumptionPolicy/1",
"policyId": policy_id,
"recordId": record_id,
"role": record["role"],
"consumerRef": consumer_ref,
"route": f"route-{name}",
"autoSelect": False,
"mode": "frozen_preflight",
}
policy_raw = _canonical(policy)
(root / policy_ref).write_bytes(policy_raw)
release = {
"schemaVersion": "ReferenceAssetRelease/1",
"releaseId": f"release-{name}",
"registryRef": registry_ref,
"registryHash": hashlib.sha256(registry_raw).hexdigest(),
"policyRef": policy_ref,
"policyHash": hashlib.sha256(policy_raw).hexdigest(),
"verifierVersion": "reference-asset-verifier/1.0.0",
"trustedRootId": "wanxiang-reference-assets-root-v1",
}
release_raw = _canonical(release)
(root / release_ref).write_bytes(release_raw)
return {
"policy_id": policy_id,
"release_ref": release_ref,
"expected_release_hash": hashlib.sha256(release_raw).hexdigest(),
"trusted_root": root,
"mode": "frozen_preflight",
}
def test_capture_selected_files_reads_real_files_and_returns_immutable_snapshot(tmp_path):
"""选择性捕获应以相对 NFC 路径读取真实文件并返回不可变映射。"""
root = tmp_path / "root"
(root / "dir").mkdir(parents=True)
(root / "dir" / "a.txt").write_bytes(b"a\x00b")
(root / "z.txt").write_bytes(b"z")
capture = getattr(artifact_snapshot, "capture_selected_files", None)
assert capture is not None
snapshot = capture(root, ["z.txt", "dir/a.txt"], {"max_files": 2, "max_bytes": 64})
assert dict(snapshot.files) == {"dir/a.txt": b"a\x00b", "z.txt": b"z"}
with pytest.raises(TypeError):
snapshot.files["new.txt"] = b"nope"
assert snapshot.file_count == 2
assert snapshot.total_bytes == 4
def test_verify_policy_returns_frozen_files_records_receipts_and_snapshot_hash(tmp_path):
"""冻结输入匹配时应一次返回受保护文件、约束记录、回执和 canonical snapshot hash。"""
result = _verify(_fixture_root(tmp_path))
assert result.snapshot_hash == result.canonical_snapshot_hash
assert result.files
assert result.constraint_records[0]["recordId"] == "gac-shanhai-xingji"
assert result.receipts[0]["expectedRegistryHash"] == EXPECTED_REGISTRY_HASH
assert result.receipts[0]["observedRegistryHash"] == EXPECTED_REGISTRY_HASH
assert result.receipts[0]["expected"]["artifactHash"] == EXPECTED_BUNDLE_HASH
with pytest.raises(TypeError):
result.files["new.txt"] = b"nope"
with pytest.raises(TypeError):
result.constraint_records[0]["recordId"] = "tampered"
def test_snapshot_hash_matches_domain_vector(tmp_path):
"""snapshot hash 必须使用域标签、UTF-8 路径长度和原始内容长度向量。"""
root = _fixture_root(tmp_path)
result = _verify(root)
expected = hashlib.sha256()
expected.update(b"reference-asset-consumption-snapshot/1\n")
for path in sorted(result.files, key=lambda value: value.encode("utf-8")):
path_bytes = path.encode("utf-8")
content = result.files[path]
expected.update(len(path_bytes).to_bytes(8, "big"))
expected.update(path_bytes)
expected.update(len(content).to_bytes(8, "big"))
expected.update(content)
assert result.snapshot_hash == expected.hexdigest()
@pytest.mark.parametrize(
("label", "mutate", "expected_code"),
[
("release hash", lambda root: None, "reference_registry_untrusted"),
("registry hash", lambda root: (root / REGISTRY_REF).write_bytes(
(root / REGISTRY_REF).read_bytes() + b" "), "reference_registry_untrusted"),
("registry schema", lambda root: _mutate_json(root / REGISTRY_REF, {"schemaVersion": "bad"}),
"reference_registry_invalid"),
("registry duplicate recordId", lambda root: _duplicate_registry_record(root),
"reference_registry_invalid"),
("policy hash", lambda root: (root / POLICY_REF).write_bytes(
(root / POLICY_REF).read_bytes() + b" "), "reference_registry_untrusted"),
("policy identity", lambda root: _mutate_json(root / POLICY_REF, {"policyId": "other"}),
"reference_policy_missing"),
("policy mode", lambda root: _mutate_json(root / POLICY_REF, {"mode": "live"}),
"reference_declaration_mismatch"),
("bundle hash", lambda root: (root / BUNDLE_REF).write_bytes(b"tampered bundle"),
"reference_artifact_hash_mismatch"),
("manifest hash", lambda root: (root / MANIFEST_REF).write_bytes(
(root / MANIFEST_REF).read_bytes() + b" "), "reference_manifest_hash_mismatch"),
("entry hash", lambda root: _tamper_manifest_entry(root), "reference_entry_hash_mismatch"),
],
)
def test_frozen_input_tampering_is_rejected_with_stable_code(tmp_path, label, mutate, expected_code):
"""release、registry、policy、bundle、manifest 和 entry 漂移不得形成消费快照。"""
root = _fixture_root(tmp_path)
if label == "release hash":
code, message = _error_code(lambda: _gate().verify_policy(
POLICY_ID,
release_ref=RELEASE_REF,
expected_release_hash="0" * 64,
trusted_root=root,
mode="frozen_preflight",
))
else:
mutate(root)
expected_hash = EXPECTED_RELEASE_HASH
if label in {"registry schema", "registry duplicate recordId"}:
expected_hash = _rebind_release(
root,
registry_hash=hashlib.sha256((root / REGISTRY_REF).read_bytes()).hexdigest(),
)
elif label in {"policy identity", "policy mode"}:
expected_hash = _rebind_release(
root,
policy_hash=hashlib.sha256((root / POLICY_REF).read_bytes()).hexdigest(),
)
code, message = _error_code(lambda: _verify(root, expected_release_hash=expected_hash))
assert code == expected_code
assert str(tmp_path) not in message
assert b"tampered" not in message.encode("utf-8")
@pytest.mark.parametrize(
"invalid_record_id",
[
"bad\nrecord",
"bad\x00record",
("x" * 2049) + "/",
],
ids=["newline", "control", "overlong"],
)
def test_invalid_record_id_error_does_not_echo_untrusted_value(tmp_path, invalid_record_id):
"""recordId 未通过 identifier 校验时,异常正文只能保留稳定 code。"""
root = _fixture_root(tmp_path)
registry_path = root / REGISTRY_REF
registry = json.loads(registry_path.read_bytes())
record = next(row for row in registry["records"] if row["recordId"] == "gac-shanhai-xingji")
record["recordId"] = invalid_record_id
registry_raw = _canonical(registry)
registry_path.write_bytes(registry_raw)
expected_hash = _rebind_release(root, registry_hash=hashlib.sha256(registry_raw).hexdigest())
code, message = _error_code(lambda: _verify(root, expected_release_hash=expected_hash))
assert code == "reference_registry_invalid"
assert message == "code=reference_registry_invalid"
assert invalid_record_id not in message
assert "\n" not in message
assert str(tmp_path) not in message
def test_valid_record_id_remains_in_later_registry_error_context(tmp_path):
"""recordId 已合法时,后续结构错误仍可携带稳定上下文。"""
root = _fixture_root(tmp_path)
registry_path = root / REGISTRY_REF
registry = json.loads(registry_path.read_bytes())
record = next(row for row in registry["records"] if row["recordId"] == "gac-shanhai-xingji")
record["schemaVersion"] = "bad"
registry_raw = _canonical(registry)
registry_path.write_bytes(registry_raw)
expected_hash = _rebind_release(root, registry_hash=hashlib.sha256(registry_raw).hexdigest())
code, message = _error_code(lambda: _verify(root, expected_release_hash=expected_hash))
assert code == "reference_registry_invalid"
assert message == "code=reference_registry_invalid recordId=gac-shanhai-xingji"
def _mutate_json(path: Path, changes: dict) -> None:
"""对临时 JSON 输入做最小字段变异。"""
value = json.loads(path.read_bytes())
value.update(changes)
path.write_bytes(_canonical(value))
def _duplicate_registry_record(root: Path) -> None:
"""复制 active recordId,命中 registry 跨记录唯一性语义。"""
path = root / REGISTRY_REF
value = json.loads(path.read_bytes())
value["records"].append(dict(value["records"][-1]))
path.write_bytes(_canonical(value))
def _tamper_manifest_entry(root: Path) -> None:
"""只改清单覆盖的真实文件,保持 manifest/registry 双 hash 仍匹配。"""
manifest = json.loads((root / MANIFEST_REF).read_bytes())
path = root / manifest["entries"][0]["path"]
path.write_bytes(path.read_bytes() + b"tampered")
@pytest.mark.parametrize(
("relative", "expected_code"),
[
("/absolute.txt", "reference_path_escape"),
("../escape.txt", "reference_path_escape"),
("dir/../../escape.txt", "reference_path_escape"),
("dir//file.txt", "reference_path_invalid"),
("dir/./file.txt", "reference_path_invalid"),
("dir\\file.txt", "reference_path_invalid"),
("", "reference_path_invalid"),
("e\u0301.txt", "reference_path_invalid"),
],
)
def test_selected_capture_rejects_unsafe_paths(tmp_path, relative, expected_code):
"""选择性读取器在打开任何对象前拒绝越界、非 NFC 和歧义路径。"""
root = tmp_path / "root"
root.mkdir()
code, _ = _error_code(lambda: artifact_snapshot.capture_selected_files(
root, [relative], {"max_files": 2, "max_bytes": 64},
))
assert code == expected_code
def test_selected_capture_rejects_normalization_collision_before_io(tmp_path):
"""NFC 规范化后重复的两个名字不能让读取顺序产生歧义。"""
root = tmp_path / "root"
root.mkdir()
(root / "é.txt").write_bytes(b"one")
code, _ = _error_code(lambda: artifact_snapshot.capture_selected_files(
root, ["é.txt", "e\u0301.txt"], None,
))
assert code == "reference_path_invalid"
@pytest.mark.parametrize(
("setup", "expected_code"),
[
("root_symlink", "reference_symlink"),
("ancestor_symlink", "reference_symlink"),
("leaf_symlink", "reference_symlink"),
("missing", "reference_missing"),
("directory", "reference_not_regular"),
("fifo", "reference_not_regular"),
("socket", "reference_not_regular"),
],
)
def test_selected_capture_fails_closed_for_object_types_and_symlinks(tmp_path, setup, expected_code):
"""根、祖先、末级 symlink 及特殊文件均不得进入内存快照。"""
root = tmp_path / "root"
root.mkdir()
relative = "target.txt"
if setup == "root_symlink":
target = tmp_path / "real-root"
target.mkdir()
(target / relative).write_bytes(b"x")
root.rmdir()
root.symlink_to(target, target_is_directory=True)
root_for_read = root
else:
root_for_read = root
if setup == "ancestor_symlink":
target = tmp_path / "real-dir"
target.mkdir()
(target / "file.txt").write_bytes(b"x")
(root / "dir").symlink_to(target, target_is_directory=True)
relative = "dir/file.txt"
elif setup == "leaf_symlink":
(root / "real.txt").write_bytes(b"x")
(root / relative).symlink_to(root / "real.txt")
elif setup == "directory":
(root / relative).mkdir()
elif setup == "fifo":
if not hasattr(os, "mkfifo"):
pytest.skip("平台没有 FIFO")
os.mkfifo(root / relative)
elif setup == "socket":
relative = "s"
shutil.rmtree(root)
socket_parent = "/private/tmp" if Path("/private/tmp").is_dir() else "/tmp"
root = Path(tempfile.mkdtemp(prefix="rasset-", dir=socket_parent))
root_for_read = root
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
listener.bind(str(root / relative))
elif setup == "missing":
pass
if setup != "socket":
listener = None
try:
code, _ = _error_code(lambda: artifact_snapshot.capture_selected_files(
root_for_read, [relative], None,
))
assert code == expected_code
finally:
if setup == "socket":
listener.close()
(root / relative).unlink(missing_ok=True)
@pytest.mark.parametrize(
("limits", "expected_code"),
[
({"max_files": 1}, "reference_oversize"),
({"max_bytes": 2}, "reference_oversize"),
({"max_file_bytes": 2}, "reference_oversize"),
],
)
def test_selected_capture_enforces_file_count_and_logical_size_limits(tmp_path, limits, expected_code):
"""文件数、总记录和单文件上限在读前按逻辑大小 fail-closed。"""
root = tmp_path / "root"
root.mkdir()
(root / "a.txt").write_bytes(b"abc")
paths = ["a.txt", "missing.txt"] if limits.get("max_files") else ["a.txt"]
code, _ = _error_code(lambda: artifact_snapshot.capture_selected_files(root, paths, limits))
assert code == expected_code
def test_manifest_must_be_canonical_and_entries_must_be_sorted_and_bounded(tmp_path):
"""非 canonical、重复/乱序、超项数和超清单字节均拒绝且不读取 entry。"""
root = _fixture_root(tmp_path)
manifest_path = root / MANIFEST_REF
manifest = json.loads(manifest_path.read_bytes())
manifest["entries"] = list(reversed(manifest["entries"]))
manifest_path.write_bytes(json.dumps(manifest, ensure_ascii=False).encode("utf-8"))
code, _ = _error_code(lambda: _verify(root))
assert code == "reference_manifest_hash_mismatch"
def test_manifest_path_outside_protected_asset_roots_is_rejected_before_entry_io(tmp_path):
"""清单只能读取 active 记录的 assetRef 与批准 designRef 覆盖范围。"""
root = _fixture_root(tmp_path)
manifest_path = root / MANIFEST_REF
manifest = json.loads(manifest_path.read_bytes())
manifest["entries"][0]["path"] = "contracts/play-loop/reference-asset-release.initial.json"
manifest["entries"].sort(key=lambda entry: entry["path"].encode("utf-8"))
manifest_path.write_bytes(_canonical(manifest))
expected_hash = _rebind_manifest_chain(root)
code, _ = _error_code(lambda: _verify(root, expected_release_hash=expected_hash))
assert code == "reference_path_escape"
def test_declarations_must_match_policy_and_empty_live_mode_is_rejected(tmp_path):
"""声明缺失/冲突及 frozen policy 误用于 live 均在读取 bundle 前拒绝。"""
root = _fixture_root(tmp_path)
for declarations in (
{},
{"referenceAssetRecordIds": ["other"], "consumerRef": "generation-runtime@reference-assets/2"},
{"referenceAssetRecordIds": ["gac-shanhai-xingji"], "consumerRef": "other"},
):
code, _ = _error_code(lambda: _verify(root, declarations=declarations))
assert code == "reference_declaration_mismatch"
code, _ = _error_code(lambda: _verify(root, declarations=[], mode="live"))
assert code == "reference_declaration_mismatch"
def test_unknown_policy_is_rejected_before_placeholder_asset_io(tmp_path, monkeypatch):
"""未知 policy 必须在任何 bundle/manifest/entry 占位读取前终止。"""
root = _fixture_root(tmp_path)
gate = _gate()
calls = []
original = artifact_snapshot.capture_selected_files
def spy(*args, **kwargs):
calls.append(args[1] if len(args) > 1 else kwargs.get("paths"))
return original(*args, **kwargs)
monkeypatch.setattr(artifact_snapshot, "capture_selected_files", spy)
code, _ = _error_code(lambda: gate.verify_policy(
"unknown-policy",
release_ref=RELEASE_REF,
expected_release_hash=EXPECTED_RELEASE_HASH,
trusted_root=root,
mode="frozen_preflight",
))
assert code == "reference_policy_missing"
assert calls == []
@pytest.mark.parametrize("entrypoint", ["single", "batch"])
def test_auto_select_policy_is_rejected_before_target_asset_io(tmp_path, monkeypatch, entrypoint):
"""autoSelect=true 的禁用策略在两条入口均不得读取目标资产。"""
root = _fixture_root(tmp_path)
_mutate_json(root / POLICY_REF, {"autoSelect": True})
expected_hash = _rebind_release(
root,
policy_hash=hashlib.sha256((root / POLICY_REF).read_bytes()).hexdigest(),
)
manifest = json.loads((root / MANIFEST_REF).read_bytes())
target_paths = {BUNDLE_REF, MANIFEST_REF, *(entry["path"] for entry in manifest["entries"])}
calls = []
original = artifact_snapshot.capture_selected_files
def spy(*args, **kwargs):
"""记录真实选择性读取请求,保留原读取器验证其余信任链。"""
paths = tuple(args[1] if len(args) > 1 else kwargs["paths"])
calls.extend(paths)
return original(*args, **kwargs)
monkeypatch.setattr(artifact_snapshot, "capture_selected_files", spy)
if entrypoint == "single":
call = lambda: _verify(root, expected_release_hash=expected_hash)
else:
call = lambda: _gate().verify_policies([{
"policy_id": POLICY_ID,
"release_ref": RELEASE_REF,
"expected_release_hash": expected_hash,
"trusted_root": root,
"mode": "frozen_preflight",
}])
code, _ = _error_code(call)
assert code == "reference_policy_missing"
assert target_paths.isdisjoint(calls)
def test_non_active_record_rejects_without_reading_placeholder_asset(tmp_path, monkeypatch):
"""registry 中非 active 记录即使声明了路径,也不能触发占位资产 I/O。"""
root = _fixture_root(tmp_path)
registry_path = root / REGISTRY_REF
registry = json.loads(registry_path.read_bytes())
active = next(row for row in registry["records"] if row["recordId"] == "gac-shanhai-xingji")
active["lifecycleStatus"] = "migration_pending"
active["consumerRef"] = None
active["designRef"] = None
active["artifactRef"] = None
active["consumptionManifestRef"] = None
active["consumptionManifestHash"] = None
registry_raw = _canonical(registry)
registry_path.write_bytes(registry_raw)
expected_hash = _rebind_release(root, registry_hash=hashlib.sha256(registry_raw).hexdigest())
calls = []
gate = _gate()
original = artifact_snapshot.capture_selected_files
def spy(*args, **kwargs):
calls.append(tuple(args[1]))
return original(*args, **kwargs)
monkeypatch.setattr(artifact_snapshot, "capture_selected_files", spy)
code, _ = _error_code(lambda: _verify(root, expected_release_hash=expected_hash))
assert code == "reference_registry_invalid"
assert all(BUNDLE_REF not in call and MANIFEST_REF not in call for call in calls)
def test_all_or_nothing_does_not_expose_files_or_receipts_after_entry_failure(tmp_path):
"""任一 entry 失败时 verify_policy 只抛异常,不返回任何半成品对象。"""
root = _fixture_root(tmp_path)
manifest = json.loads((root / MANIFEST_REF).read_bytes())
bad_entry = manifest["entries"][-1]
(root / bad_entry["path"]).write_bytes(b"changed during setup")
code, _ = _error_code(lambda: _verify(root))
assert code == "reference_entry_hash_mismatch"
def test_cwd_does_not_change_trusted_root_resolution(tmp_path, monkeypatch):
"""releaseRef 始终相对 trusted_root 解析,调用 cwd 不参与身份和结果。"""
root = _fixture_root(tmp_path)
first = _verify(root)
other = tmp_path / "other-cwd"
other.mkdir()
monkeypatch.chdir(other)
second = _verify(root)
assert first.snapshot_hash == second.snapshot_hash
assert dict(first.files) == dict(second.files)
def test_release_registry_policy_and_manifest_missing_map_to_stable_codes(tmp_path):
"""受信链各层缺失不能落到裸 FileNotFoundError 或泄露系统路径。"""
for relative, expected_code in (
(RELEASE_REF, "reference_missing"),
(REGISTRY_REF, "reference_missing"),
(POLICY_REF, "reference_policy_missing"),
(BUNDLE_REF, "reference_missing"),
(MANIFEST_REF, "reference_missing"),
):
root = _fixture_root(tmp_path / relative.replace("/", "_"))
(root / relative).unlink()
code, message = _error_code(lambda: _verify(root))
assert code == expected_code
assert str(root) not in message
def test_reading_file_changed_during_read_is_rejected(tmp_path, monkeypatch):
"""同一 fd 读中发生截断或改写必须命中 changed_during_read。"""
root = tmp_path / "root"
root.mkdir()
target = root / "file.txt"
target.write_bytes(b"original")
original_read = os.read
changed = False
def rewrite_after_first_read(fd, size):
nonlocal changed
chunk = original_read(fd, size)
if chunk and not changed:
changed = True
target.write_bytes(b"rewritten")
return chunk
monkeypatch.setattr(artifact_snapshot.os, "read", rewrite_after_first_read)
code, _ = _error_code(lambda: artifact_snapshot.capture_selected_files(root, ["file.txt"], None))
assert code == "reference_changed_during_read"
def test_sparse_file_uses_logical_size_for_budget(tmp_path):
"""稀疏文件不能借助物理占用小绕过逻辑大小预算。"""
root = tmp_path / "root"
root.mkdir()
target = root / "sparse.bin"
with target.open("wb") as handle:
handle.seek(16 * 1024 * 1024)
handle.write(b"x")
code, _ = _error_code(lambda: artifact_snapshot.capture_selected_files(
root, ["sparse.bin"], {"max_file_bytes": 16 * 1024 * 1024},
))
assert code == "reference_oversize"
def test_verify_policy_rejects_active_missing_dual_identity_and_retired_partial_identity(tmp_path):
"""active 双身份缺失和 retired 半身份均不得进入可信消费。"""
for field in ("artifactRef", "consumptionManifestRef", "consumptionManifestHash", "consumerRef"):
root = _fixture_root(tmp_path / field)
registry_path = root / REGISTRY_REF
registry = json.loads(registry_path.read_bytes())
record = next(row for row in registry["records"] if row["recordId"] == "gac-shanhai-xingji")
record[field] = None
registry_raw = _canonical(registry)
registry_path.write_bytes(registry_raw)
expected_hash = _rebind_release(root, registry_hash=hashlib.sha256(registry_raw).hexdigest())
code, _ = _error_code(lambda: _verify(root, expected_release_hash=expected_hash))
assert code == "reference_registry_invalid"
def test_registry_allows_non_active_record_with_optional_identity_fields_omitted(tmp_path):
"""candidate/migration_pending/无历史 retired 可按 Record/2 省略可选身份字段。"""
root = _fixture_root(tmp_path)
registry_path = root / REGISTRY_REF
registry = json.loads(registry_path.read_bytes())
pending = next(row for row in registry["records"] if row["recordId"] == "_template-puzzle")
for field in (
"consumerRef", "designRef", "signedBy", "signedAt", "artifactRef",
"consumptionManifestRef", "consumptionManifestHash",
):
pending.pop(field, None)
registry_raw = _canonical(registry)
registry_path.write_bytes(registry_raw)
expected_hash = _rebind_release(root, registry_hash=hashlib.sha256(registry_raw).hexdigest())
result = _verify(root, expected_release_hash=expected_hash)
assert result.constraint_records[0]["recordId"] == "gac-shanhai-xingji"
def test_registry_rejects_retired_record_with_partial_historical_identity(tmp_path):
"""retired 一旦保留任一历史签认字段,就必须保留完整双身份闭包。"""
root = _fixture_root(tmp_path)
registry_path = root / REGISTRY_REF
registry = json.loads(registry_path.read_bytes())
retired = next(row for row in registry["records"] if row["recordId"] == "_template-puzzle")
retired["lifecycleStatus"] = "retired"
retired["consumerRef"] = "historical-consumer"
for field in ("signedBy", "signedAt", "artifactRef", "consumptionManifestRef", "consumptionManifestHash"):
retired.pop(field, None)
registry_raw = _canonical(registry)
registry_path.write_bytes(registry_raw)
expected_hash = _rebind_release(root, registry_hash=hashlib.sha256(registry_raw).hexdigest())
code, _ = _error_code(lambda: _verify(root, expected_release_hash=expected_hash))
assert code == "reference_registry_invalid"
def test_selected_capture_rejects_files_over_default_single_record_limit(tmp_path):
"""默认单文件和单记录上限分别为 16 MiB 与 64 MiB。"""
root = tmp_path / "root"
root.mkdir()
target = root / "large.bin"
with target.open("wb") as handle:
handle.seek(16 * 1024 * 1024)
handle.write(b"x")
code, _ = _error_code(lambda: artifact_snapshot.capture_selected_files(root, ["large.bin"], None))
assert code == "reference_oversize"
def test_custom_limits_cannot_relax_selected_capture_hard_caps(tmp_path):
"""调用方给更大 limits 时,16 MiB 单文件、512 文件和 64 MiB 单记录硬帽仍生效。"""
root = tmp_path / "root"
root.mkdir()
oversized = root / "oversized.bin"
with oversized.open("wb") as handle:
handle.truncate(17 * 1024 * 1024)
code, _ = _error_code(lambda: artifact_snapshot.capture_selected_files(
root,
["oversized.bin"],
{"max_file_bytes": 20 * 1024 * 1024, "max_record_bytes": 80 * 1024 * 1024},
))
assert code == "reference_oversize"
many_paths = []
for index in range(513):
relative = f"many/{index:03d}.txt"
target = root / relative
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(b"x")
many_paths.append(relative)
code, _ = _error_code(lambda: artifact_snapshot.capture_selected_files(
root, many_paths, {"max_files": 1000},
))
assert code == "reference_oversize"
record_paths = []
for index in range(5):
relative = f"record/{index}.bin"
target = root / relative
target.parent.mkdir(parents=True, exist_ok=True)
with target.open("wb") as handle:
handle.truncate(16 * 1024 * 1024 if index < 4 else 1)
record_paths.append(relative)
code, _ = _error_code(lambda: artifact_snapshot.capture_selected_files(
root,
record_paths,
{"max_file_bytes": 20 * 1024 * 1024, "max_record_bytes": 80 * 1024 * 1024},
))
assert code == "reference_oversize"
def test_verified_result_uses_deep_read_only_containers_and_json_copy(tmp_path):
"""内建基类旁路不能篡改验证结果,显式 JSON 副本仍符合 receipt schema。"""
gate = _gate()
result = _verify(_fixture_root(tmp_path))
record = result.records[0]
receipt = result.receipts[0]
assert not isinstance(record, dict)
assert not isinstance(receipt, dict)
assert isinstance(record["designRef"], tuple)
with pytest.raises(TypeError):
dict.__setitem__(record, "recordId", "tampered")
with pytest.raises(TypeError):
dict.update(receipt, {"recordId": "tampered"})
with pytest.raises(TypeError):
list.extend(record["designRef"], ["tampered"])
receipt_json = gate.to_json_value(receipt)
assert isinstance(receipt_json, dict)
assert json.loads(json.dumps(receipt_json, ensure_ascii=False)) == receipt_json
validate_path = REPO_ROOT / "contracts/play-loop/validate.py"
spec = importlib.util.spec_from_file_location("play_loop_validate_for_receipt", validate_path)
validator = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(validator)
schema = validator._load(REPO_ROOT / "contracts/play-loop/reference-asset-verification-receipt.schema.json")
assert validator.validate(schema, receipt_json, schema) == []
def test_files_is_a_pure_path_to_bytes_mapping_with_separate_root_index(tmp_path):
"""files 的迭代、成员关系和取值必须都只表达逻辑路径到 bytes。"""
result = _verify(_fixture_root(tmp_path))
assert all(isinstance(path, str) and isinstance(result.files[path], bytes) for path in result.files)
assert "gac-shanhai-xingji" not in result.files
with pytest.raises(KeyError):
result.files["gac-shanhai-xingji"]
assert result.reference_roots["gac-shanhai-xingji"]
with pytest.raises(TypeError):
result.reference_roots["other"] = ("assets",)
@pytest.mark.parametrize(
("mutation", "expected_code"),
[
("canonical-unsorted", "reference_path_invalid"),
("duplicate", "reference_path_invalid"),
("513", "reference_oversize"),
("over-1mib", "reference_oversize"),
],
)
def test_manifest_semantics_are_checked_after_rebinding_upstream_hashes(tmp_path, mutation, expected_code):
"""每类 manifest 负例都重绑上游 hash,确保命中自身语义而非陈旧 hash。"""
root = _fixture_root(tmp_path)
manifest_path = root / MANIFEST_REF
manifest = json.loads(manifest_path.read_bytes())
if mutation == "canonical-unsorted":
manifest["entries"] = list(reversed(manifest["entries"]))
elif mutation == "duplicate":
manifest["entries"].append(dict(manifest["entries"][0]))
elif mutation == "513":
manifest["entries"] = [
{"path": f"game-runtime/games/shanhai-xingji/src/generated-{index:03d}.js", "size": 0,
"sha256": hashlib.sha256(b"").hexdigest()}
for index in range(513)
]
else:
manifest["padding"] = "x" * (1024 * 1024)
manifest_path.write_bytes(_canonical(manifest))
expected_hash = _rebind_manifest_chain(root)
code, _ = _error_code(lambda: _verify(root, expected_release_hash=expected_hash))
assert code == expected_code
@pytest.mark.parametrize("field", ["role", "consumerRef"])
def test_active_record_role_and_consumer_ref_drift_are_rejected(tmp_path, field):
"""active record 与 release 绑定 policy 的 role/consumerRef 漂移必须 fail-closed。"""
root = _fixture_root(tmp_path)
registry_path = root / REGISTRY_REF
registry = json.loads(registry_path.read_bytes())
record = next(row for row in registry["records"] if row["recordId"] == "gac-shanhai-xingji")
record[field] = "generation_exemplar" if field == "role" else "drifted-consumer"
registry_raw = _canonical(registry)
registry_path.write_bytes(registry_raw)
expected_hash = _rebind_release(root, registry_hash=hashlib.sha256(registry_raw).hexdigest())
code, _ = _error_code(lambda: _verify(root, expected_release_hash=expected_hash))
assert code == "reference_declaration_mismatch"
def test_inode_replacement_after_stat_is_rejected(tmp_path, monkeypatch):
"""初次 lstat 后、open 前替换目录项 inode,必须命中 changed_during_read。"""
root = tmp_path / "root"
root.mkdir()
target = root / "file.txt"
replacement = root / "replacement.txt"
target.write_bytes(b"original")
replacement.write_bytes(b"replacement")
original_open = os.open
replaced = False
def replace_before_open(path, flags, *args, **kwargs):
nonlocal replaced
if path == "file.txt" and kwargs.get("dir_fd") is not None and not replaced:
replaced = True
os.replace(replacement, target)
return original_open(path, flags, *args, **kwargs)
monkeypatch.setattr(artifact_snapshot.os, "open", replace_before_open)
code, _ = _error_code(lambda: artifact_snapshot.capture_selected_files(root, ["file.txt"], None))
assert code == "reference_changed_during_read"
def test_unreadable_file_maps_to_stable_code_when_permissions_are_enforced(tmp_path):
"""平台执行权限位时,不可读普通文件必须映射为稳定 unreadable。"""
if hasattr(os, "geteuid") and os.geteuid() == 0:
pytest.skip("root 会绕过普通权限位")
root = tmp_path / "root"
root.mkdir()
target = root / "file.txt"
target.write_bytes(b"secret")
target.chmod(0)
try:
code, _ = _error_code(lambda: artifact_snapshot.capture_selected_files(root, ["file.txt"], None))
assert code == "reference_unreadable"
finally:
target.chmod(stat.S_IRUSR | stat.S_IWUSR)
def test_unprotected_legacy_path_is_rejected_before_target_io(tmp_path, monkeypatch):
"""manifest 指向未受保护旧路径时,不得对该旧路径发起选择性读取。"""
root = _fixture_root(tmp_path)
legacy_path = "contracts/play-loop/reference-asset-registry.initial.json"
manifest_path = root / MANIFEST_REF
manifest = json.loads(manifest_path.read_bytes())
manifest["entries"][0]["path"] = legacy_path
manifest["entries"].sort(key=lambda entry: entry["path"].encode("utf-8"))
manifest_path.write_bytes(_canonical(manifest))
expected_hash = _rebind_manifest_chain(root)
calls = []
original = artifact_snapshot.capture_selected_files
def spy(*args, **kwargs):
paths = tuple(args[1] if len(args) > 1 else kwargs["paths"])
calls.append(paths)
return original(*args, **kwargs)
monkeypatch.setattr(artifact_snapshot, "capture_selected_files", spy)
code, _ = _error_code(lambda: _verify(root, expected_release_hash=expected_hash))
assert code == "reference_path_escape"
assert all(legacy_path not in paths for paths in calls)
def test_verify_policies_atomically_merges_multiple_records_and_deduplicates_identical_paths(tmp_path):
"""batch 全部通过后才合并,多记录文件与相同路径同字节按逻辑路径去重。"""
first_root = tmp_path / "first"
second_root = tmp_path / "second"
first_root.mkdir()
second_root.mkdir()
shared = "assets/shared/common.bin"
first = _build_batch_policy(first_root, "first", [(shared, 3, b"x"), ("assets/first/a.bin", 2, b"a")])
second = _build_batch_policy(second_root, "second", [(shared, 3, b"x"), ("assets/second/b.bin", 4, b"b")])
result = _gate().verify_policies([first, second])
assert [record["recordId"] for record in result.records] == ["record-first", "record-second"]
assert len(result.receipts) == 2
assert dict(result.files) == {
"assets/first/a.bin": b"aa",
"assets/second/b.bin": b"bbbb",
shared: b"xxx",
}
assert set(result.reference_roots) == {"record-first", "record-second"}
def test_verify_policies_accepts_exact_128_mib_of_deduplicated_logical_bytes(tmp_path):
"""两个各 64 MiB 的记录合并后,去重逻辑字节恰为 128 MiB 时允许。"""
root = tmp_path / "root"
root.mkdir()
chunk = 16 * 1024 * 1024
first_entries = [(f"assets/large-first/{index}.bin", chunk, b"\x00") for index in range(4)]
second_entries = [(f"assets/large-second/{index}.bin", chunk, b"\x00") for index in range(4)]
first = _build_batch_policy(root, "large-first", first_entries)
second = _build_batch_policy(root, "large-second", second_entries)
result = _gate().verify_policies([first, second])
assert sum(len(content) for content in result.files.values()) == 128 * 1024 * 1024
def test_verify_policies_rejects_deduplicated_total_over_128_mib(tmp_path):
"""所有单记录均不超 64 MiB 时,合并唯一逻辑路径超过 128 MiB 仍拒绝。"""
root = tmp_path / "root"
root.mkdir()
chunk = 16 * 1024 * 1024
first = _build_batch_policy(
root, "limit-first", [(f"assets/limit-first/{index}.bin", chunk, b"\x00") for index in range(4)],
)
second = _build_batch_policy(
root, "limit-second", [(f"assets/limit-second/{index}.bin", chunk, b"\x00") for index in range(4)],
)
third = _build_batch_policy(root, "limit-third", [("assets/limit-third/extra.bin", 1, b"x")])
code, _ = _error_code(lambda: _gate().verify_policies([first, second, third]))
assert code == "reference_oversize"
def test_verify_policies_does_not_construct_partial_result_when_later_record_fails(tmp_path, monkeypatch):
"""第二条记录失败时,公开结果对象一次也不能被构造。"""
root = tmp_path / "root"
root.mkdir()
first = _build_batch_policy(root, "atomic-first", [("assets/atomic-first/a.bin", 1, b"a")])
second = _build_batch_policy(root, "atomic-second", [("assets/atomic-second/b.bin", 1, b"b")])
(root / "assets/atomic-second/b.bin").write_bytes(b"tampered")
gate = _gate()
original_type = gate.VerifiedReferenceAssets
constructions = []
def track_construction(*args, **kwargs):
constructions.append((args, kwargs))
return original_type(*args, **kwargs)
monkeypatch.setattr(gate, "VerifiedReferenceAssets", track_construction)
code, _ = _error_code(lambda: gate.verify_policies([first, second]))
assert code == "reference_entry_hash_mismatch"
assert constructions == []
def test_verify_policies_rejects_same_logical_path_with_conflicting_bytes(tmp_path):
"""不同可信根出现同逻辑路径但内容冲突时,batch 必须 fail-closed。"""
first_root = tmp_path / "first"
second_root = tmp_path / "second"
first_root.mkdir()
second_root.mkdir()
shared = "assets/shared/conflict.bin"
first = _build_batch_policy(first_root, "conflict-first", [(shared, 3, b"a")])
second = _build_batch_policy(second_root, "conflict-second", [(shared, 3, b"b")])
code, _ = _error_code(lambda: _gate().verify_policies([first, second]))
assert code == "reference_entry_hash_mismatch"
def test_verify_policies_releases_each_stage_before_staging_next_record(tmp_path, monkeypatch):
"""batch 合并应在下一条记录读取前释放上一条未合并 stage。"""
root = tmp_path / "root"
root.mkdir()
requests = [
_build_batch_policy(
root,
f"stream-{index}",
[(f"assets/stream-{index}/item.bin", 1, bytes([65 + index]))],
)
for index in range(4)
]
gate = _gate()
original = gate._stage_one_policy
alive_stages = []
peak_alive = 0
def tracked_stage(*args, **kwargs):
"""用弱引用统计未合并 stage 的真实生命周期,不改变 stage 行为。"""
nonlocal peak_alive
item = original(*args, **kwargs)
def release(reference):
"""阶段对象释放时从生命周期计数中移除对应弱引用。"""
alive_stages.remove(reference)
reference = weakref.ref(item, release)
alive_stages.append(reference)
peak_alive = max(peak_alive, len(alive_stages))
return item
monkeypatch.setattr(gate, "_stage_one_policy", tracked_stage)
result = gate.verify_policies(requests)
assert len(result.records) == len(requests)
assert peak_alive <= 2