# SPDX-License-Identifier: AGPL-3.0-or-later """MazeNET compose-generator + teardown-order tests.""" from __future__ import annotations import pytest from decnet.engine.deployer import _teardown_order from decnet.topology.compose import ( _container_name, _network_name, generate_topology_compose, ) from decnet.topology.config import TopologyConfig from decnet.topology.generator import generate from decnet.topology.persistence import hydrate, persist from decnet.web.db.factory import get_repository def _cfg(**kw) -> TopologyConfig: base = dict( name="cmp", depth=2, branching_factor=2, deckies_per_lan_min=1, deckies_per_lan_max=1, cross_edge_probability=0.0, randomize_services=False, services_explicit=["ssh"], seed=9, ) base.update(kw) return TopologyConfig(**base) @pytest.fixture async def repo(tmp_path): r = get_repository(db_path=str(tmp_path / "compose.db")) await r.initialize() return r @pytest.mark.anyio async def test_compose_has_one_network_per_lan(repo): plan = generate(_cfg()) tid = await persist(repo, plan) hydrated = await hydrate(repo, tid) data = generate_topology_compose(hydrated) assert set(data["networks"].keys()) == { _network_name(tid, lan.name) for lan in plan.lans } for net in data["networks"].values(): assert net["external"] is True @pytest.mark.anyio async def test_compose_multi_home_bridge_decky(repo): plan = generate(_cfg()) tid = await persist(repo, plan) hydrated = await hydrate(repo, tid) data = generate_topology_compose(hydrated) # Every bridge decky (multi-homed) must list ≥2 networks in its base. for decky in hydrated["deckies"]: cfg = decky["decky_config"] base = data["services"][cfg["name"]] assert base["container_name"] == _container_name(tid, cfg["name"]) assert len(base["networks"]) == len(cfg["ips_by_lan"]) for lan_name, ip in cfg["ips_by_lan"].items(): net_key = _network_name(tid, lan_name) assert base["networks"][net_key]["ipv4_address"] == ip @pytest.mark.anyio async def test_compose_forwards_l3_sets_sysctl(repo): # Force every bridge to forward L3, then assert at least one base has it. plan = generate(_cfg(bridge_forward_probability=1.0)) tid = await persist(repo, plan) hydrated = await hydrate(repo, tid) data = generate_topology_compose(hydrated) forwarders = [ d for d in hydrated["deckies"] if d["decky_config"].get("forwards_l3") ] assert forwarders, "expected at least one forwarding bridge decky" for d in forwarders: base = data["services"][d["decky_config"]["name"]] assert base["sysctls"]["net.ipv4.ip_forward"] == 1 assert "NET_ADMIN" in base["cap_add"] @pytest.mark.anyio async def test_compose_labels_service_containers_for_collector(repo): """Service fragments must carry ``decnet.topology.service=true`` so the host-side collector picks up their log streams — the old fleet state file never mentions topology containers.""" plan = generate(_cfg()) tid = await persist(repo, plan) hydrated = await hydrate(repo, tid) data = generate_topology_compose(hydrated) service_keys = [ k for k in data["services"] if "-" in k and k not in {d["decky_config"]["name"] for d in hydrated["deckies"]} ] assert service_keys, "expected at least one service container" for k in service_keys: labels = data["services"][k].get("labels") or {} assert labels.get("decnet.topology.service") == "true", ( f"service {k!r} missing collector-discovery label: {labels}" ) assert labels.get("decnet.topology.id") == tid assert "decnet.topology.decky" in labels assert "decnet.topology.service_name" in labels # Base containers get their own label (role=base) but MUST NOT carry # the service marker — otherwise the collector double-attaches. base_keys = {d["decky_config"]["name"] for d in hydrated["deckies"]} for k in base_keys: labels = data["services"][k].get("labels") or {} assert labels.get("decnet.topology.role") == "base" assert labels.get("decnet.topology.service") != "true" def test_teardown_order_is_leaf_first(): lans = [ {"name": "LAN-00"}, {"name": "LAN-01"}, {"name": "LAN-02"}, {"name": "LAN-03"}, ] order = _teardown_order(lans) assert order == ["LAN-03", "LAN-02", "LAN-01", "LAN-00"] # DMZ is last — nothing should be torn down after LAN-00. assert order[-1] == "LAN-00"