fix(release): package TUI compliance materials

This commit is contained in:
Xubin Ren
2026-08-17 20:56:10 +08:00
parent 7838e244ba
commit b17f227eb9
19 changed files with 1686 additions and 83 deletions
+118 -17
View File
@@ -1,5 +1,7 @@
import hashlib
import io
import subprocess
import zipfile
from pathlib import Path
from types import SimpleNamespace
@@ -24,6 +26,32 @@ from nanobot.cli.tui_launcher import (
from nanobot.config.schema import Config, ModelPresetConfig
def _release_archive(
asset: str,
*,
binary: bytes = b"native-tui",
omit: str | None = None,
) -> tuple[bytes, bytes]:
files = {
asset: binary,
**{name: f"contents of {name}\n".encode() for name in tui_launcher._TUI_RELEASE_FILES},
}
if omit:
files.pop(omit)
manifest = "".join(
f"{hashlib.sha256(content).hexdigest()} {name}\n" for name, content in files.items()
).encode()
files["MANIFEST.sha256"] = manifest
output = io.BytesIO()
with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as archive:
for name, content in files.items():
archive.writestr(name, content)
payload = output.getvalue()
archive_name = f"{asset}.zip"
checksum = f"{hashlib.sha256(payload).hexdigest()} {archive_name}\n".encode()
return payload, checksum
def test_authenticated_ws_url_preserves_existing_query(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("nanobot.cli.tui_launcher.os.getpid", lambda: 42)
url = _authenticated_ws_url(
@@ -374,26 +402,28 @@ def test_release_tui_is_verified_and_cached(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
asset = "nanobot-tui-linux-x64"
binary = b"native-tui"
digest = hashlib.sha256(binary).hexdigest().encode()
archive, checksum = _release_archive(asset, binary=binary)
downloads: list[str] = []
def read_asset(url: str, *, max_bytes: int) -> bytes:
downloads.append(url)
return digest if url.endswith(".sha256") else binary
return checksum if url.endswith(".sha256") else archive
monkeypatch.setattr("nanobot.cli.tui_launcher.__version__", "9.9.9")
monkeypatch.setattr("nanobot.cli.tui_launcher.get_data_dir", lambda: tmp_path)
monkeypatch.setattr("nanobot.cli.tui_launcher._read_release_asset", read_asset)
target = _download_release_tui("nanobot-tui-linux-x64")
target = _download_release_tui(asset)
assert target == tmp_path / "bin" / "tui" / "9.9.9" / "nanobot-tui-linux-x64"
assert target == tmp_path / "bin" / "tui" / "9.9.9" / asset
assert target.read_bytes() == binary
assert target.with_name(f"{target.name}.sha256").read_text().startswith(digest.decode())
for name in (*tui_launcher._TUI_RELEASE_FILES, "MANIFEST.sha256"):
assert (target.parent / name).is_file()
assert len(downloads) == 2
assert _download_release_tui("nanobot-tui-linux-x64") == target
assert _download_release_tui(asset) == target
assert len(downloads) == 2
@@ -402,42 +432,113 @@ def test_release_tui_replaces_a_corrupted_cached_binary(
tmp_path: Path,
) -> None:
binary = b"native-tui"
digest = hashlib.sha256(binary).hexdigest().encode()
asset = "nanobot-tui-linux-x64"
target = tmp_path / "bin" / "tui" / "9.9.9" / asset
target.parent.mkdir(parents=True)
target.write_bytes(b"corrupted")
target.with_name(f"{target.name}.sha256").write_text(
f"{hashlib.sha256(binary).hexdigest()} {asset}\n"
)
archive, checksum = _release_archive(asset, binary=binary)
downloads: list[str] = []
def read_asset(url: str, *, max_bytes: int) -> bytes:
downloads.append(url)
return digest if url.endswith(".sha256") else binary
return checksum if url.endswith(".sha256") else archive
monkeypatch.setattr("nanobot.cli.tui_launcher.__version__", "9.9.9")
monkeypatch.setattr("nanobot.cli.tui_launcher.get_data_dir", lambda: tmp_path)
monkeypatch.setattr("nanobot.cli.tui_launcher._read_release_asset", read_asset)
target = _download_release_tui(asset)
assert target is not None
target.write_bytes(b"corrupted")
assert _download_release_tui(asset) == target
assert target.read_bytes() == binary
assert len(downloads) == 2
assert len(downloads) == 4
def test_release_tui_replaces_corrupted_cached_notices(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
asset = "nanobot-tui-linux-x64"
archive, checksum = _release_archive(asset)
downloads: list[str] = []
def read_asset(url: str, *, max_bytes: int) -> bytes:
downloads.append(url)
return checksum if url.endswith(".sha256") else archive
monkeypatch.setattr("nanobot.cli.tui_launcher.__version__", "9.9.9")
monkeypatch.setattr("nanobot.cli.tui_launcher.get_data_dir", lambda: tmp_path)
monkeypatch.setattr("nanobot.cli.tui_launcher._read_release_asset", read_asset)
target = _download_release_tui(asset)
assert target is not None
notices = target.parent / "THIRD_PARTY_NOTICES.txt"
notices.write_text("corrupted", encoding="utf-8")
assert _download_release_tui(asset) == target
assert notices.read_bytes() == b"contents of THIRD_PARTY_NOTICES.txt\n"
assert len(downloads) == 4
def test_release_tui_rejects_bad_checksum(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
asset = "nanobot-tui-linux-x64"
archive, _checksum = _release_archive(asset)
monkeypatch.setattr("nanobot.cli.tui_launcher.__version__", "9.9.9")
monkeypatch.setattr("nanobot.cli.tui_launcher.get_data_dir", lambda: tmp_path)
monkeypatch.setattr(
"nanobot.cli.tui_launcher._read_release_asset",
lambda url, *, max_bytes: b"0" * 64 if url.endswith(".sha256") else b"tampered",
lambda url, *, max_bytes: (
f"{'0' * 64} {asset}.zip\n".encode() if url.endswith(".sha256") else archive
),
)
with pytest.raises(TuiUnavailableError, match="checksum"):
_download_release_tui("nanobot-tui-linux-x64")
_download_release_tui(asset)
def test_release_tui_rejects_an_archive_without_required_notices(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
asset = "nanobot-tui-linux-x64"
archive, checksum = _release_archive(asset, omit="THIRD_PARTY_NOTICES.txt")
monkeypatch.setattr("nanobot.cli.tui_launcher.__version__", "9.9.9")
monkeypatch.setattr("nanobot.cli.tui_launcher.get_data_dir", lambda: tmp_path)
monkeypatch.setattr(
"nanobot.cli.tui_launcher._read_release_asset",
lambda url, *, max_bytes: checksum if url.endswith(".sha256") else archive,
)
with pytest.raises(TuiUnavailableError, match="archive is incomplete"):
_download_release_tui(asset)
def test_release_tui_rejects_an_empty_required_file(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
asset = "nanobot-tui-linux-x64"
archive, _checksum = _release_archive(asset)
source = io.BytesIO(archive)
output = io.BytesIO()
with zipfile.ZipFile(source) as original, zipfile.ZipFile(output, "w") as rebuilt:
for entry in original.infolist():
content = b"" if entry.filename == "SOURCE_OFFER.md" else original.read(entry)
rebuilt.writestr(entry.filename, content)
payload = output.getvalue()
checksum = f"{hashlib.sha256(payload).hexdigest()} {asset}.zip\n".encode()
monkeypatch.setattr("nanobot.cli.tui_launcher.__version__", "9.9.9")
monkeypatch.setattr("nanobot.cli.tui_launcher.get_data_dir", lambda: tmp_path)
monkeypatch.setattr(
"nanobot.cli.tui_launcher._read_release_asset",
lambda url, *, max_bytes: checksum if url.endswith(".sha256") else payload,
)
with pytest.raises(TuiUnavailableError, match="invalid size"):
_download_release_tui(asset)
def test_gateway_reuse_requires_the_matching_managed_instance(