Skip to content

Commit 7161e9a

Browse files
committed
test(haos): retry Supervisor setup readiness
1 parent f4b40e7 commit 7161e9a

2 files changed

Lines changed: 63 additions & 6 deletions

File tree

tests/haos_image_build/build_image.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1747,6 +1747,22 @@ def _is_transient_supervisor_error(exc: BaseException) -> bool:
17471747
return exc.code == "unknown_error" and exc.supervisor_message == ""
17481748

17491749

1750+
def _is_transient_supervisor_readiness_error(exc: BaseException) -> bool:
1751+
"""Return whether readiness polling can retry a Supervisor failure.
1752+
1753+
Mutation responses stay strict because their outcome can be ambiguous. A
1754+
read during a known restart window may also observe Supervisor's setup
1755+
state before the API becomes ready again.
1756+
"""
1757+
return _is_transient_supervisor_error(exc) or (
1758+
isinstance(exc, WSCommandError)
1759+
and exc.code == "unknown_error"
1760+
and (exc.supervisor_message or "").startswith(
1761+
"System is not ready with state: "
1762+
)
1763+
)
1764+
1765+
17501766
def _supervisor_info_ready(
17511767
info: dict[str, Any],
17521768
*,
@@ -1811,7 +1827,7 @@ def _wait_supervisor_ready(
18111827
try:
18121828
info = ws.supervisor_api("/supervisor/info", method="get", timeout=30.0)
18131829
except _SUPERVISOR_WAIT_TRANSIENT_ERRORS as e:
1814-
if not _is_transient_supervisor_error(e):
1830+
if not _is_transient_supervisor_readiness_error(e):
18151831
raise
18161832
# A Supervisor self-update can return a transient command error or
18171833
# drop the WebSocket. Preserve the failure for timeout diagnostics,
@@ -1821,7 +1837,7 @@ def _wait_supervisor_ready(
18211837
try:
18221838
ws.reconnect()
18231839
except _SUPERVISOR_WAIT_TRANSIENT_ERRORS as reconnect_err:
1824-
if not _is_transient_supervisor_error(reconnect_err):
1840+
if not _is_transient_supervisor_readiness_error(reconnect_err):
18251841
raise
18261842
# Handler-start timeouts and transport failures are expected
18271843
# while Supervisor/Core restarts; keep them visible in CI logs.
@@ -1863,7 +1879,7 @@ def _wait_core_version(
18631879
ws.reconnect()
18641880
last_info = ws.supervisor_api("/core/info", method="get", timeout=30.0)
18651881
except _SUPERVISOR_WAIT_TRANSIENT_ERRORS as exc:
1866-
if not _is_transient_supervisor_error(exc):
1882+
if not _is_transient_supervisor_readiness_error(exc):
18671883
raise
18681884
last_error = exc
18691885
LOG.debug("Core still restarting after update: %r", exc)
@@ -1894,7 +1910,7 @@ def _reconnect_during_supervisor_update(
18941910
try:
18951911
ws.reconnect()
18961912
except _SUPERVISOR_WAIT_TRANSIENT_ERRORS as exc:
1897-
if not _is_transient_supervisor_error(exc):
1913+
if not _is_transient_supervisor_readiness_error(exc):
18981914
raise
18991915
LOG.warning("Reconnect %s failed: %r", context, exc)
19001916
return exc
@@ -1917,7 +1933,7 @@ def _wait_supervisor_channel_metadata(
19171933
"/supervisor/info", method="get", timeout=30.0
19181934
)
19191935
except _SUPERVISOR_WAIT_TRANSIENT_ERRORS as exc:
1920-
if not _is_transient_supervisor_error(exc):
1936+
if not _is_transient_supervisor_readiness_error(exc):
19211937
raise
19221938
last_error = exc
19231939
LOG.debug("Transient Supervisor reload failure: %r", exc)
@@ -1985,7 +2001,7 @@ def _apply_supervisor_image_update(
19852001
except _SupervisorReadinessTimeout:
19862002
raise
19872003
except _SUPERVISOR_WAIT_TRANSIENT_ERRORS as exc:
1988-
if not _is_transient_supervisor_error(exc):
2004+
if not _is_transient_supervisor_readiness_error(exc):
19892005
raise
19902006
if time.monotonic() >= deadline:
19912007
raise TimeoutError(

tests/src/unit/test_haos_supervisor_wait.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,47 @@ def test_configure_beta_variant_tolerates_restart_error_from_update_call() -> No
703703
)
704704

705705

706+
def test_configure_beta_variant_tolerates_setup_state_while_polling() -> None:
707+
"""A setup-state read during restart is retried after the update POST."""
708+
ws = Mock()
709+
ws.supervisor_api.side_effect = [
710+
{},
711+
{},
712+
_info(
713+
update_available=True,
714+
version="2026.07.5",
715+
version_latest="2026.08.0",
716+
channel="beta",
717+
),
718+
{},
719+
WSCommandError(
720+
"not ready",
721+
code="unknown_error",
722+
supervisor_message="System is not ready with state: setup",
723+
),
724+
_info(
725+
update_available=False,
726+
version="2026.08.0",
727+
version_latest="2026.08.0",
728+
channel="beta",
729+
),
730+
]
731+
732+
with patch("tests.haos_image_build.build_image.time.sleep"):
733+
_configure_supervisor_image_variant(
734+
ws,
735+
channel="beta",
736+
minimum_version="2026.08.0",
737+
)
738+
739+
ws.reconnect.assert_called_once_with()
740+
assert ws.supervisor_api.call_args_list[-1] == call(
741+
"/supervisor/info",
742+
method="get",
743+
timeout=30.0,
744+
)
745+
746+
706747
def test_configure_beta_variant_rejects_permanent_unknown_update_error() -> None:
707748
"""A bridged permanent rejection is not mistaken for a restart."""
708749
ws = Mock()

0 commit comments

Comments
 (0)