* refactor(update): one predicate for serve rows outside the gateway matrix The inventory branch of `_marker_only_restart_obsolete` inlined the rule for which serve/dashboard rows the gateway matrix neither covers nor needs to (supervisor-owned, or a manual serve handed to its own reminder). The inventory-less branch needs the same rule for #118742, so it moves to `update_cmd_fleet_gatewayless.runtime_outside_gateway_evidence` and both branches will read one definition. No behaviour change. * fix(update): a Desktop-only host settles an inventory-less restart obligation A host that runs no gateway (the Desktop app alone) can be left with an inventory-less fleet-restart obligation: an updater that died before recording its inventory, or the pre-inventory writer. With no owed set, the live gateway matrix is its only evidence, and on that host the matrix is empty forever, so `_marker_only_restart_obsolete` never settled and every later `hermes update` exited 1 with "gateways are still off the checkout code" (#118742). An empty fleet alone cannot tell that host from one whose gateway the dying update stopped, so the inventory-less branch now asks the live host, never a historical receipt: `host_owes_no_gateway_restart` settles only when no profile's `gateway_state.json` claims a state other than stopped/startup_failed (a gateway that went away without a clean stop keeps the obligation) and every live runtime sits outside the gateway matrix (`runtime_outside_gateway_evidence`, shared with the inventory branch). HEAD must still contain the pulled SHA (`checkout_contains`, same rule as #119367). Probe failures keep it. Tests: the scoped-reconciliation matrix now holds its host at "the update stopped a gateway" so it keeps pinning receipt independence; a new host-evidence matrix covers Desktop-only, clean stop, carried commit, stopped gateway in a named profile, unclassified and unidentified serves, a gateway row without fleet identity, and a diverged checkout. Two manual-serve tests that assumed an empty fleet always stays pending now stub the live host and assert the manual reminder survives the gateway obligation settling. Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com> --------- Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com>
50 lines
2.5 KiB
Python
50 lines
2.5 KiB
Python
"""Gateway-less host evidence for the update-restart obligation (``update_cmd_fleet`` sibling).
|
|
|
|
An inventory-less obligation (the pre-inventory writer, or a tail that died before recording one)
|
|
has no owed set, so the live gateway matrix is its whole evidence. On a host that runs no gateway
|
|
(the Desktop app alone, #118742) that matrix is empty forever and the obligation could never
|
|
settle. An empty probe cannot tell that host from one whose gateway the dying update stopped, so
|
|
these readers ask the live host directly. A historical receipt is never consulted: it can belong
|
|
to an older update and says nothing about what runs now.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict
|
|
|
|
|
|
def runtime_outside_gateway_evidence(runtime: dict) -> bool:
|
|
"""A serve/dashboard row the gateway matrix neither covers nor needs to.
|
|
|
|
Its supervisor owns the restart (Desktop backend, launchd/systemd unit, Windows service), or it
|
|
is a manual serve whose restart ``defer_manual_serve`` has handed to its own durable reminder.
|
|
Unclassified backends and failed transfers stay evidence against settlement (#115090, #111494).
|
|
"""
|
|
from hermes_cli.update_cmd_fleet import _SUPERVISOR_OWNED_SERVE_BACKENDS
|
|
from hermes_cli.update_serve_obligations import defer_manual_serve
|
|
|
|
return runtime.get("kind") in ("serve", "dashboard") and (
|
|
defer_manual_serve(runtime) or runtime.get("supervisor") in _SUPERVISOR_OWNED_SERVE_BACKENDS
|
|
)
|
|
|
|
|
|
def host_owes_no_gateway_restart() -> bool:
|
|
"""True when no profile expects a gateway to be running and every live runtime is outside the matrix.
|
|
|
|
A ``gateway_state.json`` that does not say ``stopped``/``startup_failed`` belongs to a gateway
|
|
that went away without a clean stop, which is what an update that died mid-restart leaves
|
|
behind; it keeps the obligation. Profiles that never ran a gateway have no record at all.
|
|
"""
|
|
from gateway.status import read_runtime_status
|
|
from hermes_cli.update_inventory import collect_runtime_inventory
|
|
from hermes_cli.update_receipt import _NOT_EXPECTED_STATES, _profile_homes
|
|
|
|
for _profile, home in _profile_homes():
|
|
record = read_runtime_status(home / "gateway_state.json")
|
|
if record is None:
|
|
continue
|
|
state = record.get("gateway_state") if isinstance(record, dict) else None
|
|
if not (isinstance(state, str) and state in _NOT_EXPECTED_STATES):
|
|
return False
|
|
return all(runtime_outside_gateway_evidence(asdict(runtime)) for runtime in collect_runtime_inventory().runtimes)
|