Files
hermes-agent/tests/scripts/desktop_update/test_desktop_update_gateway_flag.py
Teknium 260cef995a fix(update): every owed follow-up reaches the Desktop hand-off result
A committed update exits 0 and prints one whole "Update follow-up '<step>'
did not finish:" line per owed step (update_receipt.record_followup). The
hand-offs only reacted to the Desktop build line, so a gateway proven to
run old code (gateway_restart), a refused Windows resume, a lost
completion or a failed channel adoption ended as a plain
"Update complete.".

posix.sh collects the distinct owed steps from that line
(owed_followup_steps) and turns them into the manual result message
(DONE_NOTE, the only channel the main-based script has); a gateway_restart
adds the `hermes gateway restart` hint. windows.ps1 does the same through
its manual outcome. With #132354's add_warning protocol the same
owed_followup_steps output feeds one add_warning per step.

record_followup's docstring names the line as a protocol; a contract test
runs the real posix.sh parser and the windows.ps1 regex against it.

Review: kshitijk4poor 5411222454 regression 1 / open thread F6.
2026-10-06 03:44:49 -07:00

136 lines
6.0 KiB
Python

"""The hand-off's `--gateway` flag, exercised against the real posix script.
`hermes update --gateway` (re)starts the local messaging gateway after the
update. A Desktop served by a remote gateway (#117529) must not ask for that:
the restarted local gateway shares the remote host's channel credentials and
becomes a competing long-poll consumer — Telegram answers the conflict by
rejecting one of the two `getUpdates` callers, taking the production bot
offline. The Desktop therefore passes `--no-gateway` whenever its active
connection is remote-shaped; these tests pin both sides of that contract
against the real `posix.sh`, so neither the default (local ownership) nor the
opt-out can silently regress.
"""
from __future__ import annotations
import json
import os
import subprocess
import time
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
SHIM_DIR = REPO_ROOT / "scripts" / "desktop-update"
requires_posix_handoff = pytest.mark.skipif(
not (os.path.exists("/bin/bash") and os.path.exists("/usr/bin/python3")),
reason="posix.sh detaches through /bin/bash and /usr/bin/python3",
)
# Stands in for `hermes`: answers the `update --help` probe (so --keep-stash
# is kept), and appends every non-help invocation's argv as one JSON line so
# the tests can inspect exactly what the update was invoked with.
FAKE_HERMES = """#!/usr/bin/env bash
case "$*" in *--help*) echo "--keep-stash"; exit 0 ;; esac
printf '%s\\n' "$*" >> "$HERMES_TEST_ARGV"
case " $* " in *" update "*) printf '%s\\n' "${HERMES_TEST_UPDATE_OUTPUT:-}" ;; esac
exit 0
"""
def _run_handoff(tmp_path: Path, extra_args: list[str], update_output: str = "") -> list[str]:
"""Run the real hand-off end to end; return the argv of each hermes call."""
install_root = tmp_path / "hermes-agent"
(install_root / "venv" / "bin").mkdir(parents=True)
hermes = install_root / "venv" / "bin" / "hermes"
hermes.write_text(FAKE_HERMES)
hermes.chmod(0o755)
argv_log = tmp_path / "argv.jsonl"
# posix.sh honours an ambient HERMES_HOME; the result file must land where this test looks.
env = {**os.environ, "TMPDIR": str(tmp_path), "HERMES_TEST_ARGV": str(argv_log), "HERMES_HOME": str(tmp_path),
"HERMES_TEST_UPDATE_OUTPUT": update_output}
subprocess.run(
["/bin/bash", str(SHIM_DIR / "posix.sh"), "--install-root", str(install_root), "--no-ui", *extra_args],
env=env,
timeout=60,
check=True,
)
result = tmp_path / ".hermes-update-result.json"
deadline = time.monotonic() + 45
while time.monotonic() < deadline and not result.exists():
time.sleep(0.1)
assert result.exists(), "hand-off never wrote its result file"
return argv_log.read_text().splitlines()
@requires_posix_handoff
def test_default_handoff_asks_for_the_local_gateway(tmp_path):
"""A locally-served Desktop owns its gateway: the update must restart it."""
calls = _run_handoff(tmp_path, [])
update_calls = [c for c in calls if " update " in f" {c} "]
assert update_calls, "hand-off never ran hermes update"
assert "--gateway" in update_calls[0].split()
@requires_posix_handoff
def test_no_gateway_flag_omits_gateway_from_update(tmp_path):
"""A remote-served Desktop (#117529): no local gateway may be (re)started.
The competing long-poll consumer is silent channel outage risk, and the
flag must survive the hand-off's own retry path — every update invocation
is checked, not just the first.
"""
calls = _run_handoff(tmp_path, ["--no-gateway"])
update_calls = [c for c in calls if " update " in f" {c} "]
assert update_calls, "hand-off never ran hermes update"
for call in update_calls:
argv = call.split()
assert "--gateway" not in argv, f"--gateway reappeared in update argv: {call}"
assert "--keep-stash" in argv, "--no-gateway must not disturb --keep-stash"
assert "--yes" in argv
@requires_posix_handoff
def test_owed_desktop_build_is_a_manual_outcome_even_when_the_followup_is_truncated(tmp_path):
"""Contract C3: the update committed (exit 0) but this app was not rebuilt.
The receipt follow-up is capped at 500 chars and leads with whichever product failed first,
so it can lose the desktop part; the CLI's own whole-line marker is what the hand-off reads.
"""
followup = " ⚠ Update follow-up 'build' did not finish: feature dependencies: " + "x" * 440
_run_handoff(tmp_path, [], f"{followup}\n Desktop app build owed: desktop app build failed\n✓ Update complete!")
result = json.loads((tmp_path / ".hermes-update-result.json").read_text(encoding="utf-8-sig"))
assert result["exit_code"] == 0
assert result["manual"] is True
assert "could not be rebuilt" in result["message"]
@requires_posix_handoff
@pytest.mark.parametrize("step", ["gateway_restart", "windows_resume", "channel_adoption", "completion"])
def test_every_owed_followup_reaches_the_desktop_result(tmp_path, step):
"""Contract C3 (review regression 1): a committed update exits 0, but a follow-up it still owes
(a gateway proven to run old code, a refused resume, a lost completion) must never end as a
plain "Update complete." -- the result names the owed step."""
line = f" ⚠ Update follow-up '{step}' did not finish: old code still serving (the next launch retries it)"
_run_handoff(tmp_path, [], f"{line}\n✓ Update complete!")
result = json.loads((tmp_path / ".hermes-update-result.json").read_text(encoding="utf-8-sig"))
assert result["exit_code"] == 0 and result["ok"] is True
assert result["manual"] is True
assert step in result["message"]
assert ("hermes gateway restart" in result["message"]) is (step == "gateway_restart")
@requires_posix_handoff
def test_a_clean_update_stays_plain_success(tmp_path):
_run_handoff(tmp_path, [], "✓ Update complete!")
result = json.loads((tmp_path / ".hermes-update-result.json").read_text(encoding="utf-8-sig"))
assert result["exit_code"] == 0 and result["manual"] is False
assert result["message"] == "Update complete."