Files
self 3b38221abe refactor: simplify node recovery actions
Remove the old restore/operator-task recovery model and make actor/execution state the source of truth. Align GUI and CLI node actions around start, stop, reboot, retry, and reinstall, with provider image reset folded into reinstall when supported.

Update statechart and architecture docs to match the reinstall/repair model.
2026-06-17 14:09:18 +02:00

15 KiB

Testing

Local checks

These do not require provider credentials:

npm run precommit
npm test
npm run lint
npm run format:check
npm run typecheck
npm run test:unit
npm run test:integration:mock
node --import tsx --test tests/unit/<file>.test.ts

GUI checks

Non-billed Electron UI tests live in tests/gui and run against an isolated temporary state directory.

npm run test:gui

Billed provider-backed GUI tests should live in tests/billed/gui and must stay out of npm test, npm run precommit, and npm run test:gui.

Build kexec installer image

Legion consumes a prebuilt Guix kexec installer tarball. The supported builder now lives in this repo and uses local Guix by default. A remote Guix host can still be selected explicitly when needed.

Default run:

npm run build:kexec-image

Explicit remote build host:

npm run build:kexec-image -- --build-host=HOST

Explicit output path:

npm run build:kexec-image -- --output=/absolute/path/to/guix-kexec-installer.tar.gz

Optional environment:

export GUIX_TRIBES_SOURCE=/absolute/path/to/guix-tribes
export LEGION_BUILD_REMOTE_DIR=tribes_legion_build
export NBDE_KEXEC_GZIP_LEVEL=4

Notes:

  • Local builds use the sibling ../guix-tribes checkout directly by default.
  • The generated channels.scm comes from Legion's current pinned Guix channel config, so the kexec image matches deployment expectations.
  • Output defaults to .state/guix-kexec-installer.tar.gz.
  • When --build-host is set, the builder syncs the sibling ../guix-tribes checkout to the remote host.
  • The default remote workspace is $HOME/tribes_legion_build, so repeated remote builds reuse the same checkout/cache layout.
  • Pass --clean-remote to remove the remote workspace after a successful remote build.

Live provider checks

The commands below call real provider APIs. Keep them in a dedicated test account where possible.

Aggregate live suite:

npm run test:integration:live

Hetzner catalog smoke test

Read-only. Fetches the live Hetzner VPS and DNS catalogs and validates the normalized catalog data.

Required environment:

export HCLOUD_TOKEN=...

Run:

npm run test:integration:hcloud

Hetzner resize-policy integration test

Read-only. Fetches the live Hetzner catalog and verifies that at least one same-family, same-generation server line is classified as an in-place resize path.

Required environment:

export HCLOUD_TOKEN=...

Run:

npm run test:integration:hcloud:resize

OVH smoke test

Read-only. Authenticates against the live OVH API, checks the API surface we depend on, and validates live VPS and DNS catalog loading.

Required environment:

export OVH_APP_KEY=...
export OVH_APP_SECRET=...
export OVH_CONSUMER_KEY=...
export OVH_ENDPOINT=ovh-eu

OVH_ENDPOINT is optional and defaults to ovh-eu.

Run:

npm run test:integration:ovh

OVH domain live checks

These tests are intentionally split out from the normal live suite.

  • npm run test:integration:ovh:domain is the safe path.
    • It probes OVH registration quotes for a concrete domain without checkout.
    • It can also inspect an already-owned domain and read its current nameservers.
    • It creates a temporary OVH cart for the quote probe and deletes it again.
  • npm run test:integration:ovh:domain-purchase:manual is the paid path.
    • It is manual-only.
    • It must never be wired into npm test, precommit, or test:integration:live.
    • It places a real domain order and may incur unrecoverable cost.

Safe OVH domain probe

Non-destructive. Quotes a specific FQDN through the OVH order API and optionally inspects an existing OVH domain already in the account.

Required environment:

export OVH_APP_KEY=...
export OVH_APP_SECRET=...
export OVH_CONSUMER_KEY=...
export LEGION_CC_TEST_OVH_QUOTE_DOMAIN="example.tld"

Optional environment:

export OVH_ENDPOINT=ovh-eu
export LEGION_CC_TEST_OVH_EXISTING_DOMAIN="already-owned.example"

Run:

npm run test:integration:ovh:domain

Notes:

  • The quote probe does not call POST /order/cart/{cartId}/checkout.
  • It is still provider-live and depends on OVH returning offers for the requested FQDN.
  • Use a realistic candidate domain for LEGION_CC_TEST_OVH_QUOTE_DOMAIN; this is not a provider-wide TLD catalog test.
  • If LEGION_CC_TEST_OVH_EXISTING_DOMAIN is set, the test also verifies read-only existing-domain observation and nameserver listing.

Manual OVH domain purchase

Destructive and billable. This is an operator-confirmed test for the actual OVH domain registration flow.

Required environment:

export OVH_APP_KEY=...
export OVH_APP_SECRET=...
export OVH_CONSUMER_KEY=...
export LEGION_CC_ALLOW_PAID_DOMAIN_ORDER=1
export LEGION_CC_TEST_OVH_PURCHASE_DOMAIN="fresh-domain-you-intend-to-buy.tld"
export LEGION_CC_TEST_REGISTRANT_FIRST_NAME=...
export LEGION_CC_TEST_REGISTRANT_LAST_NAME=...
export LEGION_CC_TEST_REGISTRANT_EMAIL=...
export LEGION_CC_TEST_REGISTRANT_PHONE=...
export LEGION_CC_TEST_REGISTRANT_STREET_1=...
export LEGION_CC_TEST_REGISTRANT_CITY=...
export LEGION_CC_TEST_REGISTRANT_POSTAL_CODE=...
export LEGION_CC_TEST_REGISTRANT_COUNTRY_CODE=DE

Optional environment:

export OVH_ENDPOINT=ovh-eu
export LEGION_CC_TEST_REGISTRANT_ORGANIZATION=...
export LEGION_CC_TEST_REGISTRANT_STREET_2=...
export LEGION_CC_TEST_REGISTRANT_STATE=...

Run:

npm run test:integration:ovh:domain-purchase:manual

Notes:

  • This test is expected to buy a real domain.
  • It does not attempt cleanup because registrar orders are not a normal reversible test resource.
  • It asserts that the target domain is not already present in the OVH account before ordering.
  • Run it only with an intentionally chosen domain and registrant details.

Scaleway smoke test

Read-only. Authenticates against the live Scaleway Instance API, loads the normalized compute catalog, and lists current Instances in the configured project/zone.

Required environment:

export SCW_ACCESS_KEY=...
export SCW_SECRET_KEY=...
export SCW_DEFAULT_PROJECT_ID=...

Optional environment:

export SCW_DEFAULT_ZONE=fr-par-1

SCW_DEFAULT_ZONE is optional and defaults to fr-par-1.

Run:

npm run test:integration:scaleway

Mocked Hetzner GUI lifecycle

Non-destructive. Builds the Electron app, launches it via Playwright, walks the GUI through a mocked Hetzner lifecycle, and uses mocked provider/API responses.

Run all GUI checks:

npm run test:gui

Run only the mocked Hetzner lifecycle:

npm run build
npx playwright test tests/gui/hetzner-mock-lifecycle.test.ts

Provisioning metrics

Managed node add/reinstall operations append sanitized provisioning summaries as JSONL.

  • Dev/test default path: data/deployment-metrics/provisioning.raw.jsonl.
  • Production default path: provisioning-metrics.jsonl beside the local state file.
  • Override the path with LEGION_PROVISIONING_METRICS_PATH.
  • Override the source label with LEGION_PROVISIONING_METRICS_SOURCE.
  • Disable metrics writes with LEGION_PROVISIONING_METRICS_DISABLED=1; the mocked GUI harness sets this so GUI tests do not append to data/deployment-metrics/provisioning.raw.jsonl.
  • Summarize recorded provisioning timings with legion metrics provisioning, or pass --path <jsonl> for another metrics file.
  • Records round recordedAt down to 15 minutes, include only partial IP hints (a.b.x.x for IPv4, first 32 bits for IPv6), and omit node names, domains, provider account IDs, provider server IDs, SSH keys, and raw error messages.
  • Metrics writes are best-effort and must not fail provisioning.

Self-signed certificate test mode

Headless CLI node E2E tests use the normal ACME path by default. For provider/debug runs that should avoid public ACME issuance, set:

export LEGION_TEST_CERT_MODE=self-signed

This forces the deployed edge certificate profile to self-signed, makes the public health probe tolerate the self-signed HTTPS certificate, and records certMode: "self-signed" in provisioning metrics. Leave the variable unset, or set it to acme, for the normal ACME path.

Hetzner CLI node E2E

Destructive. Runs the headless Legion CLI against an isolated state directory: initializes config, adds a Hetzner provider account, provisions a real node, verifies tracked state, then destroys it again.

Required environment:

export HCLOUD_TOKEN=...

Optional environment:

export LEGION_KEXEC_IMAGE=/absolute/path/to/guix-kexec-installer.tar.gz
export LEGION_CC_E2E_NAME="Legion CLI E2E"
export LEGION_CC_E2E_DOMAIN="legion-cli-e2e.example"
export LEGION_UNLOCK_PASSWORD="legion-cli-e2e-unlock-password"
export LEGION_CC_E2E_BOOTSTRAP_PASSWORD="legion-cli-e2e-bootstrap-password"
export LEGION_CC_E2E_PROVIDER_ACCOUNT_ID="hetzner-e2e"
export LEGION_CC_E2E_HCLOUD_INSTANCE="cx23"
export LEGION_CC_E2E_HCLOUD_BOOT_MODE="bios"
export LEGION_CC_E2E_ACME_EMAIL="hostmaster@tribe-one.org"
export LEGION_TEST_CERT_MODE=self-signed
export LEGION_CC_E2E_KEEP_INSTANCE=1

Notes:

  • This test currently covers the NBDE path only. The first node is expected to install in degraded mode.
  • This is a headless CLI test. It does not open the Electron window and does not use Playwright.
  • Kexec image resolution is: --kexec-image, then LEGION_KEXEC_IMAGE_URL, then LEGION_KEXEC_IMAGE, then Legion's pinned mirror image. A local .state/guix-kexec-installer.tar.gz is used only when passed explicitly.
  • Legion still aborts if no installer image can be resolved after those checks. It does not build one during the test run.
  • The test uses IP-based edge/TLS by default; it does not require a DNS name.
  • The test uses Legion's normal Hetzner default offer selection unless LEGION_CC_E2E_HCLOUD_INSTANCE is set.
  • Boot mode defaults to Legion's Hetzner deployment profile when LEGION_CC_E2E_HCLOUD_BOOT_MODE is unset.
  • The shared live CLI harness injects LEGION_UNLOCK_PASSWORD automatically for its isolated temp home, but exporting it explicitly makes local debugging and ad-hoc CLI runs behave the same way.
  • The test passes an explicit ACME email. Default: hostmaster@tribe-one.org.
  • The isolated test home, state, cache, and logs are always preserved for debugging and printed at the end of the run.
  • Set LEGION_CC_E2E_KEEP_INSTANCE=1 to keep the VM as well. The test will also print a ready-to-use legion ssh command that uses the isolated state directory.
  • This test creates real Hetzner resources and may incur cost if cleanup fails.

Run:

npm run test:billed:e2e:hetzner-cli

npm run test:billed:e2e:ovh-cli

Destructive live OVH CLI deployment test. This uses the same headless NBDE deployment path as the Hetzner CLI test, but with OVH provider credentials.

Required environment:

export OVH_APP_KEY=...
export OVH_APP_SECRET=...
export OVH_CONSUMER_KEY=...

Optional environment:

export OVH_ENDPOINT=ovh-eu
export LEGION_KEXEC_IMAGE=/absolute/path/to/guix-kexec-installer.tar.gz
export LEGION_CC_E2E_NAME="Legion CLI E2E"
export LEGION_CC_E2E_DOMAIN="legion-cli-e2e.example"
export LEGION_UNLOCK_PASSWORD="legion-cli-e2e-unlock-password"
export LEGION_CC_E2E_BOOTSTRAP_PASSWORD="legion-cli-e2e-bootstrap-password"
export LEGION_CC_E2E_OVH_PROVIDER_ACCOUNT_ID="ovh-e2e"
export LEGION_CC_E2E_OVH_INSTANCE="d2-2"
export LEGION_CC_E2E_OVH_BOOT_MODE="bios"
export LEGION_CC_E2E_ACME_EMAIL="hostmaster@tribe-one.org"
export LEGION_TEST_CERT_MODE=self-signed
export LEGION_CC_E2E_KEEP_INSTANCE=1

Notes:

  • This test currently covers the NBDE path only. The first node is expected to install in degraded mode.
  • This is a headless CLI test. It does not open the Electron window and does not use Playwright.
  • Kexec image resolution is: --kexec-image, then LEGION_KEXEC_IMAGE_URL, then LEGION_KEXEC_IMAGE, then Legion's pinned mirror image. A local .state/guix-kexec-installer.tar.gz is used only when passed explicitly.
  • Legion still aborts if no installer image can be resolved after those checks. It does not build one during the test run.
  • OVH credentials are passed via the standard OVH app/consumer key environment variables.
  • Boot mode defaults to Legion's OVH deployment profile when LEGION_CC_E2E_OVH_BOOT_MODE is unset.
  • The isolated test home, state, cache, and logs are always preserved for debugging and printed at the end of the run.
  • Set LEGION_CC_E2E_KEEP_INSTANCE=1 to keep the VM as well. The test will also print a ready-to-use legion ssh command that uses the isolated state directory.
  • This test creates real OVH resources and may incur cost if cleanup fails.

Run:

npm run test:billed:e2e:ovh-cli

npm run test:billed:e2e:scaleway-cli

Destructive live Scaleway CLI deployment test. This uses the same headless NBDE deployment path as the Hetzner and OVH CLI tests, but with Scaleway provider credentials.

Required environment:

export SCW_ACCESS_KEY=...
export SCW_SECRET_KEY=...
export SCW_DEFAULT_PROJECT_ID=...

Optional environment:

export SCW_DEFAULT_ZONE=fr-par-1
export LEGION_KEXEC_IMAGE=/absolute/path/to/guix-kexec-installer.tar.gz
export LEGION_CC_E2E_NAME="Legion CLI E2E"
export LEGION_CC_E2E_DOMAIN="legion-cli-e2e.example"
export LEGION_UNLOCK_PASSWORD="legion-cli-e2e-unlock-password"
export LEGION_CC_E2E_BOOTSTRAP_PASSWORD="legion-cli-e2e-bootstrap-password"
export LEGION_CC_E2E_SCW_PROVIDER_ACCOUNT_ID="scaleway-e2e"
export LEGION_CC_E2E_SCW_INSTANCE="DEV1-M"
export LEGION_CC_E2E_SCW_BOOT_MODE="efi"
export LEGION_CC_E2E_ACME_EMAIL="hostmaster@tribe-one.org"
export LEGION_TEST_CERT_MODE=self-signed
export LEGION_CC_E2E_KEEP_INSTANCE=1

Notes:

  • This test currently covers the NBDE path only. The first node is expected to install in degraded mode.
  • If LEGION_CC_E2E_SCW_INSTANCE is unset, the live Scaleway E2E defaults to DEV1-M.
  • This is a headless CLI test. It does not open the Electron window and does not use Playwright.
  • Kexec image resolution is: --kexec-image, then LEGION_KEXEC_IMAGE_URL, then LEGION_KEXEC_IMAGE, then Legion's pinned mirror image. A local .state/guix-kexec-installer.tar.gz is used only when passed explicitly.
  • Legion still aborts if no installer image can be resolved after those checks. It does not build one during the test run.
  • Scaleway credentials are passed via the standard SCW_* environment variables used by the official SDK.
  • Boot mode defaults to Legion's Scaleway deployment profile when LEGION_CC_E2E_SCW_BOOT_MODE is unset.
  • The isolated test home, state, cache, and logs are always preserved for debugging and printed at the end of the run.
  • Set LEGION_CC_E2E_KEEP_INSTANCE=1 to keep the VM as well. The test will also print a ready-to-use legion ssh command that uses the isolated state directory.
  • This test creates real Scaleway resources and may incur cost if cleanup fails.

Run:

npm run test:billed:e2e:scaleway-cli

Conventions

  • Live tests generally skip themselves when the required environment variables are missing.
  • Prefer running destructive tests one at a time.
  • After a failed destructive run, verify provider state manually before re-running.