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