fix(update): ignore successful receipt stop reason
This commit is contained in:
@@ -78,14 +78,27 @@ def _current_checkout_sha() -> str | None:
|
||||
|
||||
|
||||
def _receipt_looks_unfinished(receipt: dict) -> bool:
|
||||
"""True when *receipt* is from an update that did not finish cleanly."""
|
||||
"""True when *receipt* is from an update that did not finish cleanly.
|
||||
|
||||
``stop_reason`` records *how* the command boundary closed the receipt
|
||||
(``completed at command boundary``, ``sys.exit(0)``, even KeyboardInterrupt
|
||||
on an otherwise successful run). A truthy stop_reason must not make a
|
||||
successful receipt look unfinished, or the next ``hermes update`` retriggers
|
||||
``fleet_restart_pending`` from pre-pull plan SHAs.
|
||||
"""
|
||||
exit_code = receipt.get("exit_code")
|
||||
outcome = receipt.get("outcome")
|
||||
if exit_code not in (0, None):
|
||||
return True
|
||||
if outcome in ("failed", "partial", "running"):
|
||||
return True
|
||||
gateway_restart = receipt.get("gateway_restart")
|
||||
return bool(
|
||||
receipt.get("stop_reason")
|
||||
or receipt.get("exit_code") not in (0, None)
|
||||
or receipt.get("outcome") in ("failed", "partial", "running")
|
||||
or (isinstance(gateway_restart, dict) and gateway_restart.get("incomplete"))
|
||||
)
|
||||
if isinstance(gateway_restart, dict) and gateway_restart.get("incomplete"):
|
||||
return True
|
||||
stop_reason = receipt.get("stop_reason")
|
||||
if stop_reason and outcome != "success" and exit_code != 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _receipt_reports_stale_runtime(expected_sha: str | None = None) -> bool:
|
||||
|
||||
@@ -254,6 +254,67 @@ def test_successful_receipt_with_pre_update_plan_shas_does_not_retrigger(
|
||||
assert update_cmd._pending_fleet_restart_needed() is False
|
||||
|
||||
|
||||
def test_successful_command_boundary_receipt_without_fleet_does_not_retrigger(
|
||||
monkeypatch,
|
||||
):
|
||||
"""A normal command-boundary stop is not an interrupted update."""
|
||||
disk_sha = "n" * 40
|
||||
old_sha = "o" * 40
|
||||
monkeypatch.setattr(update_cmd, "_current_checkout_sha", lambda: disk_sha)
|
||||
monkeypatch.setattr(update_cmd_fleet, "_current_checkout_sha", lambda: disk_sha)
|
||||
|
||||
receipt_dir = get_hermes_home() / "logs" / "update_receipts"
|
||||
receipt_dir.mkdir(parents=True)
|
||||
(receipt_dir / "latest.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"exit_code": 0,
|
||||
"outcome": "success",
|
||||
"stop_reason": "completed at command boundary",
|
||||
"plan": {
|
||||
"expected_sha": old_sha,
|
||||
"runtimes": [
|
||||
{
|
||||
"kind": "gateway",
|
||||
"profile": "default",
|
||||
"pid": 1,
|
||||
"code_sha": old_sha,
|
||||
}
|
||||
],
|
||||
},
|
||||
"fleet": [],
|
||||
"gateway_restart": {},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert update_cmd._pending_fleet_restart_needed() is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"receipt",
|
||||
[
|
||||
{"outcome": "success", "exit_code": 0, "stop_reason": "sys.exit(0)"},
|
||||
{"outcome": "success", "stop_reason": "KeyboardInterrupt: "},
|
||||
{"exit_code": 0, "stop_reason": "sys.exit(0)"},
|
||||
],
|
||||
)
|
||||
def test_successful_non_boundary_stop_reasons_are_finished(receipt):
|
||||
"""Successful sys.exit(0)/KeyboardInterrupt must not look unfinished."""
|
||||
assert update_cmd._receipt_looks_unfinished(receipt) is False
|
||||
|
||||
|
||||
def test_failed_interrupt_stop_reason_is_unfinished():
|
||||
assert update_cmd._receipt_looks_unfinished(
|
||||
{
|
||||
"outcome": "failed",
|
||||
"exit_code": 1,
|
||||
"stop_reason": "KeyboardInterrupt: ",
|
||||
}
|
||||
) is True
|
||||
|
||||
|
||||
def test_stale_fleet_matrix_on_latest_receipt_is_pending(monkeypatch):
|
||||
disk_sha = "n" * 40
|
||||
monkeypatch.setattr(update_cmd, "_current_checkout_sha", lambda: disk_sha)
|
||||
|
||||
Reference in New Issue
Block a user