"""Software-only tests for fixed RF-001 agent release archives.""" from __future__ import annotations import json from pathlib import Path import stat import zipfile import pytest from tools.rf001_agent_update import ( CONTENT_MANIFEST_NAME, RELEASE_SOURCE_FILES, ReleaseValidationError, build_release_archive, canonical_json_bytes, inspect_release_archive, sign_update_manifest, verify_update_manifest_signature, ) def _source_tree(root: Path, *, agent_version: str = "2026.09.29.1") -> None: for index, relative in enumerate(RELEASE_SOURCE_FILES.values()): path = root / relative path.parent.mkdir(parents=True, exist_ok=True) content = f"# fixed fixture {index}\n" if relative == RELEASE_SOURCE_FILES["app/tools/rf001_rx_http_agent.py"]: content += f'AGENT_VERSION = "{agent_version}"\n' path.write_text(content, encoding="utf-8") def test_release_roundtrip_uses_exact_allowlist(tmp_path: Path) -> None: source = tmp_path / "source" _source_tree(source) package = tmp_path / "rf001-agent-2026.09.29.1.zip" built = build_release_archive( source, package, release_id="rf001-agent-2026.09.29.1", agent_version="2026.09.29.1", protocol_version=1, ) extraction = tmp_path / "extracted" checked = inspect_release_archive( package, expected_package_sha256=built.package_sha256, expected_package_bytes=built.package_bytes, expected_content_manifest_sha256=built.content_manifest_sha256, extraction_root=extraction, ) assert checked == built assert { path.relative_to(extraction).as_posix() for path in extraction.rglob("*") if path.is_file() } == set(RELEASE_SOURCE_FILES) | {CONTENT_MANIFEST_NAME} def test_update_manifest_rejects_bad_signature() -> None: token = b"fixed-test-token-0123456789abcdef" manifest = { "schema_version": 1, "release_id": "rf001-agent-2026.09.29.1", "package_sha256": "a" * 64, } signature = sign_update_manifest(token, manifest) verify_update_manifest_signature(token, manifest, signature) with pytest.raises(ReleaseValidationError, match="mismatch"): verify_update_manifest_signature(token, manifest, "0" * 64) changed = dict(manifest, package_sha256="b" * 64) with pytest.raises(ReleaseValidationError, match="mismatch"): verify_update_manifest_signature(token, changed, signature) @pytest.mark.parametrize( "unsafe_name", ( "../outside.py", "/absolute.py", "C:/absolute.py", "app\\tools\\backslash.py", ), ) def test_release_rejects_unsafe_paths(tmp_path: Path, unsafe_name: str) -> None: package = tmp_path / "unsafe.zip" with zipfile.ZipFile(package, "w") as archive: archive.writestr(unsafe_name, b"x") # zipfile normalizes a backslash to a slash before it can be inspected; # that spelling still cannot enter the fixed release allowlist. with pytest.raises( ReleaseValidationError, match="unsafe|drive-like|not allowlisted|manifest is absent", ): inspect_release_archive(package) def test_release_rejects_symlink_and_duplicate_names(tmp_path: Path) -> None: symlink_package = tmp_path / "symlink.zip" with zipfile.ZipFile(symlink_package, "w") as archive: info = zipfile.ZipInfo("app/tools/link.py") info.create_system = 3 info.external_attr = (stat.S_IFLNK | 0o777) << 16 archive.writestr(info, b"target") with pytest.raises(ReleaseValidationError, match="links"): inspect_release_archive(symlink_package) duplicate_package = tmp_path / "duplicate.zip" with zipfile.ZipFile(duplicate_package, "w") as archive: archive.writestr("app/tools/a.py", b"a") archive.writestr("APP/TOOLS/A.PY", b"b") with pytest.raises(ReleaseValidationError, match="duplicate"): inspect_release_archive(duplicate_package) def test_release_rejects_unsupported_protocol_and_sha(tmp_path: Path) -> None: source = tmp_path / "source" _source_tree(source) package = tmp_path / "release.zip" built = build_release_archive( source, package, release_id="rf001-agent-2026.09.29.1", agent_version="2026.09.29.1", protocol_version=2, ) assert built.protocol_version == 2 with pytest.raises(ReleaseValidationError, match="package SHA256"): inspect_release_archive(package, expected_package_sha256="0" * 64) def test_content_manifest_must_be_canonical(tmp_path: Path) -> None: package = tmp_path / "noncanonical.zip" manifest = { "schema_version": 1, "release_id": "rf001-agent-2026.09.29.1", "agent_version": "2026.09.29.1", "protocol_version": 1, "files": [], } assert canonical_json_bytes(manifest) != json.dumps(manifest).encode("utf-8") with zipfile.ZipFile(package, "w") as archive: archive.writestr(CONTENT_MANIFEST_NAME, json.dumps(manifest)) with pytest.raises(ReleaseValidationError, match="canonical"): inspect_release_archive(package) @pytest.mark.parametrize( ("source_version", "requested_version"), ( ("2026.09.29.2", "2026.09.29.1"), ("2026.09.29.1", "2026.09.29.2"), ), ) def test_release_builder_rejects_version_mismatch_before_artifact( tmp_path: Path, source_version: str, requested_version: str, ) -> None: source = tmp_path / "source" _source_tree(source, agent_version=source_version) package = tmp_path / "must-not-exist.zip" with pytest.raises(ReleaseValidationError, match="AGENT_VERSION|release_id"): build_release_archive( source, package, release_id=f"rf001-agent-{requested_version}", agent_version=requested_version, protocol_version=1, ) assert not package.exists()