Files
self adbc9d3ab8 refactor: derive managed node bootstrap mode
Remove managedBootstrap.mode and public bootstrap-mode inputs from the shared model, state schema, CLI, preload, and renderer. Derive init vs join per deployment operation instead, while keeping satellites as internal provisioning input only.

Align GUI and CLI delete/reinstall actions with last healthy cluster node data-loss confirmation, update docs/statechart, and refresh tests around the new direct model shape.
2026-06-17 15:30:02 +02:00

2147 lines
54 KiB
TypeScript

import assert from "node:assert/strict"
import { EventEmitter } from "node:events"
import test from "node:test"
import {
isCliHelpRequest,
isCliVersionRequest,
getCliRuntimePolicy,
parseCliArgs,
renderCliHelp,
renderCliVersion,
runCli,
shouldCliMarkInterruptedOperations,
shouldCliRequireDaemon
} from "../../src/main/cli/cli"
import type {
DomainNameServersResult,
DomainListEntry,
LegionCliCommands,
NodeCliUnlockable,
NodeListEntry
} from "../../src/main/cli/node-cli-service"
import type { ProvisioningMetricsStatsResult } from "../../src/main/provisioning-metrics"
import type { DomainQuoteResult } from "../../src/shared/app"
function createBaseService(): LegionCliCommands {
return {
initConfig: async () => {
throw new Error("unexpected")
},
rekeyConfig: async () => {
throw new Error("unexpected")
},
exportConfig: async () => {
throw new Error("unexpected")
},
importConfig: async () => {
throw new Error("unexpected")
},
listProviders: () => [],
configureProvider: async () => {
throw new Error("unexpected")
},
findNodeOffers: async () => [],
listNodes: () => [],
listDomains: () => [],
addDomain: async () => {
throw new Error("unexpected")
},
quoteDomain: async () => {
throw new Error("unexpected")
},
destroyDomain: async () => {
throw new Error("unexpected")
},
getDomainNameServers: async () => {
throw new Error("unexpected")
},
destroyNode: async () => {
throw new Error("unexpected")
},
performNodeAction: async () => {
throw new Error("unexpected")
},
reinstallNode: async () => {
throw new Error("unexpected")
},
addNode: async () => {
throw new Error("unexpected")
},
materialize: async () => ({ activity: null }),
retryNode: async () => {
throw new Error("unexpected")
},
promoteNbdeNode: async () => {
throw new Error("unexpected")
},
reconcileNbde: async () => ({ nodes: [] }),
runNodeSsh: async () => {
throw new Error("unexpected")
},
getProvisioningMetricsStats: async () => {
throw new Error("unexpected")
}
}
}
async function withoutLegionUnlockPassword<T>(work: () => Promise<T>): Promise<T> {
const previous = process.env["LEGION_UNLOCK_PASSWORD"]
delete process.env["LEGION_UNLOCK_PASSWORD"]
try {
return await work()
} finally {
if (typeof previous === "string") {
process.env["LEGION_UNLOCK_PASSWORD"] = previous
} else {
delete process.env["LEGION_UNLOCK_PASSWORD"]
}
}
}
function createDomainListEntry(overrides: Partial<DomainListEntry> = {}): DomainListEntry {
return {
id: "domain-1",
domain: "example.test",
provider: "ovh",
providerId: "ovh",
source: "managed",
label: "example.test",
autoRenew: true,
observedAutoRenew: true,
registrantEmail: "ops@example.test",
zoneId: "dns-domain-1",
bound: true,
remoteId: "example.test",
remoteName: "example.test",
observedAt: "2026-04-10T00:00:00.000Z",
registrationStatus: "registered",
delegationStatus: "hosted",
desiredNameServers: ["dns10.ovh.net", "ns10.ovh.net"],
observedNameServers: ["dns10.ovh.net", "ns10.ovh.net"],
dnsMode: "hosted",
...overrides
}
}
function createDomainNameServersResult(
overrides: Partial<DomainNameServersResult> = {}
): DomainNameServersResult {
return {
domain: createDomainListEntry(),
zoneName: "example.test",
nameServers: ["dns10.ovh.net", "ns10.ovh.net"],
source: "zone-records",
...overrides
}
}
function createDomainQuoteResult(overrides: Partial<DomainQuoteResult> = {}): DomainQuoteResult {
return {
domain: "example.test",
provider: "ovh",
providerId: "ovh",
available: true,
source: "ovh-order-api",
offers: [
{
months: 12,
duration: "P1Y",
currency: "EUR",
totalGrossEur: 12,
monthlyGrossEur: 1,
planCode: "com",
pricingMode: "create-default"
}
],
...overrides
}
}
function createNodeListEntry(overrides: Partial<NodeListEntry> = {}): NodeListEntry {
return {
id: "srv-1",
name: "node-a",
provider: "hetzner",
providerServerId: "123",
publicIp: "203.0.113.10",
region: "fsn1",
plan: "CAX11",
status: "running",
role: "primary",
...overrides
}
}
class MockCliInput extends EventEmitter {
isTTY = true
rawMode = false
resume(): this {
return this
}
pause(): this {
return this
}
setRawMode(value: boolean): this {
this.rawMode = value
return this
}
}
test("parseCliArgs returns null for desktop argv", () => {
assert.equal(parseCliArgs(["electron", "out/main/index.js"]), null)
})
test("shouldCliMarkInterruptedOperations leaves read-only node commands passive", () => {
const list = parseCliArgs(["electron", "out/main/index.js", "node", "list", "--json"])
const ssh = parseCliArgs(["electron", "out/main/index.js", "ssh", "node-a", "--", "uptime"])
assert.ok(list)
assert.ok(ssh)
assert.equal(shouldCliMarkInterruptedOperations(list), false)
assert.equal(shouldCliMarkInterruptedOperations(ssh), false)
})
test("getCliRuntimePolicy allows only read-only commands to run without daemon", () => {
const senderStart = parseCliArgs(["legion", "sender", "start"])
const senderStatus = parseCliArgs(["legion", "sender", "status"])
const senderCapabilities = parseCliArgs(["legion", "sender", "capabilities", "node-a"])
const nodeList = parseCliArgs(["legion", "node", "list"])
const nodeCreate = parseCliArgs(["legion", "node", "create", "--name", "node-a"])
const providerList = parseCliArgs(["legion", "provider", "list"])
const providerConfigure = parseCliArgs([
"legion",
"provider",
"configure",
"hetzner",
"--api-key",
"x"
])
const domainQuote = parseCliArgs(["legion", "domain", "quote", "example.com"])
const configExport = parseCliArgs(["legion", "config", "export", "/tmp/state.json"])
const configImport = parseCliArgs(["legion", "config", "import", "/tmp/state.json"])
assert.ok(senderStart)
assert.ok(senderStatus)
assert.ok(senderCapabilities)
assert.ok(nodeList)
assert.ok(nodeCreate)
assert.ok(providerList)
assert.ok(providerConfigure)
assert.ok(domainQuote)
assert.ok(configExport)
assert.ok(configImport)
assert.equal(getCliRuntimePolicy(senderStart), "daemon-required")
assert.equal(getCliRuntimePolicy(senderStatus), "daemon-required")
assert.equal(getCliRuntimePolicy(senderCapabilities), "daemon-required")
assert.equal(getCliRuntimePolicy(nodeList), "local-ok")
assert.equal(getCliRuntimePolicy(nodeCreate), "daemon-required")
assert.equal(getCliRuntimePolicy(providerList), "local-ok")
assert.equal(getCliRuntimePolicy(providerConfigure), "daemon-required")
assert.equal(getCliRuntimePolicy(domainQuote), "local-ok")
assert.equal(getCliRuntimePolicy(configExport), "local-ok")
assert.equal(getCliRuntimePolicy(configImport), "daemon-required")
assert.equal(shouldCliRequireDaemon(nodeList), false)
assert.equal(shouldCliRequireDaemon(nodeCreate), true)
})
test("shouldCliMarkInterruptedOperations marks deployment commands active", () => {
const createApply = parseCliArgs([
"electron",
"out/main/index.js",
"node",
"create",
"--name",
"node-a",
"--apply"
])
const retry = parseCliArgs(["electron", "out/main/index.js", "node", "retry", "node-a"])
const apply = parseCliArgs(["electron", "out/main/index.js", "apply", "--json"])
assert.ok(createApply)
assert.ok(retry)
assert.ok(apply)
assert.equal(shouldCliMarkInterruptedOperations(createApply), true)
assert.equal(shouldCliMarkInterruptedOperations(retry), true)
assert.equal(shouldCliMarkInterruptedOperations(apply), true)
})
test("CLI help detection and rendering work for top-level help", () => {
assert.equal(isCliHelpRequest(["node", "src/engine/cli-main.ts", "help"]), true)
assert.equal(isCliHelpRequest(["node", "src/engine/cli-main.ts", "--help"]), true)
assert.equal(isCliHelpRequest(["node", "src/engine/cli-main.ts", "node", "list"]), false)
assert.equal(isCliVersionRequest(["node", "src/engine/cli-main.ts", "version"]), true)
assert.equal(isCliVersionRequest(["node", "src/engine/cli-main.ts", "--version"]), true)
assert.equal(isCliVersionRequest(["node", "src/engine/cli-main.ts", "node", "list"]), false)
assert.match(renderCliHelp(), /Usage:/)
assert.match(renderCliHelp(), /node create/)
assert.match(
renderCliHelp("/tmp/legion-state.json"),
/Default state file: \/tmp\/legion-state\.json/
)
assert.match(renderCliHelp(), /Run `legion version`/)
assert.match(renderCliVersion("/tmp/legion-state.json"), /State file: \/tmp\/legion-state\.json/)
assert.match(renderCliVersion(), /Legion K&K CLI v/)
})
test("parseCliArgs parses node list --json", () => {
assert.deepEqual(parseCliArgs(["electron", "out/main/index.js", "node", "list", "--json"]), {
scope: "nodes",
command: "list",
json: true
})
})
test("parseCliArgs parses provisioning metrics command", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"metrics",
"provisioning",
"--path",
"/tmp/provisioning.jsonl",
"--json"
]),
{
scope: "metrics",
command: "provisioning",
json: true,
provisioningMetricsStatsRequest: {
path: "/tmp/provisioning.jsonl"
}
}
)
})
test("parseCliArgs parses logs command", () => {
assert.deepEqual(
parseCliArgs([
"legion",
"logs",
"--tail",
"50",
"--since",
"2026-05-19T00:00:00.000Z",
"--severity",
"error",
"--type",
"app.crash",
"--node-id",
"node-pubkey",
"--target",
"node-a",
"--offset",
"10",
"--no-pager",
"--json"
]),
{
scope: "logs",
command: "list",
json: true,
noPager: true,
logEntriesRequest: {
targetRef: "node-a",
limit: 50,
offset: 10,
nodeId: "node-pubkey",
severity: "error",
type: "app.crash",
occurredAtGte: "2026-05-19T00:00:00.000Z"
}
}
)
})
test("parseCliArgs parses logs all-pages command", () => {
assert.deepEqual(
parseCliArgs(["legion", "logs", "--all", "--page-size", "5000", "--no-pager", "--json"]),
{
scope: "logs",
command: "list",
json: true,
noPager: true,
logEntriesRequest: {
all: true,
pageSize: 5000
}
}
)
})
test("parseCliArgs rejects logs all with tail", () => {
assert.throws(
() => parseCliArgs(["legion", "logs", "--all", "--tail", "10"]),
/--all cannot be combined with --tail/
)
})
test("parseCliArgs parses domain create with managed registrant details", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"domain",
"create",
"example.test",
"--provider",
"ovh",
"--managed",
"--first-name",
"Ada",
"--last-name",
"Lovelace",
"--email",
"ada@example.test",
"--phone",
"+49.30.555555",
"--street-1",
"Cloud Street 1",
"--city",
"Berlin",
"--postal-code",
"10115",
"--country",
"de"
]),
{
scope: "domains",
command: "add",
json: false,
domainAddRequest: {
domain: "example.test",
provider: "ovh",
source: "managed",
registrantContact: {
firstName: "Ada",
lastName: "Lovelace",
email: "ada@example.test",
phone: "+49.30.555555",
address: {
line1: "Cloud Street 1",
city: "Berlin",
postalCode: "10115",
countryCode: "de"
}
}
}
}
)
})
test("parseCliArgs parses domain delete --apply", () => {
assert.deepEqual(
parseCliArgs(["electron", "out/main/index.js", "domain", "delete", "dom-1", "--apply"]),
{
scope: "domains",
command: "destroy",
ref: "dom-1",
json: false,
materialize: true
}
)
})
test("parseCliArgs parses domain nameservers", () => {
assert.deepEqual(
parseCliArgs(["electron", "out/main/index.js", "domain", "nameservers", "example.test"]),
{
scope: "domains",
command: "nameservers",
ref: "example.test",
json: false
}
)
})
test("parseCliArgs parses domain quote", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"domain",
"quote",
"example.test",
"--provider",
"ovh"
]),
{
scope: "domains",
command: "quote",
json: false,
domainQuoteRequest: {
domain: "example.test",
provider: "ovh"
}
}
)
})
test("parseCliArgs parses node create installer options", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"node",
"create",
"--name",
"node-b",
"--kexec-image",
"/tmp/kexec.tar.gz",
"--channel-url",
"https://git.teralink.net/tribes/guix-tribes.git",
"--channel-branch",
"supertest-dev",
"--channel-commit",
"7fe4ad21cbd12b75865fff9dc9dc1f168b7bb054",
"--channel-introduction-commit",
"3a0fad9d32a71dc98dd998eab489ad1dba884204",
"--channel-introduction-signer",
"F29B A6DA 96E5 EC29 FDDE D994 8F4F 75B3 B19D 4784",
"--apply",
"--upstream",
"node-a"
]),
{
scope: "nodes",
command: "add",
json: false,
materialize: true,
addRequest: {
name: "node-b",
kexecImage: "/tmp/kexec.tar.gz",
channelUrl: "https://git.teralink.net/tribes/guix-tribes.git",
channelBranch: "supertest-dev",
channelCommit: "7fe4ad21cbd12b75865fff9dc9dc1f168b7bb054",
channelIntroductionCommit: "3a0fad9d32a71dc98dd998eab489ad1dba884204",
channelIntroductionSigner: "F29B A6DA 96E5 EC29 FDDE D994 8F4F 75B3 B19D 4784",
upstream: "node-a"
}
}
)
})
test("parseCliArgs parses node reinstall installer options", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"node",
"reinstall",
"node-a",
"--kexec-image",
"/tmp/kexec.tar.gz",
"--boot-mode",
"efi"
]),
{
scope: "nodes",
command: "reinstall",
ref: "node-a",
json: false,
reinstallRequest: {
kexecImage: "/tmp/kexec.tar.gz",
bootMode: "efi"
}
}
)
})
test("parseCliArgs parses node create low-level offer options", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"node",
"create",
"--provider",
"scaleway",
"--instance",
"dev1-m",
"--disk",
"80",
"--bandwidth",
"500"
]),
{
scope: "nodes",
command: "add",
json: false,
addRequest: {
provider: "scaleway",
instance: "dev1-m",
diskGb: 80,
bandwidthMbps: 500
}
}
)
})
test("parseCliArgs parses manual node create connection options", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"node",
"create",
"--name",
"manual-a",
"--provider",
"manual",
"--public-ip",
"203.0.113.10",
"--ssh-user",
"ubuntu",
"--password-env",
"MANUAL_PASSWORD"
]),
{
scope: "nodes",
command: "add",
json: false,
addRequest: {
name: "manual-a",
provider: "manual",
offerVariables: {
publicIp: "203.0.113.10",
sshUsername: "ubuntu"
},
offerVariableEnv: {
password: "MANUAL_PASSWORD"
}
}
}
)
})
test("parseCliArgs parses node offers", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"node",
"offers",
"--provider",
"hetzner",
"--min-cores",
"4",
"--min-memory",
"8",
"--max-hourly-eur",
"0.04",
"--json"
]),
{
scope: "nodes",
command: "offers-find",
json: true,
offerSearchRequest: {
providerKind: "hetzner",
minCores: 4,
minMemoryGb: 8,
maxHourlyGrossEur: 0.04
}
}
)
})
test("parseCliArgs normalizes node offer architecture", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"node",
"offers",
"--provider",
"hetzner",
"--architecture",
"x86",
"--json"
]),
{
scope: "nodes",
command: "offers-find",
json: true,
offerSearchRequest: {
providerKind: "hetzner",
architecture: "x86_64"
}
}
)
})
test("parseCliArgs rejects unsupported node offer architecture", () => {
assert.throws(
() =>
parseCliArgs(["electron", "out/main/index.js", "node", "offers", "--architecture", "arm64"]),
/Unsupported --architecture: arm64/
)
})
test("parseCliArgs parses apply", () => {
assert.deepEqual(parseCliArgs(["electron", "out/main/index.js", "apply", "--json"]), {
scope: "materialize",
command: "run",
json: true
})
assert.deepEqual(parseCliArgs(["electron", "out/main/index.js", "apply", "--wait"]), {
scope: "materialize",
command: "run",
json: false,
wait: true
})
assert.deepEqual(parseCliArgs(["electron", "out/main/index.js", "apply", "--trace"]), {
scope: "materialize",
command: "run",
json: false,
wait: true,
trace: true
})
})
test("parseCliArgs rejects unknown provider kinds for node create and node offers", () => {
assert.throws(
() =>
parseCliArgs([
"electron",
"out/main/index.js",
"node",
"create",
"--provider",
"unknown-cloud"
]),
/Unsupported provider for node create: unknown-cloud/
)
assert.throws(
() =>
parseCliArgs([
"electron",
"out/main/index.js",
"node",
"offers",
"--provider",
"unknown-cloud"
]),
/Unsupported provider for node offers: unknown-cloud/
)
})
test("parseCliArgs parses node edge options", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"node",
"create",
"--name",
"node-edge",
"--dns-name",
"node.example.test",
"--acme-email",
"ops@example.test"
]),
{
scope: "nodes",
command: "add",
json: false,
addRequest: {
name: "node-edge",
dnsName: "node.example.test",
acmeEmail: "ops@example.test"
}
}
)
})
test("parseCliArgs parses config init", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"config",
"init",
"--name",
"Legion E2E",
"--domain",
"example.test",
"--password-env",
"LEGION_TEST_PASSWORD"
]),
{
scope: "config",
command: "init",
json: false,
configInitRequest: {
name: "Legion E2E",
domain: "example.test",
passwordEnv: "LEGION_TEST_PASSWORD"
}
}
)
})
test("parseCliArgs parses config rekey", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"config",
"rekey",
"--current-password-env",
"LEGION_OLD_PASSWORD",
"--new-password-env",
"LEGION_NEW_PASSWORD"
]),
{
scope: "config",
command: "rekey",
json: false,
configRekeyRequest: {
currentPasswordEnv: "LEGION_OLD_PASSWORD",
newPasswordEnv: "LEGION_NEW_PASSWORD"
}
}
)
})
test("parseCliArgs parses config export", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"config",
"export",
"/tmp/legion-backup.json",
"--password-env",
"LEGION_BACKUP_PASSWORD"
]),
{
scope: "config",
command: "export",
json: false,
configExportRequest: {
out: "/tmp/legion-backup.json",
passwordEnv: "LEGION_BACKUP_PASSWORD"
}
}
)
})
test("parseCliArgs parses config import", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"config",
"import",
"/tmp/legion-backup.json",
"--password",
"backup-password"
]),
{
scope: "config",
command: "import",
json: false,
configImportRequest: {
in: "/tmp/legion-backup.json",
password: "backup-password"
}
}
)
})
test("parseCliArgs parses provider configure hetzner", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"provider",
"configure",
"hetzner",
"--api-key-env",
"HCLOUD_TOKEN"
]),
{
scope: "providers",
command: "configure",
json: false,
providerConfigureRequest: {
kind: "hetzner",
credentialEnv: {
apiKey: "HCLOUD_TOKEN"
}
}
}
)
})
test("parseCliArgs parses provider configure ovh", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"provider",
"configure",
"ovh",
"--endpoint",
"ovh-eu",
"--application-key-env",
"OVH_APP_KEY",
"--application-secret-env",
"OVH_APP_SECRET",
"--consumer-key-env",
"OVH_CONSUMER_KEY"
]),
{
scope: "providers",
command: "configure",
json: false,
providerConfigureRequest: {
kind: "ovh",
credentialValues: {
endpoint: "ovh-eu"
},
credentialEnv: {
applicationKey: "OVH_APP_KEY",
applicationSecret: "OVH_APP_SECRET",
consumerKey: "OVH_CONSUMER_KEY"
}
}
}
)
})
test("parseCliArgs parses provider configure manual", () => {
assert.deepEqual(
parseCliArgs(["electron", "out/main/index.js", "provider", "configure", "manual"]),
{
scope: "providers",
command: "configure",
json: false,
providerConfigureRequest: {
kind: "manual"
}
}
)
})
test("parseCliArgs parses provider configure scaleway", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"provider",
"configure",
"scaleway",
"--access-key-env",
"SCW_ACCESS_KEY",
"--secret-key-env",
"SCW_SECRET_KEY",
"--project-id-env",
"SCW_DEFAULT_PROJECT_ID"
]),
{
scope: "providers",
command: "configure",
json: false,
providerConfigureRequest: {
kind: "scaleway",
credentialEnv: {
accessKey: "SCW_ACCESS_KEY",
secretKey: "SCW_SECRET_KEY",
projectId: "SCW_DEFAULT_PROJECT_ID"
}
}
}
)
})
test("runCli writes list output as json", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
listNodes: () => [createNodeListEntry()]
}
const exitCode = await runCli(["node", "list", "--json"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.match(stdout, /"node-a"/)
})
test("runCli prompts for the Legion unlock password when the service is locked", async () => {
await withoutLegionUnlockPassword(async () => {
let stderr = ""
const stdin = new MockCliInput()
let unlockedWith: string | null = null
let locked = true
let listCalls = 0
const service = {
...createBaseService(),
listNodes: () => {
listCalls += 1
return []
},
hasPersistedState: async () => true,
isLocked: () => locked,
unlockWithPassword: async (password: string) => {
unlockedWith = password
locked = false
}
} satisfies LegionCliCommands & NodeCliUnlockable
setTimeout(() => {
stdin.emit("data", "secret\n")
}, 0)
const exitCode = await runCli(["node", "list"], service, {
stdout: { write: () => true },
stderr: { write: (chunk: string) => ((stderr += chunk), true) },
stdin
})
assert.equal(exitCode, 0)
assert.equal(unlockedWith, "secret")
assert.equal(listCalls, 1)
assert.equal(stdin.rawMode, false)
assert.match(stderr, /Legion unlock password:/)
})
})
test("runCli prompts for a new password and confirmation when no state file exists", async () => {
await withoutLegionUnlockPassword(async () => {
let stderr = ""
const stdin = new MockCliInput()
let unlockedWith: string | null = null
let locked = true
const service = {
...createBaseService(),
listNodes: () => [],
hasPersistedState: async () => false,
isLocked: () => locked,
unlockWithPassword: async (password: string) => {
unlockedWith = password
locked = false
}
} satisfies LegionCliCommands & NodeCliUnlockable
setTimeout(() => {
stdin.emit("data", "secret\n")
setTimeout(() => {
stdin.emit("data", "secret\n")
}, 0)
}, 0)
const exitCode = await runCli(["node", "list"], service, {
stdout: { write: () => true },
stderr: { write: (chunk: string) => ((stderr += chunk), true) },
stdin
})
assert.equal(exitCode, 0)
assert.equal(unlockedWith, "secret")
assert.equal(stdin.rawMode, false)
assert.match(stderr, /No Legion local state file was found/)
assert.match(stderr, /New Legion unlock password:/)
assert.match(stderr, /Confirm new Legion unlock password:/)
})
})
test("parseCliArgs parses NBDE commands", () => {
assert.deepEqual(parseCliArgs(["electron", "out/main/index.js", "node", "nbde", "reconcile"]), {
scope: "nodes",
command: "nbde-reconcile",
json: false
})
assert.deepEqual(
parseCliArgs(["electron", "out/main/index.js", "node", "nbde", "promote", "node-a"]),
{
scope: "nodes",
command: "nbde-promote",
ref: "node-a",
json: false
}
)
})
test("parseCliArgs parses node admin call", () => {
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"node",
"admin",
"call",
"node-a",
"--method",
"cluster_rollout_status",
"--params-json",
'{"rollout_id":"r1"}',
"--json"
]),
{
scope: "nodes",
command: "admin-call",
ref: "node-a",
json: true,
adminCallRequest: {
method: "cluster_rollout_status",
paramsJson: '{"rollout_id":"r1"}'
}
}
)
})
test("parseCliArgs parses sender commands", () => {
assert.deepEqual(parseCliArgs(["legion", "sender", "status", "--json"]), {
scope: "sender",
command: "status",
json: true,
senderStatusRequest: {}
})
assert.deepEqual(
parseCliArgs([
"legion",
"sender",
"start",
"--origin",
"node-a",
"--input-url",
"rtmp://node/live/key",
"--ffmpeg",
"/run/current-system/profile/bin/ffmpeg"
]),
{
scope: "sender",
command: "start",
json: false,
senderStartRequest: {
origin: "node-a",
inputUrl: "rtmp://node/live/key",
ffmpegExecutable: "/run/current-system/profile/bin/ffmpeg"
}
}
)
assert.deepEqual(
parseCliArgs(["legion", "sender", "key", "create", "node-a", "--label", "OBS"]),
{
scope: "sender",
command: "key-create",
ref: "node-a",
json: false,
senderCreateStreamKeyRequest: {
label: "OBS"
}
}
)
})
test("parseCliArgs parses ssh command aliases", () => {
assert.deepEqual(
parseCliArgs(["electron", "out/main/index.js", "ssh", "node-a", "--", "ss", "-lnt"]),
{
scope: "nodes",
command: "ssh",
ref: "node-a",
json: false,
sshRequest: {
command: ["ss", "-lnt"]
}
}
)
assert.deepEqual(
parseCliArgs([
"electron",
"out/main/index.js",
"node",
"ssh",
"node-a",
"--user",
"admin",
"--ssh-option",
"LogLevel=ERROR",
"--insecure-host-key"
]),
{
scope: "nodes",
command: "ssh",
ref: "node-a",
json: false,
sshRequest: {
command: [],
user: "admin",
insecureHostKey: true,
sshOptions: ["LogLevel=ERROR"]
}
}
)
})
test("parseCliArgs parses node rpc command", () => {
const parsed = parseCliArgs([
"electron",
"out/main/index.js",
"node",
"rpc",
"node-a",
"--user",
"admin",
"--",
"Application.started_applications()"
])
assert.equal(parsed?.scope, "nodes")
assert.equal(parsed?.command, "rpc")
assert.equal(parsed?.ref, "node-a")
assert.equal(parsed?.sshRequest?.command.length, 1)
assert.equal(parsed?.sshRequest?.user, "admin")
assert.match(parsed?.sshRequest?.command[0] ?? "", /tribes-app/)
assert.match(parsed?.sshRequest?.command[0] ?? "", /Application\.started_applications\(\)/)
})
test("parseCliArgs parses retry command", () => {
assert.deepEqual(parseCliArgs(["electron", "out/main/index.js", "node", "retry", "node-a"]), {
scope: "nodes",
command: "retry",
ref: "node-a",
json: false
})
})
test("parseCliArgs parses node provider action commands", () => {
assert.deepEqual(parseCliArgs(["electron", "out/main/index.js", "node", "reboot", "node-a"]), {
scope: "nodes",
command: "reboot",
ref: "node-a",
json: false
})
})
test("parseCliArgs parses delete --apply", () => {
assert.deepEqual(
parseCliArgs(["electron", "out/main/index.js", "node", "delete", "node-a", "--apply"]),
{
scope: "nodes",
command: "destroy",
ref: "node-a",
json: false,
materialize: true
}
)
})
test("runCli reports missing destroy reference", async () => {
let stdout = ""
let stderr = ""
const service = createBaseService()
const exitCode = await runCli(["node", "delete"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 1)
assert.equal(stdout, "")
assert.match(stderr, /requires a node reference/)
})
test("runCli writes add output and streams reporter setup", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
addNode: async (_request, options) => ({
node: {
...createNodeListEntry({
id: "node-b",
name: "node-b",
providerServerId: "456",
publicIp: "203.0.113.20"
})
},
activity: options?.materialize
? {
id: "activity-1",
kind: "materialize",
status: "running",
title: "Materialize",
stage: "reconcile",
message: "running",
progress: 50,
lines: [],
startedAt: "2026-04-07T00:00:00.000Z"
}
: null
})
}
const exitCode = await runCli(["node", "create", "--name", "node-b"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.match(stdout, /planned: node-b/)
})
test("runCli writes materialized add output", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
addNode: async () => ({
node: createNodeListEntry({
id: "node-b",
name: "node-b",
providerServerId: "456",
publicIp: "203.0.113.20"
}),
activity: null
})
}
const exitCode = await runCli(["node", "create", "--name", "node-b", "--apply"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.match(stdout, /provisioned: node-b/)
})
test("runCli writes NBDE reconcile output", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
reconcileNbde: async () => ({
nodes: [
createNodeListEntry({
id: "node-a",
nbdeMode: "degraded"
})
]
})
}
const exitCode = await runCli(["node", "nbde", "reconcile"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.match(stdout, /nbde-reconciled/)
assert.match(stdout, /node-a/)
})
test("runCli writes retry output", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
retryNode: async () => ({
node: {
...createNodeListEntry({
id: "node-b",
name: "node-b",
providerServerId: "456",
publicIp: "203.0.113.20"
})
},
activity: null
})
}
const exitCode = await runCli(["node", "retry", "node-b"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.match(stdout, /retried: node-b/)
})
test("runCli writes node provider action output", async () => {
let stdout = ""
let stderr = ""
const calls: Array<{ ref: string; action: string }> = []
const service: LegionCliCommands = {
...createBaseService(),
performNodeAction: async (ref, action) => {
calls.push({ ref, action })
return {
node: {
...createNodeListEntry({
id: "node-b",
name: "node-b",
providerServerId: "456",
publicIp: "203.0.113.20"
})
},
activity: null
}
}
}
const exitCode = await runCli(["node", "reboot", "node-b"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.deepEqual(calls, [{ ref: "node-b", action: "reboot" }])
assert.match(stdout, /rebooted: node-b/)
})
test("runCli writes reinstall output", async () => {
let stdout = ""
let stderr = ""
const calls: Array<{ ref: string; kexecImage?: string; bootMode?: string }> = []
const service: LegionCliCommands = {
...createBaseService(),
reinstallNode: async (ref, request) => {
calls.push({ ref, ...request })
return {
node: {
...createNodeListEntry({
id: "node-b",
name: "node-b",
providerServerId: "456",
publicIp: "203.0.113.20"
})
},
activity: null
}
}
}
const exitCode = await runCli(
["node", "reinstall", "node-b", "--kexec-image", "/tmp/kexec.tar.gz", "--boot-mode", "efi"],
service,
{
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
}
)
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.deepEqual(calls, [{ ref: "node-b", kexecImage: "/tmp/kexec.tar.gz", bootMode: "efi" }])
assert.match(stdout, /reinstall-triggered: node-b/)
})
test("runCli writes node admin call output as json", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
callNodeAdminMethod: async (_ref, method, params) => ({
nodeId: "node-a",
method,
result: {
ok: true,
echoed: params
}
})
}
const exitCode = await runCli(
[
"node",
"admin",
"call",
"node-a",
"--method",
"cluster_rollout_status",
"--params-json",
'{"rollout_id":"r1"}',
"--json"
],
service,
{
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
}
)
assert.equal(exitCode, 0)
assert.equal(stderr, "")
const payload = JSON.parse(stdout) as {
ok: boolean
status: string
nodeId: string
method: string
result: { ok: boolean; echoed: { rollout_id: string } }
}
assert.equal(payload.ok, true)
assert.equal(payload.status, "admin-called")
assert.equal(payload.nodeId, "node-a")
assert.equal(payload.method, "cluster_rollout_status")
assert.equal(payload.result.echoed.rollout_id, "r1")
})
test("runCli writes local log entries", async () => {
let stdout = ""
let stderr = ""
const requests: Array<{ limit?: number; severity?: string; all?: boolean; pageSize?: number }> =
[]
const service: LegionCliCommands = {
...createBaseService(),
listLogEntries: async (request) => {
requests.push({
limit: request?.limit,
severity: request?.severity,
all: request?.all,
pageSize: request?.pageSize
})
return {
targetNodeId: "node-a",
generatedAt: "2026-05-19T12:01:00.000Z",
entryCount: 2,
entries: [
{
id: "entry-2",
node_id: "bbbbbbbbbbbbbbbb",
source_kind: "core",
source_name: "tribes",
severity: "error",
facility: null,
type: "app.error",
message: "newer error",
occurred_at: "2026-05-19T12:00:02.000Z",
observed_at: "2026-05-19T12:00:03.000Z",
host: "node-b",
program: "tribes",
syslog_tag: null,
ingest_source: "syslog_jsonl",
payload: {},
metadata: {},
inserted_at: null,
updated_at: null
},
{
id: "entry-1",
node_id: "aaaaaaaaaaaaaaaa",
source_kind: "plugin",
source_name: "sender",
severity: "info",
facility: null,
type: "stream.started",
message: "older message",
occurred_at: "2026-05-19T12:00:01.000Z",
observed_at: "2026-05-19T12:00:02.000Z",
host: "node-a",
program: "tribes",
syslog_tag: null,
ingest_source: "syslog_jsonl",
payload: {},
metadata: {},
inserted_at: null,
updated_at: null
}
]
}
}
}
const exitCode = await runCli(["legion", "logs", "--tail", "2", "--severity", "error"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.deepEqual(requests, [{ limit: 2, severity: "error", all: undefined, pageSize: undefined }])
assert.match(
stdout,
/2026-05-19T12:00:01.000Z\taaaaaaaaaaaa\tinfo\tplugin.sender\tstream.started\ttribes\tolder message/
)
assert.match(
stdout,
/2026-05-19T12:00:02.000Z\tbbbbbbbbbbbb\terror\tcore.tribes\tapp.error\ttribes\tnewer error/
)
assert.ok(stdout.indexOf("older message") < stdout.indexOf("newer error"))
})
test("runCli requests all local log entries", async () => {
let stdout = ""
let stderr = ""
const requests: Array<{ all?: boolean; pageSize?: number }> = []
const service: LegionCliCommands = {
...createBaseService(),
listLogEntries: async (request) => {
requests.push({ all: request?.all, pageSize: request?.pageSize })
return {
targetNodeId: "node-a",
generatedAt: "2026-05-19T12:01:00.000Z",
entryCount: 0,
pageCount: 1,
entries: []
}
}
}
const exitCode = await runCli(
["legion", "logs", "--all", "--page-size", "5000", "--json"],
service,
{
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
}
)
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.deepEqual(requests, [{ all: true, pageSize: 5000 }])
assert.equal(JSON.parse(stdout).pageCount, 1)
})
test("runCli writes sender status output as json", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
getSenderStreamStatus: async (request) => ({
ok: true,
status: "sender-status",
warnings: [],
session: {
id: "session-1",
status: "running",
generationId: request?.generationId ?? "generation-1",
originNodeId: "node-a",
originEndpointId: "endpoint-1",
endpoints: [],
startedAt: "2026-05-15T00:00:00.000Z",
updatedAt: "2026-05-15T00:00:00.000Z"
}
})
}
const exitCode = await runCli(
["legion", "sender", "status", "--generation-id", "generation-2", "--json"],
service,
{
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
}
)
assert.equal(exitCode, 0)
assert.equal(stderr, "")
const payload = JSON.parse(stdout) as {
ok: boolean
status: string
session: { generationId: string }
}
assert.equal(payload.ok, true)
assert.equal(payload.status, "sender-status")
assert.equal(payload.session.generationId, "generation-2")
})
test("runCli delegates ssh command and returns ssh exit code", async () => {
let stdout = ""
let stderr = ""
const calls: Array<{ ref: string; command: string[] }> = []
const service: LegionCliCommands = {
...createBaseService(),
runNodeSsh: async (ref, request) => {
calls.push({ ref, command: request.command })
return { exitCode: 23 }
}
}
const exitCode = await runCli(["ssh", "node-a", "--", "uptime"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 23)
assert.equal(stdout, "")
assert.equal(stderr, "")
assert.deepEqual(calls, [{ ref: "node-a", command: ["uptime"] }])
})
test("runCli delegates rpc command over ssh", async () => {
let stdout = ""
let stderr = ""
const calls: Array<{ ref: string; command: string[] }> = []
const service: LegionCliCommands = {
...createBaseService(),
runNodeSsh: async (ref, request) => {
calls.push({ ref, command: request.command })
return { exitCode: 17 }
}
}
const exitCode = await runCli(["node", "rpc", "node-a", "--", "Node.self()"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 17)
assert.equal(stdout, "")
assert.equal(stderr, "")
assert.equal(calls.length, 1)
assert.equal(calls[0]?.ref, "node-a")
assert.match(calls[0]?.command[0] ?? "", /Node\.self\(\)/)
})
test("runCli writes provisioning metrics table without unlocking state", async () => {
let stdout = ""
let stderr = ""
const requests: Array<{ path?: string }> = []
const result: ProvisioningMetricsStatsResult = {
path: "/tmp/provisioning.raw.jsonl",
rows: [
{
provider: "hetzner",
instance: "cx33",
samples: 2,
succeeded: 1,
failed: 1,
durations: {
providerOrderToServerReady: {
count: 2,
min: 10,
median: 20,
max: 30
},
phaseTotal: {
count: 2,
min: 70,
median: 80,
max: 90
}
}
}
]
}
const service: LegionCliCommands & NodeCliUnlockable = {
...createBaseService(),
isLocked: () => true,
hasPersistedState: async () => {
throw new Error("unexpected unlock probe")
},
unlockWithPassword: async () => {
throw new Error("unexpected unlock")
},
getProvisioningMetricsStats: async (request) => {
requests.push(request ?? {})
return result
}
}
const exitCode = await runCli(
["metrics", "provisioning", "--path", "/tmp/provisioning.raw.jsonl"],
service,
{
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
}
)
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.deepEqual(requests, [{ path: "/tmp/provisioning.raw.jsonl" }])
assert.match(stdout, /Provider/)
assert.match(stdout, /Instance/)
assert.match(stdout, /hetzner/)
assert.match(stdout, /cx33/)
assert.match(stdout, /1m10s/)
assert.match(stdout, /1m20s/)
assert.match(stdout, /1m30s/)
})
test("runCli writes config init output", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
initConfig: async () => ({
config: {
name: "Legion E2E",
domain: "example.test"
}
})
}
const exitCode = await runCli(
["config", "init", "--name", "Legion E2E", "--domain", "example.test"],
service,
{
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
}
)
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.match(stdout, /configured: Legion E2E/)
})
test("runCli writes config rekey output", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
rekeyConfig: async () => ({
config: {
name: "Legion E2E",
domain: "example.test"
}
})
}
const exitCode = await runCli(
["config", "rekey", "--new-password-env", "LEGION_NEW_PASSWORD"],
service,
{
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
}
)
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.match(stdout, /rekeyed: Legion E2E/)
})
test("runCli writes config export output", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
exportConfig: async () => ({
path: "/tmp/legion-backup.json",
config: {
name: "Legion E2E",
domain: "example.test"
}
})
}
const exitCode = await runCli(["config", "export", "/tmp/legion-backup.json"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.match(stdout, /exported: \/tmp\/legion-backup\.json \(Legion E2E, example\.test\)/)
})
test("runCli writes config import output", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
importConfig: async () => ({
path: "/tmp/legion-backup.json",
config: {
name: "Legion E2E",
domain: "example.test"
}
})
}
const exitCode = await runCli(["config", "import", "/tmp/legion-backup.json"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.match(stdout, /imported: \/tmp\/legion-backup\.json \(Legion E2E, example\.test\)/)
})
test("runCli writes provider configure output", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
configureProvider: async () => ({
provider: {
id: "hetzner-e2e",
kind: "hetzner",
configured: true,
createdAt: "2026-04-06T00:00:00.000Z"
}
})
}
const exitCode = await runCli(["provider", "configure", "hetzner"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.match(stdout, /configured: hetzner \(hetzner-e2e\)/)
})
test("runCli writes materialize output", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
materialize: async () => ({
activity: {
id: "activity-1",
kind: "materialize",
status: "running",
title: "Materialize",
stage: "reconcile",
message: "Cluster reconcile finished",
progress: 100,
lines: [],
startedAt: "2026-04-07T00:00:00.000Z"
}
})
}
const exitCode = await runCli(["apply"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.match(stdout, /^materialized/m)
assert.match(stdout, /Cluster reconcile finished/)
})
test("runCli writes domain list output", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
listDomains: () => [createDomainListEntry()]
}
const exitCode = await runCli(["domain", "list"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.match(stdout, /example\.test/)
assert.match(stdout, /\tregistered\thosted\t/)
assert.match(stdout, /desired-ns\tdns10\.ovh\.net,ns10\.ovh\.net/)
assert.match(stdout, /observed-ns\tdns10\.ovh\.net,ns10\.ovh\.net/)
})
test("runCli prompts for managed domain registrant details", async () => {
const input = new MockCliInput()
const prompts = [
"Ada\n",
"Lovelace\n",
"\n",
"ada@example.test\n",
"+49.30.555555\n",
"Cloud Street 1\n",
"\n",
"Berlin\n",
"\n",
"10115\n",
"de\n"
]
const captured: Array<Record<string, unknown>> = []
const service: LegionCliCommands = {
...createBaseService(),
addDomain: async (request) => {
captured.push(request as Record<string, unknown>)
return { domain: createDomainListEntry(), activity: null }
}
}
let stderr = ""
const emitNextPrompt = (index = 0): void => {
if (index >= prompts.length) {
return
}
setTimeout(() => {
input.emit("data", prompts[index]!)
emitNextPrompt(index + 1)
}, 0)
}
emitNextPrompt()
const exitCode = await runCli(
["domain", "create", "example.test", "--provider", "ovh", "--managed"],
service,
{
stdout: { write: () => true },
stderr: { write: (chunk: string) => ((stderr += chunk), true) },
stdin: input
}
)
assert.equal(exitCode, 0)
assert.equal(captured.length, 1)
assert.deepEqual(captured[0]?.registrantContact, {
firstName: "Ada",
lastName: "Lovelace",
email: "ada@example.test",
phone: "+49.30.555555",
address: {
line1: "Cloud Street 1",
city: "Berlin",
postalCode: "10115",
countryCode: "DE"
}
})
assert.match(stderr, /Managed domain registrant details are required\./)
assert.match(
stderr,
/Provider domain catalogue: https:\/\/www\.ovhcloud\.com\/en\/domains\/tld\//
)
})
test("runCli writes a provider domain catalogue hint for managed domains without prompts", async () => {
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
addDomain: async () => ({ domain: createDomainListEntry(), activity: null }),
listProviders: () => [
{
id: "ovh",
kind: "ovh",
label: "OVH",
configured: true,
createdAt: "2026-04-10T00:00:00.000Z"
}
]
}
const exitCode = await runCli(
[
"domain",
"create",
"example.test",
"--provider",
"ovh",
"--managed",
"--first-name",
"Ada",
"--last-name",
"Lovelace",
"--email",
"ada@example.test",
"--phone",
"+49.30.555555",
"--street-1",
"Cloud Street 1",
"--city",
"Berlin",
"--postal-code",
"10115",
"--country",
"de"
],
service,
{
stdout: { write: () => true },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
}
)
assert.equal(exitCode, 0)
assert.match(
stderr,
/Provider domain catalogue: https:\/\/www\.ovhcloud\.com\/en\/domains\/tld\//
)
})
test("runCli writes domain nameserver output", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
getDomainNameServers: async () => createDomainNameServersResult()
}
const exitCode = await runCli(["domain", "nameservers", "example.test"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.match(stdout, /authoritative nameservers for example\.test/)
assert.match(stdout, /dns10\.ovh\.net/)
assert.match(stdout, /ns10\.ovh\.net/)
})
test("runCli writes domain quote output", async () => {
let stdout = ""
let stderr = ""
const service: LegionCliCommands = {
...createBaseService(),
quoteDomain: async () => createDomainQuoteResult()
}
const exitCode = await runCli(["domain", "quote", "example.test", "--provider", "ovh"], service, {
stdout: { write: (chunk: string) => ((stdout += chunk), true) },
stderr: { write: (chunk: string) => ((stderr += chunk), true) }
})
assert.equal(exitCode, 0)
assert.equal(stderr, "")
assert.match(stdout, /domain quote for example\.test via ovh/)
assert.match(stdout, /P1Y/)
assert.match(stdout, /12\.00 EUR/)
})