Add a stateful Proxmox API console and broad handler coverage beyond the
initial QEMU slice, backed by imported contracts for majors 6–9. - Implement durable handlers for access/auth, cluster, LXC, storage, HA, firewall, Ceph, SDN, ACME, notifications, pools, mapping, and node ops - Serve an interactive Web UI with catalog browsing, demo seed controls, and OpenAPI/help surfaces - Bundle PVE 6.4-15, 7.4-16, and 8.4.5 contract revisions alongside 9.2.3 - Support in-memory runtime contract Apply (POST /ui/api/contract/apply) so /version and /api2 routes follow the selected major until restart - Expand seed profiles (including demo-cluster), migrations 007–008, TLS gateway config, Compose/Makefile tooling, and compatibility evidence - Tighten .gitignore for macOS, hidden directories (.*/), and local secrets
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"""Runtime contract hot-swap tests."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from app.config import Settings
|
||||
from app.main import create_app
|
||||
from tests.unit.test_health import FakeDatabase
|
||||
|
||||
_BUNDLED_9 = Path(
|
||||
"contracts/e61a893e996d05d376579226e7dfbedbcfce8b71787adacffbc557e6e35901c1/snapshot.json"
|
||||
)
|
||||
_EVIDENCE_9 = Path("evidence/pve-9.2.3.json")
|
||||
|
||||
|
||||
def _app() -> FastAPI:
|
||||
settings = Settings(
|
||||
contract_snapshot=_BUNDLED_9,
|
||||
compatibility_evidence=_EVIDENCE_9,
|
||||
)
|
||||
return create_app(
|
||||
settings=settings,
|
||||
database_factory=lambda _settings: FakeDatabase(True),
|
||||
worker_factories=(),
|
||||
)
|
||||
|
||||
|
||||
async def test_contract_apply_swaps_version_and_routes() -> None:
|
||||
app = _app()
|
||||
async with app.router.lifespan_context(app):
|
||||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||
before = await client.get("/api2/json/version")
|
||||
assert before.status_code == 200
|
||||
assert before.json()["data"]["version"] == "9.2.3"
|
||||
assert before.json()["data"]["release"] == "9.2"
|
||||
|
||||
versions = await client.get("/ui/api/versions")
|
||||
assert versions.status_code == 200
|
||||
assert versions.json()["runtime_version"] == "9.2.3"
|
||||
|
||||
applied = await client.post("/ui/api/contract/apply", params={"major": 7})
|
||||
assert applied.status_code == 200
|
||||
payload = applied.json()
|
||||
assert payload["ok"] is True
|
||||
assert payload["major"] == 7
|
||||
assert payload["runtime_version"] == "7.4-16"
|
||||
assert payload["path_count"] > 0
|
||||
assert payload["method_count"] > 0
|
||||
|
||||
after = await client.get("/api2/json/version")
|
||||
assert after.status_code == 200
|
||||
assert after.json()["data"]["version"] == "7.4-16"
|
||||
assert after.json()["data"]["release"] == "7.4"
|
||||
|
||||
versions_after = await client.get("/ui/api/versions")
|
||||
assert versions_after.json()["runtime_version"] == "7.4-16"
|
||||
|
||||
# Still routed (handler or 501), not a missing route / 404.
|
||||
nodes = await client.get("/api2/json/nodes")
|
||||
assert nodes.status_code in {200, 401, 501}
|
||||
|
||||
restored = await client.post("/ui/api/contract/apply", params={"major": 9})
|
||||
assert restored.status_code == 200
|
||||
assert restored.json()["runtime_version"] == "9.2.3"
|
||||
assert (await client.get("/api2/json/version")).json()["data"]["version"] == "9.2.3"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("major,version", [(6, "6.4-15"), (7, "7.4-16"), (8, "8.4.5")])
|
||||
async def test_contract_apply_loads_per_major_verified_evidence(major: int, version: str) -> None:
|
||||
app = _app()
|
||||
async with app.router.lifespan_context(app):
|
||||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||
applied = await client.post("/ui/api/contract/apply", params={"major": major})
|
||||
assert applied.status_code == 200
|
||||
assert applied.json()["runtime_version"] == version
|
||||
report = await client.get("/admin/compatibility")
|
||||
body = report.json()
|
||||
assert body["source_version"] == version
|
||||
assert body["levels"]["verified"]["count"] == body["total_declared"]
|
||||
assert body["levels"]["verified"]["count"] > 0
|
||||
|
||||
|
||||
async def test_contract_apply_requires_bootstrapped_contract() -> None:
|
||||
app = create_app(
|
||||
settings=Settings(contract_snapshot=None),
|
||||
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:
|
||||
response = await client.post("/ui/api/contract/apply", params={"major": 7})
|
||||
assert response.status_code == 503
|
||||
Reference in New Issue
Block a user