Files
SDR-Rover/tests/test_rf001_agent_update.py
2026-09-30 11:42:19 +03:00

173 lines
5.9 KiB
Python

"""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()