"""Worker agent re-localizes master-built configs to its own NIC/subnet. The master ships a DecnetConfig populated from *its own* network (master NIC name, master subnet, master-chosen decky IPs). The worker cannot run the deployer against that as-is: `ip addr show ` blows up on any worker whose NIC differs from the master's, which is ~always the case in a heterogeneous fleet. The agent's executor overrides interface/subnet/gateway/host_ip with locally-detected values before calling into the deployer, and if the subnet doesn't match, it re-allocates decky IPs from the local subnet. """ from __future__ import annotations import pytest from decnet.agent import executor from decnet.models import DecnetConfig, DeckyConfig def _cfg(subnet: str, interface: str = "wlp6s0") -> DecnetConfig: return DecnetConfig( mode="swarm", interface=interface, subnet=subnet, gateway=subnet.rsplit(".", 1)[0] + ".1", deckies=[ DeckyConfig( name=f"decky-0{i}", ip=subnet.rsplit(".", 1)[0] + f".{10 + i}", services=["ssh"], distro="debian", base_image="debian:bookworm-slim", hostname=f"decky-0{i}", ) for i in range(1, 3) ], ) def test_relocalize_swaps_interface_and_subnet(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(executor, "detect_interface", lambda: "enp0s3") monkeypatch.setattr(executor, "detect_subnet", lambda _i: ("10.0.0.0/24", "10.0.0.1")) monkeypatch.setattr(executor, "get_host_ip", lambda _i: "10.0.0.99") monkeypatch.setattr( executor, "allocate_ips", lambda **kw: [f"10.0.0.{20 + i}" for i in range(kw["count"])], ) incoming = _cfg("192.168.1.0/24") out = executor._relocalize(incoming) assert out.interface == "enp0s3" assert out.subnet == "10.0.0.0/24" assert out.gateway == "10.0.0.1" # Subnet changed → IPs re-allocated from the worker's subnet. assert [d.ip for d in out.deckies] == ["10.0.0.20", "10.0.0.21"] # Non-network fields survive. assert [d.name for d in out.deckies] == ["decky-01", "decky-02"] assert [d.services for d in out.deckies] == [["ssh"], ["ssh"]] def test_relocalize_keeps_ips_when_subnet_matches(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(executor, "detect_interface", lambda: "enp0s3") monkeypatch.setattr(executor, "detect_subnet", lambda _i: ("192.168.1.0/24", "192.168.1.1")) monkeypatch.setattr(executor, "get_host_ip", lambda _i: "192.168.1.50") # allocate_ips should NOT be called in the matching-subnet branch. def _fail(**_kw): # pragma: no cover raise AssertionError("allocate_ips must not be called when subnets match") monkeypatch.setattr(executor, "allocate_ips", _fail) incoming = _cfg("192.168.1.0/24") out = executor._relocalize(incoming) assert out.interface == "enp0s3" assert out.subnet == "192.168.1.0/24" # Decky IPs preserved verbatim. assert [d.ip for d in out.deckies] == ["192.168.1.11", "192.168.1.12"] @pytest.mark.asyncio async def test_deploy_relocalizes_before_calling_deployer(monkeypatch: pytest.MonkeyPatch) -> None: """End-to-end: agent.deploy(..) must not pass the master's interface through to the blocking deployer.""" monkeypatch.setattr(executor, "detect_interface", lambda: "enp0s3") monkeypatch.setattr(executor, "detect_subnet", lambda _i: ("192.168.1.0/24", "192.168.1.1")) monkeypatch.setattr(executor, "get_host_ip", lambda _i: "192.168.1.50") seen: dict = {} def _fake_deploy(cfg, dry_run, no_cache, parallel): seen["interface"] = cfg.interface seen["subnet"] = cfg.subnet monkeypatch.setattr(executor._deployer, "deploy", _fake_deploy) await executor.deploy(_cfg("192.168.1.0/24", interface="wlp6s0-master"), dry_run=True) assert seen == {"interface": "enp0s3", "subnet": "192.168.1.0/24"} @pytest.mark.asyncio async def test_deploy_unihost_mode_skips_relocalize(monkeypatch: pytest.MonkeyPatch) -> None: """Unihost configs have already been built against the local box — we must not second-guess them.""" def _fail(*_a, **_kw): # pragma: no cover raise AssertionError("detect_interface must not be called for unihost") monkeypatch.setattr(executor, "detect_interface", _fail) captured: dict = {} def _fake_deploy(cfg, dry_run, no_cache, parallel): captured["interface"] = cfg.interface monkeypatch.setattr(executor._deployer, "deploy", _fake_deploy) cfg = _cfg("192.168.1.0/24", interface="eth0").model_copy(update={"mode": "unihost"}) await executor.deploy(cfg, dry_run=True) assert captured["interface"] == "eth0"