Newer
Older
gnexus-book / server / tests / test_pending_changes.py
@Eugene Sukhodolskiy Eugene Sukhodolskiy 1 day ago 10 KB Add broad knowledge inventory types
from app.config import Settings
from app.pending_changes import PendingChangeError, PendingChangeRepository, ProposedChangeRequest


def test_creates_and_reads_pending_change(tmp_path) -> None:
    repo = PendingChangeRepository(Settings(tmp_path))
    request = ProposedChangeRequest(
        kind="inventory-item",
        target="virtual-machines/gnauth",
        summary="Update gnauth VM metadata",
        reason="Document newly confirmed service mapping.",
        payload={"runs_services": ["gnexus-auth"]},
    )

    created = repo.create(request)
    fetched = repo.get(created["id"])
    changes = repo.list()

    assert created["status"] == "pending"
    assert fetched["summary"] == "Update gnauth VM metadata"
    assert changes[0]["id"] == created["id"]
    assert (tmp_path / "90-maintenance" / "pending-changes" / f"{created['id']}.json").exists()


def _copy_schema_files(tmp_path) -> None:
    (tmp_path / "schemas").mkdir()
    source_schema = Settings().repo_root / "schemas"
    for schema in source_schema.glob("*.json"):
        (tmp_path / "schemas" / schema.name).write_text(schema.read_text(), encoding="utf-8")


def _create_empty_inventory(tmp_path) -> None:
    (tmp_path / "40-inventory").mkdir()
    for name in [
        "backups",
        "databases",
        "domains",
        "endpoints",
        "hardware",
        "hosts",
        "integrations",
        "networks",
        "projects",
        "services",
        "traffic-routes",
        "virtual-machines",
    ]:
        (tmp_path / "40-inventory" / f"{name}.yml").write_text("---\n[]\n", encoding="utf-8")


def test_applies_doc_change(tmp_path) -> None:
    _copy_schema_files(tmp_path)
    _create_empty_inventory(tmp_path)
    repo = PendingChangeRepository(Settings(tmp_path))
    created = repo.create(
        ProposedChangeRequest(
            kind="doc",
            target="10-systems/example.md",
            summary="Add example doc",
            payload={
                "content": (
                    "---\n"
                    "owner: gmikcon\n"
                    "status: active\n"
                    "last_reviewed: 2026-05-09\n"
                    "review_interval: 90d\n"
                    "confidence: medium\n"
                    "source_of_truth: test\n"
                    "---\n\n"
                    "# Example\n"
                )
            },
        )
    )

    applied = repo.apply(created["id"])

    assert applied["status"] == "applied"
    assert applied["validation"]["status"] == "ok"
    assert (tmp_path / "10-systems" / "example.md").exists()


def test_rolls_back_doc_change_when_validation_fails(tmp_path) -> None:
    _copy_schema_files(tmp_path)
    _create_empty_inventory(tmp_path)
    repo = PendingChangeRepository(Settings(tmp_path))
    created = repo.create(
        ProposedChangeRequest(
            kind="doc",
            target="10-systems/bad.md",
            summary="Add invalid doc",
            payload={"content": "# Missing Frontmatter\n"},
        )
    )

    try:
        repo.apply(created["id"])
    except PendingChangeError as exc:
        assert "failed validation" in str(exc)
    else:
        raise AssertionError("Expected PendingChangeError")

    assert not (tmp_path / "10-systems" / "bad.md").exists()
    assert repo.get(created["id"])["status"] == "pending"


def test_rejects_non_doc_apply(tmp_path) -> None:
    repo = PendingChangeRepository(Settings(tmp_path))
    created = repo.create(
        ProposedChangeRequest(
            kind="inventory",
            target="virtual-machines",
            summary="Update inventory",
            payload={"items": []},
        )
    )

    try:
        repo.apply(created["id"])
    except PendingChangeError as exc:
        assert "Only doc and inventory-item changes" in str(exc)
    else:
        raise AssertionError("Expected PendingChangeError")


def test_applies_inventory_item_change(tmp_path) -> None:
    _copy_schema_files(tmp_path)
    _create_empty_inventory(tmp_path)
    inventory = tmp_path / "40-inventory" / "virtual-machines.yml"
    inventory.write_text(
        "---\n"
        "- id: gnauth\n"
        "  name: gnauth\n"
        "  status: running\n"
        "  hypervisor_host: hp-proliant-dl380-g6\n"
        "  virtualization_stack: kvm-libvirt\n"
        "  docs: ../10-systems/virtualization/libvirt-vms.md\n"
        "  last_reviewed: 2026-05-09\n",
        encoding="utf-8",
    )
    docs = tmp_path / "10-systems" / "virtualization"
    docs.mkdir(parents=True)
    (docs / "libvirt-vms.md").write_text(
        "---\n"
        "owner: gmikcon\n"
        "status: active\n"
        "last_reviewed: 2026-05-09\n"
        "review_interval: 90d\n"
        "confidence: medium\n"
        "source_of_truth: test\n"
        "---\n\n"
        "# Libvirt VMs\n",
        encoding="utf-8",
    )
    repo = PendingChangeRepository(Settings(tmp_path))
    created = repo.create(
        ProposedChangeRequest(
            kind="inventory-item",
            target="virtual-machines/gnauth",
            summary="Add gnauth service mapping",
            payload={"patch": {"runs_services": ["gnexus-auth"]}},
        )
    )

    applied = repo.apply(created["id"])

    assert applied["status"] == "applied"
    assert "gnexus-auth" in inventory.read_text(encoding="utf-8")


def test_inventory_item_change_rolls_back_on_validation_error(tmp_path) -> None:
    _copy_schema_files(tmp_path)
    _create_empty_inventory(tmp_path)
    inventory = tmp_path / "40-inventory" / "virtual-machines.yml"
    original = (
        "---\n"
        "- id: gnauth\n"
        "  name: gnauth\n"
        "  status: running\n"
        "  hypervisor_host: hp-proliant-dl380-g6\n"
        "  virtualization_stack: kvm-libvirt\n"
        "  docs: ../10-systems/virtualization/libvirt-vms.md\n"
        "  last_reviewed: 2026-05-09\n"
    )
    inventory.write_text(original, encoding="utf-8")
    docs = tmp_path / "10-systems" / "virtualization"
    docs.mkdir(parents=True)
    (docs / "libvirt-vms.md").write_text(
        "---\n"
        "owner: gmikcon\n"
        "status: active\n"
        "last_reviewed: 2026-05-09\n"
        "review_interval: 90d\n"
        "confidence: medium\n"
        "source_of_truth: test\n"
        "---\n\n"
        "# Libvirt VMs\n",
        encoding="utf-8",
    )
    repo = PendingChangeRepository(Settings(tmp_path))
    created = repo.create(
        ProposedChangeRequest(
            kind="inventory-item",
            target="virtual-machines/gnauth",
            summary="Break VM status",
            payload={"patch": {"status": "invalid-status"}},
        )
    )

    try:
        repo.apply(created["id"])
    except PendingChangeError as exc:
        assert "failed validation" in str(exc)
    else:
        raise AssertionError("Expected PendingChangeError")

    assert inventory.read_text(encoding="utf-8") == original


def test_creates_inventory_item(tmp_path) -> None:
    _copy_schema_files(tmp_path)
    _create_empty_inventory(tmp_path)
    docs = tmp_path / "10-systems" / "virtualization"
    docs.mkdir(parents=True)
    (docs / "libvirt-vms.md").write_text(
        "---\n"
        "owner: gmikcon\n"
        "status: active\n"
        "last_reviewed: 2026-05-09\n"
        "review_interval: 90d\n"
        "confidence: medium\n"
        "source_of_truth: test\n"
        "---\n\n"
        "# Libvirt VMs\n",
        encoding="utf-8",
    )
    repo = PendingChangeRepository(Settings(tmp_path))
    created = repo.create(
        ProposedChangeRequest(
            kind="inventory-item",
            target="virtual-machines/new-vm",
            summary="Add new VM",
            payload={
                "mode": "create",
                "patch": {
                    "name": "new-vm",
                    "status": "running",
                    "hypervisor_host": "hp-proliant-dl380-g6",
                    "virtualization_stack": "kvm-libvirt",
                    "docs": "../10-systems/virtualization/libvirt-vms.md",
                    "last_reviewed": "2026-05-09",
                },
            },
        )
    )

    applied = repo.apply(created["id"])

    assert applied["status"] == "applied"
    assert "new-vm" in (tmp_path / "40-inventory" / "virtual-machines.yml").read_text(
        encoding="utf-8"
    )


def test_create_inventory_item_rejects_duplicate(tmp_path) -> None:
    _copy_schema_files(tmp_path)
    _create_empty_inventory(tmp_path)
    inventory = tmp_path / "40-inventory" / "virtual-machines.yml"
    inventory.write_text(
        "---\n"
        "- id: gnauth\n"
        "  name: gnauth\n"
        "  status: running\n"
        "  hypervisor_host: hp-proliant-dl380-g6\n"
        "  virtualization_stack: kvm-libvirt\n"
        "  docs: ../10-systems/virtualization/libvirt-vms.md\n"
        "  last_reviewed: 2026-05-09\n",
        encoding="utf-8",
    )
    repo = PendingChangeRepository(Settings(tmp_path))
    created = repo.create(
        ProposedChangeRequest(
            kind="inventory-item",
            target="virtual-machines/gnauth",
            summary="Duplicate VM",
            payload={
                "mode": "create",
                "patch": {
                    "name": "gnauth",
                    "status": "running",
                    "hypervisor_host": "hp-proliant-dl380-g6",
                    "virtualization_stack": "kvm-libvirt",
                    "docs": "../10-systems/virtualization/libvirt-vms.md",
                    "last_reviewed": "2026-05-09",
                },
            },
        )
    )

    try:
        repo.apply(created["id"])
    except PendingChangeError as exc:
        assert "already exists" in str(exc)
    else:
        raise AssertionError("Expected PendingChangeError")


def test_create_inventory_item_rolls_back_on_validation_error(tmp_path) -> None:
    _copy_schema_files(tmp_path)
    _create_empty_inventory(tmp_path)
    inventory = tmp_path / "40-inventory" / "virtual-machines.yml"
    original = inventory.read_text(encoding="utf-8")
    repo = PendingChangeRepository(Settings(tmp_path))
    created = repo.create(
        ProposedChangeRequest(
            kind="inventory-item",
            target="virtual-machines/bad-vm",
            summary="Add invalid VM",
            payload={
                "mode": "create",
                "patch": {
                    "name": "bad-vm",
                    "status": "invalid-status",
                },
            },
        )
    )

    try:
        repo.apply(created["id"])
    except PendingChangeError as exc:
        assert "failed validation" in str(exc)
    else:
        raise AssertionError("Expected PendingChangeError")

    assert inventory.read_text(encoding="utf-8") == original