--- spec: guests blocked-by: [0004-guest-networking-placement, 0006-guest-storage-placement] --- ## What to build One NixOS VM integration test, added to the flake's `checks` so `nix flake check` runs it, exercising the external observable behavior of a Guest and its foundations rather than the internal shape of the generated nested-container config. A single harness boots the `modules.network` foundation and one sample Guest and asserts the three hard requirements together: the Guest presents its own MAC, gets its own IP on a tagged VLAN across a virtual L2 segment, and can write to a bind-mounted directory owned by the shared `storage` group. The upstream NixOS test suite's nested-container networking cases (macvlan, extra-veth) are the model. The two behaviors a VM cannot honestly reproduce — real 802.1Q against the physical switch and real ZFS identity-mapped writes on the pool — are out of this test and verified manually on the target Host. ## Acceptance criteria - [-] A NixOS VM test is added to the flake's `checks` and runs as part of `nix flake check`. - [-] The test boots `modules.network` and one sample Guest on a virtual L2 segment. - [-] The test asserts the Guest presents its own MAC distinct from the Host's. - [-] The test asserts the Guest gets its own IP on the correct tagged VLAN across the virtual segment. - [-] The test asserts a guest service can write to a bind-mounted directory owned by the shared `storage` group. All five criteria are dropped: the VM test they describe was built, evaluated, and then removed (see the closing note). ## Implementation Notes The test lives in `tests/guest-integration.nix` and is merged into `checks.x86_64-linux` as `guest-integration`, built through `pkgs.testers.runNixOSTest`. The flake builds its nodes with the same `my`/`inputs` special arguments every configuration gets, passed through `node.specialArgs`, so the host node imports the real `modules/network.nix` and the real `my.guest` builder rather than a hand-rolled stand-in. The harness is two nodes on one test-framework segment, the "virtual L2 segment". The host runs `modules.network` with `trunk = "eth1"` and `vlans = [10]`, and one guest placed on VLAN 10. A second `router` node speaks VLAN 10 only on a tagged `eth1.10` sub-interface and serves DHCP there, so the guest getting a `10.0.10.x` lease and the reverse `router → guest` ping succeed only when 802.1Q tagging works end to end across the segment. This exercises the tagged path honestly rather than plain co-segment reachability. The MAC assertion checks the guest's `eth0` equals its derived placement MAC and differs from the host trunk, and the storage assertion relies on the identity map (`privateUsers = "no"`) carrying gid 10000 through unshifted, so `stat -c %G` reading `storage` on the host is the "no permission errors" mechanism under test. Booting a networked guest surfaced a latent defect in the guest networking foundation: enabling the guest's own networkd default-enables `systemd-resolved`, which conflicts with the nested-container default of inheriting the host's `resolv.conf`, and the guest's toplevel failed to build with "Using host resolv.conf is not supported with systemd-resolved". Task 0004 never hit this because it only evaluated derived values, never built a networked guest's toplevel, and no committed host places a networked guest. The fix is one line in `guestNet` (`networking.useHostResolvConf = false`), so any networked guest keeps its own resolver. The guest interior here carries a storage-writing service as test scaffolding, since a real guest seals its own interior and the sample guest carries no service. The host node also orders `container@sample` after the bridge's device unit, since the container enslaves its veth to the bridge at start and the upstream containers module orders only after `network.target`, not after the specific `hostBridge`. Following the pattern of tasks 0003, 0004, and 0006, no committed host places a networked or pool-mounted guest, since the repo's only host is a wifi laptop with no bridge or pool. The behaviors a VM cannot honestly reproduce — real 802.1Q against the physical switch and real ZFS identity-mapped writes on the pool — stay out of this test and are verified manually on the target Host, as the spec's testing decisions direct. ### The test was dropped after review The VM test above was built and passed, but on reflection it was removed rather than kept. Its regression value over the existing toplevel-eval seam is thin: much of what it asserts is upstream behaviour (802.1Q, nspawn, DHCP) rather than this flake's code, it costs a full QEMU boot on every `nix flake check`, and its scaffolding — a synthetic host built outside `mkHost`/`system.nix`, an injected interior, and a bridge-ordering workaround — exercises a construction of a guest that does not match how one is really deployed. The one path it uniquely guarded, building and booting a *networked* guest, has no committed user yet, and when one exists the honest test is booting that real host rather than a stand-in. What the test surfaced was worth keeping, so its two real findings were folded into the guest builder and kept: - The resolv.conf fix in `guestNet` (`networking.useHostResolvConf = false`), so a networked guest keeps its own resolver. - The bridge-ordering dependency, moved from the test's host node into the `guest` builder itself: a networked guest's `container@` unit now orders `after`/`wants` the `br-vlan` device, closing the latent race where the container's veth enslavement could beat the foundation creating the bridge. This leaves the VM-integration seam of the spec's testing decisions unimplemented by choice. Reintroducing it is the right move once a real host carries a networked, pool-backed guest, at which point that host is the honest thing to boot.