Files
hermes-agent/tests/scripts/test_termux_update_refusal.py
ethernet 6a6771e5e7 termux: stamp the deb as a self-contained runtime, not a bundled payload
build_deb.sh wrote HERMES_DESKTOP_VARIANT=bundled, so the Termux stamp
carried payload=bundled and every 'bundled' reader treated the tree as
the repo/ of an Electron payload. hermes uninstall --data then called
resolve_bundle_layout on it and rejected the plan: the deb has no
enclosing app to protect, so data-only removal was impossible on Termux.

Add a 'runtime' variant to write_install_stamp.py for a sealed CLI
runtime with no desktop app around it. It stays sealed for the update
gate and channel identity (source_check, update_channel accept it next
to bundled/light) while is_bundled_payload keeps answering False, so
cleanup planning protects the APT-owned package tree the ordinary way
and never asks where the app is.
2026-09-21 18:37:21 -04:00

85 lines
3.7 KiB
Python

"""The installed-package gate distinguishes commit builds from release packages."""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
from hermes_cli.update_contract import COMMIT_BUILD_UPDATE_MESSAGE
@pytest.mark.platforms("posix")
def test_validator_child_decodes_utf8_and_preserves_strict_errors(tmp_path, monkeypatch):
import os
from scripts.termux.validate_installed import run
# Force a non-UTF-8 locale fallback without pretending to be a different OS.
monkeypatch.setattr(subprocess, "_text_encoding", lambda: "ascii")
message = "café 東京\n"
command = [sys.executable, "-c", f"import os; os.write(1, {message.encode('utf-8')!r})"]
assert run(command, dict(os.environ), tmp_path).stdout == message
with pytest.raises(subprocess.CalledProcessError) as failed:
run([*command[:-1], command[-1] + "; raise SystemExit(7)"], dict(os.environ), tmp_path)
assert failed.value.returncode == 7 and failed.value.stdout == message
with pytest.raises(UnicodeDecodeError):
run([*command[:-1], "import os; os.write(1, bytes([255]))"], dict(os.environ), tmp_path)
@pytest.fixture(params=["bundle", "commit-build"])
def artifact(tmp_path, request):
root = tmp_path / "app"
root.mkdir()
(root / "install-stamp.json").write_text(json.dumps({
"source": request.param,
"distribution": "apt-termux",
"payload": "runtime",
"updateMechanism": "external",
"commit": "a" * 40,
"tag": None if request.param == "commit-build" else "v1.2.3",
}), encoding="utf-8")
return root, request.param
@pytest.mark.platforms("posix")
def test_validator_accepts_real_cli_refusal_for_installed_identity(artifact, monkeypatch, capsys, tmp_path):
from hermes_cli import main
from scripts.termux.validate_installed import validate_update_refusal
root, source = artifact
monkeypatch.setattr(main, "PROJECT_ROOT", root)
monkeypatch.setattr("hermes_cli.image_provenance.IMAGE_PROVENANCE_PATH", tmp_path / "absent")
monkeypatch.delenv("HERMES_MANAGED", raising=False)
def unexpected_update(*args, **kwargs):
pytest.fail("sealed package reached the mutation path")
monkeypatch.setattr(main, "_install_hangup_protection", unexpected_update)
with pytest.raises(SystemExit) as stopped:
main.cmd_update(SimpleNamespace())
output = capsys.readouterr()
status = stopped.value.code
assert isinstance(status, int) and status == 2
expected = COMMIT_BUILD_UPDATE_MESSAGE if source == "commit-build" else "pkg upgrade hermes-agent"
assert expected in output.out + output.err
result = subprocess.CompletedProcess(["hermes", "update"], status, output.out, output.err)
validate_update_refusal(root, result)
marker = "COMMIT_BUILD_UPDATE_REFUSAL_OK" if source == "commit-build" else "APT_UPDATE_REFUSAL_OK"
assert marker in capsys.readouterr().out
@pytest.mark.platforms("posix")
def test_validator_rejects_wrong_refusal_or_exit_status(artifact):
from scripts.termux.validate_installed import validate_update_refusal
root, source = artifact
expected = COMMIT_BUILD_UPDATE_MESSAGE if source == "commit-build" else "pkg upgrade hermes-agent"
other = "pkg upgrade hermes-agent" if source == "commit-build" else COMMIT_BUILD_UPDATE_MESSAGE
for status, message in ((0, expected), (1, expected), (2, other), (2, "unrelated startup failure")):
result = subprocess.CompletedProcess(["hermes", "update"], status, message, "")
with pytest.raises(RuntimeError, match="wrong updater refusal"):
validate_update_refusal(root, result)