"""Tests for detecting Jenkins builds that never reach a terminal result.""" from __future__ import annotations from ariadne.services import hermes_hung_builds as module NOW = 1_800_000_000.0 _MINUTE_MS = 60_000 def _build(minutes_ago: float, building: bool = True, **overrides): # type: ignore[no-untyped-def] payload = { "number": 272, "building": building, "timestamp": int((NOW - minutes_ago * 60) * 1000), "url": "https://ci.example/job/metis/272/", } payload.update(overrides) return payload def test_elapsed_minutes_measures_from_the_start_timestamp() -> None: assert module.elapsed_minutes(_build(75), now=NOW) == 75.0 def test_elapsed_minutes_treats_an_unreadable_start_as_zero() -> None: """An unreadable timestamp must never look like a hung build.""" assert module.elapsed_minutes({}, now=NOW) == 0.0 assert module.elapsed_minutes({"timestamp": None}, now=NOW) == 0.0 assert module.elapsed_minutes({"timestamp": "not-a-number"}, now=NOW) == 0.0 assert module.elapsed_minutes({"timestamp": 0}, now=NOW) == 0.0 assert module.elapsed_minutes({"timestamp": -5}, now=NOW) == 0.0 def test_elapsed_minutes_never_goes_negative() -> None: """A clock skewed into the future must not produce a negative age.""" assert module.elapsed_minutes(_build(-30), now=NOW) == 0.0 def test_elapsed_minutes_defaults_to_the_wall_clock() -> None: assert module.elapsed_minutes(_build(0.0, timestamp=1)) > 0.0 def test_a_running_build_past_the_cap_is_hung() -> None: assert module.is_hung(_build(75), 45.0, now=NOW) is True def test_a_running_build_within_the_cap_is_not_hung() -> None: assert module.is_hung(_build(10), 45.0, now=NOW) is False assert module.is_hung(_build(45), 45.0, now=NOW) is False def test_a_finished_build_is_never_hung() -> None: """Terminal builds are the normal path's business, not this one.""" assert module.is_hung(_build(600, building=False), 45.0, now=NOW) is False assert module.is_hung({"timestamp": 1}, 45.0, now=NOW) is False def test_a_non_positive_cap_disables_the_check() -> None: assert module.is_hung(_build(6000), 0.0, now=NOW) is False assert module.is_hung(_build(6000), -1.0, now=NOW) is False def test_hung_bundle_carries_only_observed_facts() -> None: bundle = module.hung_bundle("metis", _build(75), 45.0) jenkins = bundle["jenkins"] assert jenkins["job"] == "metis" assert jenkins["build_number"] == 272 assert jenkins["building"] is True assert jenkins["cap_minutes"] == 45.0 assert jenkins["console_failures"] == [] assert bundle["log_evidence"] == {"records": []} assert "has been running for" in jenkins["console_tail"] assert "holding a Jenkins agent slot" in jenkins["console_tail"] def test_hung_bundle_survives_a_build_with_nothing_in_it() -> None: """Bundle construction must not raise on a sparse Jenkins payload.""" bundle = module.hung_bundle("metis", {}, 45.0) assert bundle["jenkins"]["build_number"] is None assert bundle["jenkins"]["url"] == "" assert bundle["jenkins"]["elapsed_minutes"] == 0.0 def test_reason_and_classification_are_stable_labels() -> None: """These land in issue titles and event details, so they are frozen.""" assert module.HUNG_REASON == "hung_build" assert module.HUNG_CLASSIFICATION == "build_exceeded_time_cap"