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.
85 lines
3.7 KiB
Python
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)
|