test: VM smoke and Lightning regtest suites
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
93a58e18f6
commit
7f4a71b98d
4 changed files with 358 additions and 0 deletions
80
tests/refactor-smoke.nix
Normal file
80
tests/refactor-smoke.nix
Normal file
|
|
@ -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 <unit>.service")
|
||||||
|
# machine.succeed("test -f /etc/<path>")
|
||||||
|
# machine.succeed("grep -q '<line>' /etc/<file>")
|
||||||
|
{
|
||||||
|
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")
|
||||||
|
'';
|
||||||
|
}
|
||||||
109
tests/regtest-core.nix
Normal file
109
tests/regtest-core.nix
Normal file
|
|
@ -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 ✓")
|
||||||
|
'';
|
||||||
|
}
|
||||||
110
tests/regtest-interactive.nix
Normal file
110
tests/regtest-interactive.nix
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
59
tests/regtest-node.nix
Normal file
59
tests/regtest-node.nix
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue