"""Version matrix and hot-swap gating.""" from httpx import ASGITransport, AsyncClient from app.config import Settings from app.main import create_app from app.vsphere.contracts.matrix import ( VERSIONS, available_for_request, catalog_entries_for_major, methods_for_major, ) from tests.unit.test_health import FakeDatabase def test_major_6_smaller_than_major_9() -> None: assert len(methods_for_major(6)) < len(methods_for_major(9)) assert len(methods_for_major(7)) <= len(methods_for_major(8)) assert len(methods_for_major(8)) <= len(methods_for_major(9)) def test_runtime_serves_all_registered_regardless_of_major() -> None: """Catalog floors remain for browse; runtime never 501s known paths.""" assert available_for_request("POST", "/api/cis/tagging/category", 6) is True assert available_for_request("GET", "/api/content/library", 6) is True assert available_for_request("POST", "/api/appliance/networking/dns/hostname", 6) is True def test_catalog_floor_still_shrinks_browse_list() -> None: from app.vsphere.contracts.matrix import methods_for_major assert ("POST", "/api/cis/tagging/category") not in methods_for_major(6) assert ("POST", "/api/cis/tagging/category") in methods_for_major(7) assert ("GET", "/api/content/library") not in methods_for_major(7) assert ("GET", "/api/content/library") in methods_for_major(8) def test_literal_path_beats_param_template() -> None: """`/api/content/library/item` must not resolve as `/{library_id}`.""" from app.vsphere.contracts.matrix import resolve_template assert resolve_template("GET", "/api/content/library/item") == "/api/content/library/item" assert available_for_request("GET", "/api/content/library/item", 8) is True def test_catalog_entries_match_version() -> None: for major in VERSIONS: entries = catalog_entries_for_major(major) assert all(e["status"] in {"implemented", "stub"} for e in entries) assert len(entries) == len(methods_for_major(major)) async def test_hot_swap_does_not_501_registered_paths() -> None: app = create_app( settings=Settings(enable_pve_stub=False), database_factory=lambda _settings: FakeDatabase(True), worker_factories=(), ) async with app.router.lifespan_context(app): async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: apply = await client.post("/ui/api/contract/apply", params={"major": 6}) assert apply.status_code == 200 assert apply.json()["runtime_version"] == "7.0.0" # Session required for library — but must not be version-gated 501. library = await client.get("/api/content/library") assert library.status_code != 501 restore = await client.post("/ui/api/contract/apply", params={"major": 9}) assert restore.status_code == 200