All checks were successful
continuous-integration/drone/push Build is passing
- should_promote_canonical gains a `tagged` requirement (canon §2.A): a green cold latest run promotes only when the tested head version is a published release tag; an untagged main commit never becomes a canonical. - warm_reconcile.is_released_version(recipe, version): release-tag membership (exact or by version_key). Caller computes `tagged` so the gate stays pure. - unit tests: untagged -> no promote; is_released_version cases. - drive-by (pre-existing reds, unrelated to canon, now green): test_warm_reconcile traefik assertion was stale vs the phase-pxgate spec (probes /api/version, no health_domain); meta.py UPGRADE_BASE_VERSION KEYS help synced to the prevb doc text. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
97 lines
4.2 KiB
Python
97 lines
4.2 KiB
Python
"""Unit tests for the WC1.2 safety-gate + version helpers in runner/warm_reconcile.py.
|
|
|
|
Pure logic only (no abra/docker). The reconcile flow itself is proven live on cc-ci against the warm
|
|
keycloak (W0.6). These lock the gate's correctness: which bumps auto-apply vs hold, and the
|
|
manual-migration marker scan.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
|
import warm_reconcile as wr # noqa: E402
|
|
|
|
|
|
def test_is_version_tag():
|
|
assert wr.is_version_tag("10.7.1+26.6.2")
|
|
assert wr.is_version_tag("3.2.0")
|
|
assert not wr.is_version_tag("main")
|
|
assert not wr.is_version_tag("latest")
|
|
assert not wr.is_version_tag("")
|
|
|
|
|
|
def test_sort_and_latest():
|
|
tags = ["10.6.0+26.5.4", "10.7.1+26.6.2", "10.5.1+26.4.5", "main", "10.7.0+26.6.1"]
|
|
assert wr.latest_version(tags) == "10.7.1+26.6.2"
|
|
assert wr.sort_versions(tags)[0] == "10.5.1+26.4.5"
|
|
|
|
|
|
def test_latest_none_when_no_tags():
|
|
assert wr.latest_version(["main", "feature-x"]) is None
|
|
|
|
|
|
def test_is_released_version(monkeypatch):
|
|
# canon §2.A tagged-promote gate. is_released_version(recipe, version) → True iff version is a
|
|
# published release tag (exact or by version_key). recipe_tags is the only I/O — monkeypatch it.
|
|
tags = ["1.13.0+1.31.1", "1.12.0+1.30.0", "main", "feature-x"]
|
|
monkeypatch.setattr(wr, "recipe_tags", lambda r: tags)
|
|
assert wr.is_released_version("custom-html", "1.13.0+1.31.1") is True # exact tag
|
|
assert wr.is_released_version("custom-html", "1.12.0+1.30.0") is True
|
|
assert wr.is_released_version("custom-html", "9.9.9+9.9.9") is False # not a tag → untagged
|
|
assert wr.is_released_version("custom-html", None) is False
|
|
assert wr.is_released_version("custom-html", "") is False
|
|
|
|
|
|
def test_is_released_version_no_tags(monkeypatch):
|
|
# a recipe that has never cut a release → no version is a release
|
|
monkeypatch.setattr(wr, "recipe_tags", lambda r: ["main"])
|
|
assert wr.is_released_version("never-released", "0.1.0") is False
|
|
|
|
|
|
def test_minor_patch_bump_not_major():
|
|
# recipe-semver 10.7.0 -> 10.7.1 (patch); app 26.6.1 -> 26.6.2 (patch). Auto-apply.
|
|
assert wr.is_major_bump("10.7.0+26.6.1", "10.7.1+26.6.2") is False
|
|
# minor recipe bump 10.7.1 -> 10.8.0. Auto-apply.
|
|
assert wr.is_major_bump("10.7.1+26.6.2", "10.8.0+26.6.2") is False
|
|
|
|
|
|
def test_recipe_major_bump_held():
|
|
# recipe-semver 10.x -> 11.0 (major). HELD.
|
|
assert wr.is_major_bump("10.7.1+26.6.2", "11.0.0+26.6.2") is True
|
|
|
|
|
|
def test_app_major_bump_held():
|
|
# app version 26.x -> 27.0 (major, e.g. keycloak DB migration era). HELD (conservative).
|
|
assert wr.is_major_bump("10.7.1+26.6.2", "10.8.0+27.0.0") is True
|
|
|
|
|
|
def test_app_major_bump_held_even_if_no_plus_on_current():
|
|
assert wr.is_major_bump("0", "11.0.0+1.0.0") is True
|
|
|
|
|
|
def test_traefik_spec_is_stateless_with_setup():
|
|
# WC1.1 traefik = stateless (version-rollback-only, NO snapshot) + its own cert/file-provider
|
|
# setup. Health is probed on traefik's OWN /api/version endpoint (phase pxgate A1: probing the
|
|
# dashboard host caused a cold-boot deadlock since deploy-dashboard is After=deploy-proxy, so the
|
|
# spec carries no `health_domain` override and health_code() falls back to the app domain).
|
|
t = wr.SPECS["traefik"]
|
|
assert t["stateful"] is False
|
|
assert callable(t.get("setup"))
|
|
assert "health_domain" not in t # probes traefik's own domain, not a routed dashboard host
|
|
assert t["health_path"] == "/api/version"
|
|
assert t["domain"] == "traefik.ci.commoninternet.net"
|
|
# keycloak stays stateful with no custom setup (default path)
|
|
assert wr.SPECS["keycloak"]["stateful"] is True
|
|
assert "setup" not in wr.SPECS["keycloak"]
|
|
|
|
|
|
def test_manual_migration_markers():
|
|
assert wr.notes_flag_manual_migration("This release requires a MANUAL MIGRATION of the DB.")
|
|
assert wr.notes_flag_manual_migration("Breaking change: action required before upgrade.")
|
|
assert wr.notes_flag_manual_migration("You must run the migration by hand.")
|
|
assert not wr.notes_flag_manual_migration("Routine patch. Automatic, no action needed.")
|
|
assert not wr.notes_flag_manual_migration("")
|