diff --git a/tests/refactor-smoke.nix b/tests/refactor-smoke.nix new file mode 100644 index 0000000..9372dad --- /dev/null +++ b/tests/refactor-smoke.nix @@ -0,0 +1,80 @@ +# refactor-smoke — headless VM boot test for autonomous refactor runs +# +# The "closed environment" for aggressive refactors is the *worktree* at +# ~/nixos-refactor/ plus the lack of sudo (so `omni-rebuild switch` +# can't activate on bohm). This test gives Claude an objective +# success/fail signal it can iterate against without a human in the loop. +# +# Run from a worktree of /etc/nixos: +# nix build .#checks.x86_64-linux.refactor-smoke -L +# +# Or as part of the full check suite: +# nix flake check --keep-going -L +# +# Extension points (the starter is intentionally minimal): +# - Add `imports` to `nodes.machine` as you bring more modules under +# test. Mind: graphical bits (Hyprland, greetd) are skipped — they +# need a real display and aren't easy to assert against in QEMU. +# - Add assertions to `testScript` as you pin more behavior. Useful +# idioms: +# machine.succeed("systemctl is-active .service") +# machine.succeed("test -f /etc/") +# machine.succeed("grep -q '' /etc/") +{ + nixpkgs, + system ? "x86_64-linux", + ... +}: + +let + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; +in +pkgs.testers.runNixOSTest { + name = "omni-refactor-smoke"; + + nodes.machine = + { + config, + pkgs, + lib, + ... + }: + { + # Match the real machine's primary user so home-managed paths + # resolve identically when the test grows to exercise them. + users.users.padreug = { + isNormalUser = true; + uid = 1000; + extraGroups = [ "wheel" ]; + }; + + # Tools the refactor surface should always ship. Mirror this list + # to whatever your CLAUDE.md / dev-env claims is installed. + environment.systemPackages = with pkgs; [ + git + pnpm + neovim + ]; + + system.stateVersion = "24.11"; + }; + + testScript = '' + machine.start() + machine.wait_for_unit("multi-user.target") + + # Boot reached multi-user. + machine.succeed("uptime") + + # Primary user exists with expected uid. + machine.succeed("test $(id -u padreug) -eq 1000") + + # Key tools functional, not just present on PATH. + machine.succeed("git --version") + machine.succeed("pnpm --version") + machine.succeed("nvim --version | head -1") + ''; +} diff --git a/tests/regtest-core.nix b/tests/regtest-core.nix new file mode 100644 index 0000000..ed47354 --- /dev/null +++ b/tests/regtest-core.nix @@ -0,0 +1,109 @@ +# regtest-core — NixOS-native Lightning regtest integration test. +# +# A reproducible, hermetic replacement for the core of the docker +# `legend-regtest-enviroment` stack (aiolabs/omnixient#27): one VM running +# bitcoind(regtest) + Core Lightning + LND (the bitcoin pack's nix-bitcoin +# modules), driving a real payment flow and asserting it settles. +# +# Both LN nodes are co-located on one node and share the local bitcoind; +# lnd's p2p port is moved to 9736 so it doesn't clash with clightning's +# 9735. CLIs are run as the nix-bitcoin `operator` user, matching how +# nix-bitcoin's own test suite drives them. +# +# The node config is shared with the interactive dev VM via +# tests/regtest-node.nix so the two can never drift. +# +# Run headless: +# nix build .#checks.x86_64-linux.regtest-core -L +# Interactive driver (poke the node by hand): +# nix build .#checks.x86_64-linux.regtest-core.driverInteractive +# ./result/bin/nixos-test-driver +# Friendlier interactive shell (SSH, copy/paste, operator user, helpers): +# nix run .#regtest (see docs/regtest.md) +{ + nixpkgs, + system ? "x86_64-linux", + nix-bitcoin, + ... +}: + +let + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; +in +pkgs.testers.runNixOSTest { + name = "omni-regtest-core"; + + nodes.regtest = { + imports = [ + nix-bitcoin.nixosModules.default + ./regtest-node.nix # shared bitcoind + CLN + LND config + ]; + }; + + testScript = '' + def op(cmd): + """Run a command as the nix-bitcoin operator and return stdout.""" + return regtest.succeed(f"runuser -u operator -- {cmd}") + + regtest.start() + + # --- services up --- + regtest.wait_for_unit("bitcoind.service") + regtest.wait_for_unit("clightning.service") + regtest.wait_for_unit("lnd.service") + + # CLIs answer (clightning/lnd take a moment past unit-active to be RPC-ready). + regtest.wait_until_succeeds("runuser -u operator -- lightning-cli getinfo", timeout=120) + regtest.wait_until_succeeds("runuser -u operator -- lncli getinfo", timeout=120) + + # --- both LN nodes synced to the pre-mined regtest chain --- + regtest.wait_until_succeeds( + f"[[ $(runuser -u operator -- lightning-cli getinfo | jq -M .blockheight) == {110} ]]", + timeout=120, + ) + regtest.wait_until_succeeds( + f"[[ $(runuser -u operator -- lncli getinfo | jq -M .block_height) == {110} ]]", + timeout=120, + ) + + # --- fund LND's on-chain wallet from the bitcoind test wallet --- + lnd_addr = op("lncli newaddress p2wkh | jq -r .address").strip() + op(f"bitcoin-cli -rpcwallet=test sendtoaddress {lnd_addr} 5") + mine_addr = op("bitcoin-cli -rpcwallet=test getnewaddress").strip() + op(f"bitcoin-cli -rpcwallet=test generatetoaddress 6 {mine_addr}") + regtest.wait_until_succeeds( + "[[ $(runuser -u operator -- lncli walletbalance | jq -M '.confirmed_balance | tonumber') -gt 0 ]]", + timeout=120, + ) + + # --- connect LND -> CLN and open a channel --- + cln_id = op("lightning-cli getinfo | jq -r .id").strip() + op(f"lncli connect {cln_id}@127.0.0.1:9735") + op(f"lncli openchannel --node_key={cln_id} --local_amt=1000000") # 0.01 BTC + + # confirm the channel and wait for it to go active on the LND side + op(f"bitcoin-cli -rpcwallet=test generatetoaddress 6 {mine_addr}") + regtest.wait_until_succeeds( + "[[ $(runuser -u operator -- lncli listchannels | jq '[.channels[] | select(.active)] | length') -ge 1 ]]", + timeout=240, + ) + + # --- CLN issues an invoice, LND pays it --- + bolt11 = op( + "lightning-cli invoice 100000000 regtest-core-test 'regtest-core payment' | jq -r .bolt11" + ).strip() # 100000 sat = 100,000,000 msat + op(f"lncli payinvoice --force {bolt11}") + + # --- assert CLN saw the invoice settle --- + regtest.wait_until_succeeds( + "[[ $(runuser -u operator -- lightning-cli listinvoices regtest-core-test " + "| jq -r '.invoices[0].status') == paid ]]", + timeout=180, + ) + + print("regtest-core: LND -> CLN channel opened and invoice paid ✓") + ''; +} diff --git a/tests/regtest-interactive.nix b/tests/regtest-interactive.nix new file mode 100644 index 0000000..c302549 --- /dev/null +++ b/tests/regtest-interactive.nix @@ -0,0 +1,110 @@ +# regtest-interactive — turns the shared regtest stack into a friendly, +# SSH-able dev VM (driven by `nix run .#regtest`). +# +# Layered on top of tests/regtest-node.nix + nix-bitcoin (see flake.nix). +# Unlike the CI test's QEMU console / Python REPL, this gives you a real +# terminal: copy/paste, scrollback, no log spam, landed straight in the +# `operator` user with the CLIs preconfigured. +{ + config, + pkgs, + lib, + settings ? { + sshKeys = [ ]; + }, + ... +}: +let + # Fund LND on-chain and open an LND -> CLN channel, so you start with a + # working channel instead of building it by hand every session. Runs as + # operator (the CLIs are on its PATH); waits for the daemons first. + regtest-fund = pkgs.writeShellScriptBin "regtest-fund" '' + set -euo pipefail + echo "⏳ waiting for the lightning daemons..." + until lncli getinfo >/dev/null 2>&1 && lightning-cli getinfo >/dev/null 2>&1; do + sleep 2 + done + + echo "💰 funding LND on-chain..." + addr=$(lncli newaddress p2wkh | jq -r .address) + bitcoin-cli -rpcwallet=test sendtoaddress "$addr" 5 >/dev/null + mine=$(bitcoin-cli -rpcwallet=test getnewaddress) + bitcoin-cli -rpcwallet=test generatetoaddress 6 "$mine" >/dev/null + until [ "$(lncli walletbalance | jq -M '.confirmed_balance | tonumber')" -gt 0 ]; do + sleep 2 + done + + echo "🔗 opening LND -> CLN channel..." + cln=$(lightning-cli getinfo | jq -r .id) + lncli connect "$cln@127.0.0.1:9735" >/dev/null 2>&1 || true + lncli openchannel --node_key="$cln" --local_amt=1000000 >/dev/null + bitcoin-cli -rpcwallet=test generatetoaddress 6 "$mine" >/dev/null + + echo "⏳ waiting for the channel to activate..." + until [ "$(lncli listchannels | jq '[.channels[] | select(.active)] | length')" -ge 1 ]; do + bitcoin-cli -rpcwallet=test generatetoaddress 1 "$mine" >/dev/null + sleep 2 + done + + echo "✓ channel active. Try:" + echo " bolt11=\$(cln invoice 50000000 demo demo | jq -r .bolt11)" + echo " lncli payinvoice --force \$bolt11" + ''; +in +{ + networking.hostName = "regtest"; + + # Throwaway VM — value is irrelevant, set only to silence the eval warning. + system.stateVersion = "24.11"; + + # SSH is the whole point — a clean terminal you can paste into. + services.openssh = { + enable = true; + settings.PasswordAuthentication = true; + }; + + # Land as `operator` (the nix-bitcoin user the CLIs are wired for). + # Simple shared password for a throwaway local VM with fake coins; also + # accept the adopter's SSH keys if settings.nix has any. + users.users.operator = { + password = "password"; # pragma: allowlist secret + extraGroups = [ "wheel" ]; + openssh.authorizedKeys.keys = settings.sshKeys or [ ]; + }; + # Auto-login operator on the serial console too (if you skip SSH). + services.getty.autologinUser = "operator"; + + # Convenience aliases. `bitcoin-cli` already auto-selects the single + # loaded `test` wallet, but `btc` makes it explicit and short. We avoid + # aliasing `ln` (that's coreutils) — use the real `lncli`. + environment.shellAliases = { + btc = "bitcoin-cli -rpcwallet=test"; + cln = "lightning-cli"; + }; + + environment.systemPackages = [ regtest-fund ]; + + users.motd = '' + + ┌─ Omnixient regtest dev VM ───────────────────────────────────┐ + user: operator password: password (sudo enabled) + aliases: btc → bitcoin-cli -rpcwallet=test + cln → lightning-cli lncli → LND CLI + helper: regtest-fund fund LND + open an LND→CLN channel + nodes: bitcoind(regtest) CLN :9735 LND :9736 + note: the stack pre-mines 110 blocks on boot — give it a few + seconds, then run `regtest-fund`. + quit: `exit` (the VM is discarded) + └───────────────────────────────────────────────────────────┘ + ''; + + # Headless + host:2222 -> guest:22 so `nix run .#regtest` can SSH in. + virtualisation.graphics = false; + virtualisation.forwardPorts = [ + { + from = "host"; + host.port = 2222; + guest.port = 22; + } + ]; +} diff --git a/tests/regtest-node.nix b/tests/regtest-node.nix new file mode 100644 index 0000000..eb3b9ea --- /dev/null +++ b/tests/regtest-node.nix @@ -0,0 +1,59 @@ +# regtest-node — shared bitcoind(regtest) + Core Lightning + LND config. +# +# The single source of truth for the regtest stack, imported by BOTH: +# - the CI test → tests/regtest-core.nix +# - the interactive VM → tests/regtest-interactive.nix (via flake.nix) +# so the two can never drift. +# +# Consumers must ALSO import `nix-bitcoin.nixosModules.default` — it needs +# the flake input, which a plain module can't reach on its own. +# +# Both LN nodes are co-located and share the local bitcoind; lnd's p2p +# port is moved to 9736 so it doesn't clash with clightning's 9735. CLIs +# run as the nix-bitcoin `operator` user. +{ + config, + pkgs, + lib, + ... +}: +{ + nix-bitcoin.generateSecrets = true; + nix-bitcoin.operator.enable = true; + + services.bitcoind = { + enable = true; + regtest = true; + # regtest has no fee estimation, so sendtoaddress/funding need a + # fallback fee. nix-bitcoin appends extraConfig inside the [regtest] + # section, so this applies to the regtest network. + extraConfig = "fallbackfee=0.0002"; + }; + + # Pre-mine a funded "test" wallet on bitcoind startup. 110 > 100 so the + # coinbase outputs are mature/spendable. + # NOTE: keep this height in sync with the assertions in + # tests/regtest-core.nix (it checks the nodes reach block 110). + systemd.services.bitcoind.postStart = lib.mkAfter '' + cli=${config.services.bitcoind.cli}/bin/bitcoin-cli + if ! $cli listwallets | ${pkgs.jq}/bin/jq -e 'index("test")' >/dev/null; then + "$cli" -named createwallet wallet_name=test load_on_startup=true + addr=$("$cli" -rpcwallet=test getnewaddress) + "$cli" generatetoaddress 110 "$addr" + fi + ''; + + services.clightning.enable = true; # p2p :9735 + services.lnd = { + enable = true; + port = 9736; # avoid clashing with clightning's 9735 + }; + + # jq is used by the test script and the interactive helpers. + environment.systemPackages = [ pkgs.jq ]; + + # Headroom for bitcoind + two lightning daemons in one VM. + virtualisation.cores = 2; + virtualisation.memorySize = 3072; + virtualisation.diskSize = 8192; +}