"""Unit tests for `run_recipe_ci.resolve_upgrade_base` — the DYNAMIC upgrade-base decision (phase prevb). Resolution order: upgrade∉stages / EXPECTED_NA[upgrade] (declared skip) → explicit UPGRADE_BASE_VERSION override → last-green (warm canonical) → target-branch (`main`) tip → skip (no predecessor). The result is a `BasePlan(kind, version, ref, reason)`: kind ∈ {"version", "ref", "skip"}; `.runs` is True for version/ref (the upgrade tier runs). Replaces the old static `recipe_versions[-2]` default. """ from __future__ import annotations import os import sys from types import SimpleNamespace sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) import run_recipe_ci # noqa: E402 from harness import canonical, lifecycle # noqa: E402 ALL = {"install", "upgrade", "backup", "restore", "custom"} HEAD = "aaaa1111head" MAIN = "bbbb2222main" def _meta(expected_na=None, upgrade_base_version=None): return SimpleNamespace(EXPECTED_NA=expected_na, UPGRADE_BASE_VERSION=upgrade_base_version) def _no_canonical(monkeypatch): monkeypatch.setattr(canonical, "read_registry", lambda r: None) def _no_main(monkeypatch): monkeypatch.setattr(lifecycle, "recipe_branch_commit", lambda r, b="main": None) def test_upgrade_not_in_stages_skip(monkeypatch): # never consults canonical/main when upgrade isn't requested monkeypatch.setattr( canonical, "read_registry", lambda r: (_ for _ in ()).throw(AssertionError("not consulted")) ) plan = run_recipe_ci.resolve_upgrade_base(ALL - {"upgrade"}, _meta(), "somerecipe") assert plan.kind == "skip" and not plan.runs def test_expected_na_upgrade_skip_even_with_canonical_and_override(monkeypatch): # EXPECTED_NA[upgrade] is the strongest, declared fact — short-circuits before override/canonical. monkeypatch.setattr( canonical, "read_registry", lambda r: {"version": "9.9.9", "status": "warm"} ) declared = {"upgrade": "no deployable upgrade base (moving tag republished)"} plan = run_recipe_ci.resolve_upgrade_base( ALL, _meta(expected_na=declared, upgrade_base_version="0.2.0+v0.4"), "bluesky-pds" ) assert plan.kind == "skip" and "EXPECTED_NA" in plan.reason def test_explicit_override_wins_over_canonical(monkeypatch): monkeypatch.setattr( canonical, "read_registry", lambda r: {"version": "9.9.9", "status": "warm"} ) monkeypatch.setattr( lifecycle, "recipe_branch_commit", lambda r, b="main": (_ for _ in ()).throw(AssertionError("not consulted")), ) plan = run_recipe_ci.resolve_upgrade_base( ALL, _meta(upgrade_base_version="0.7.0+3.3.1"), "discourse" ) assert plan.kind == "version" and plan.version == "0.7.0+3.3.1" and plan.runs def test_last_green_warm_canonical_is_primary(monkeypatch): # no override → last-green (warm canonical version) is the PRIMARY base; main is not consulted. monkeypatch.setattr( canonical, "read_registry", lambda r: {"version": "0.6.0+3.1.1", "status": "idle"} ) monkeypatch.setattr( lifecycle, "recipe_branch_commit", lambda r, b="main": (_ for _ in ()).throw(AssertionError("not consulted")), ) plan = run_recipe_ci.resolve_upgrade_base(ALL, _meta(), "keycloak", head_ref=HEAD) assert plan.kind == "version" and plan.version == "0.6.0+3.1.1" and "last-green" in plan.reason def test_main_tip_fallback_when_no_last_green(monkeypatch): _no_canonical(monkeypatch) monkeypatch.setattr(lifecycle, "recipe_branch_commit", lambda r, b="main": MAIN) plan = run_recipe_ci.resolve_upgrade_base(ALL, _meta(), "discourse", head_ref=HEAD) assert plan.kind == "ref" and plan.ref == MAIN and plan.version is None and plan.runs def test_head_equals_main_tip_skip(monkeypatch): # the PR head IS the main tip (empty PR / LATEST run) → no real predecessor → declared skip. _no_canonical(monkeypatch) monkeypatch.setattr(lifecycle, "recipe_branch_commit", lambda r, b="main": HEAD) plan = run_recipe_ci.resolve_upgrade_base(ALL, _meta(), "discourse", head_ref=HEAD) assert plan.kind == "skip" and "main tip" in plan.reason and not plan.runs def test_no_canonical_no_main_skip(monkeypatch): _no_canonical(monkeypatch) _no_main(monkeypatch) plan = run_recipe_ci.resolve_upgrade_base(ALL, _meta(), "brandnew", head_ref=HEAD) assert plan.kind == "skip" and "no predecessor" in plan.reason def test_expected_na_other_rung_does_not_suppress_upgrade(monkeypatch): # an EXPECTED_NA for a DIFFERENT rung (backup_restore) must NOT short-circuit the upgrade base — # resolution proceeds to last-green/main-tip (custom-html-tiny shape). _no_canonical(monkeypatch) monkeypatch.setattr(lifecycle, "recipe_branch_commit", lambda r, b="main": MAIN) meta = _meta(expected_na={"backup_restore": "stateless"}) plan = run_recipe_ci.resolve_upgrade_base(ALL, meta, "custom-html-tiny", head_ref=HEAD) assert plan.kind == "ref" and plan.ref == MAIN