Molecule Testing Patterns
Test-First Development
ALWAYS run
molecule testafter modifying roles, playbooks, or molecule config. NEVER consider a task complete until the full test suite passes.When adding or changing behavior, ALWAYS update
molecule/default/verify.ymlwith corresponding assertions before running the test.TDD iteration pattern:
- Write or update the verify assertion in
verify.ymlfirst molecule test— the assertion should fail (proves the test catches the issue)- Implement the fix in the role
molecule test— the assertion should now pass- Update skills/rules with lessons learned
- Write or update the verify assertion in
Test-First Reproduction
When a bug is reported against production, ALWAYS reproduce it on the test machine first. Replicate the production environment in
test.env(same env vars, same image) and runmolecule test.Only involve the production host when the test machine cannot reproduce the issue.
Previous bug: SSH timeout on production was reproduced by adding
WAN_MACtotest.env. Four fix-and-verify cycles completed in 15 minutes without touching production.
Quick Start Commands
source .venv/bin/activate
set -a; source test.env; set +a
molecule test # full clean-state pipeline
molecule converge # run playbook only (preserves baseline)
molecule verify # run assertions only
molecule cleanup # reset test host
Baseline Workflow
The OpenWrt baseline takes ~4 minutes to build. Prefer keeping it running between test runs.
Day-to-day iteration:
molecule converge # build/update baseline (idempotent)
molecule verify # run assertions
molecule converge -s mesh1-infra # run layered scenario (baseline must exist)
molecule verify -s mesh1-infra # verify layered scenario
Clean-state validation (CI, pre-commit, final proof):
molecule test # full pipeline: cleanup → converge → verify
After molecule test, the baseline is left running (verify is the last step).
To do a clean rebuild for the next iteration, run molecule converge manually.
Molecule Pipeline Sequence
molecule test runs these phases in order:
dependency— install Galaxy requirementscleanup— reset host from previous runssyntax— ansible syntax checkprepare— start API server, verify images existconverge— runplaybooks/site.ymlverify— runmolecule/default/verify.yml
There is NO trailing cleanup or reconverge. The baseline is left running after verify so mesh1 (LAN host) remains accessible. NEVER add cleanup or destroy to the end of the test_sequence.
There is NO lint phase in the Molecule config. Run ansible-lint and yamllint separately.
Architecture
- Driver:
defaultwithmanaged: false(real Proxmox hardware, not Docker) - Platforms: 6 nodes —
home(primary),ai,mesh2,bridge-1,bridge-2(directly reachable),mesh1(LAN satellite via ProxyCommand) - Provisioner:
playbooks/site.yml(phased: primary hosts → LAN bootstrap → services)
6-Node Topology
ISP Router (192.168.86.x supernet)
|
Switch
| | | | |
Home AI Node Mesh2 Bridge-1 Bridge-2
(primary) .220 .211 .230 .231
|
|-- OpenWrt VM (10.10.10.1)
| |
| LAN bridge (10.10.10.x)
| |
| Mesh1 (10.10.10.210)
- home, ai, mesh2, bridge-1, bridge-2: directly reachable on the supernet (no proxy)
- mesh1: behind home's OpenWrt, reachable via ProxyCommand through home
homeis the onlyrouter_nodesmember (runs OpenWrt VM)aiis ingaming_nodes— Gaming LXC with Sunshinemesh1is instreaming_nodes— Moonlight streaming clientbridge-1andbridge-2are inbridge_nodes— WiFi bridge AP and STA- All 6 nodes are in
kiosk_nodes(every host needs a Manager)
Use the 6 nodes intelligently for different tests
Each molecule scenario can assign different groups to the same host. A host is NOT locked into one role across all scenarios.
NEVER co-locate a streaming server and client on the same host. If ai runs Sunshine (gaming_nodes), ai CANNOT also run Moonlight (streaming_nodes).
Phased site.yml
site.yml runs in three phases to respect host reachability dependencies:
- Phase 1 (Primary hosts):
proxmox:!lan_hosts— backup, infra, OpenWrt VM, OpenWrt configure - Phase 2 (LAN satellites): After OpenWrt creates the LAN, bootstrap LAN hosts from
router_nodes, then run backup + infra onlan_hosts - Phase 3 (Services): Flavor groups that span both primary and LAN hosts — runs in parallel across all hosts
Pre-Test Checklist
- Source test env:
set -a; source test.env; set +a - Verify access:
curl -sf http://localhost:$WEBUI_PORT/api/fleet/health(if base state up) oransible home -m ping(first run) - Build custom images (required):
scripts/build-images.sh - Verify images exist:
ls images/openwrt-router-*.img.gz images/openwrt-mesh-lxc-*-rootfs.tar.gz images/debian-*.tar.zst - If previous run left host in bad state, power-cycle the machine
Common Failures
| Symptom | Cause | Fix |
|---|---|---|
UNREACHABLE during converge |
Host down or network broken | Check curl $CALLHOME_URL/api/fleet/health, then Ansible access |
community.proxmox not found |
Collections missing | ansible-galaxy collection install -r requirements.yml |
| Bridge numbers keep incrementing | Cleanup didn't remove bridges | scripts/cleanup.sh clean test.env |
| WiFi radios=0 after converge | PCI passthrough not cleaned up | Ensure cleanup unbinds vfio-pci, reloads modules, rescans PCI |
Timeout waiting for SSH |
Network restart dropped connection | Verify SSH args include ConnectTimeout=10, ServerAliveInterval=15 |
Multi-Node E2E Testing
When a service needs testing on all 6 nodes, add the flavor group to ALL platforms in the molecule default scenario — not just the static inventory.
This is a test-only change that doesn't affect production.