import assert from "node:assert/strict" import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" import test from "node:test" import { resolveRuntimeConfig } from "../src/config.js" import type { LegionTrackedServer } from "../src/legion-state.js" import { HETZNER_CAPACITY_MAX_ATTEMPTS, HETZNER_CAPACITY_RETRY_DELAY_MS, RunInterruptedError, ScenarioExecution, buildCompareSystemGenerationsScript, buildLocalBootKeyStateScript, buildNodeCreateArgs, buildSshAccessCommand, buildTribesLogRoundTripRpcExpression, buildVictoriaMetricsLatestSnapshotScript, assertDegradedNbdeShape, extractGuixBuildLogPaths, extractGuixBuildLogPathsFromText, extractReportedNodePubkey, isRunInterruptedError } from "../src/runner.js" test("scenario execution starts Legion daemon before config init and stops it", async () => { const tempDir = await mkdtemp(join(tmpdir(), "tribes-supertest-runner-")) try { const invocationLogPath = join(tempDir, "legion-invocations.jsonl") const fakeCliPath = join(tempDir, "fake-legion-cli.mjs") await writeFile(fakeCliPath, buildFakeLegionCli(invocationLogPath)) const config = resolveRuntimeConfig( "daemon-lifecycle", { LEGION_UNLOCK_PASSWORD: "unlock-secret", SUPERTEST_ARTIFACT_ROOT: join(tempDir, "artifacts"), SUPERTEST_CERT_MODE: "self-signed", SUPERTEST_LEGION_CLI_ENTRY: fakeCliPath, SUPERTEST_LEGION_REPO: process.cwd() }, process.cwd(), new Date("2026-04-09T12:34:56.000Z") ) await new ScenarioExecution(config, { name: "daemon-lifecycle", description: "daemon lifecycle test", estimatedDuration: "1m", phases: [], requiredProviders: [], execute: async () => {} }).run() const invocations = (await readFile(invocationLogPath, "utf8")) .trim() .split("\n") .map((line) => JSON.parse(line) as { args: string[]; daemonToken: string | null }) const daemonStartIndex = invocations.findIndex( (entry) => entry.args.join(" ") === "daemon start" ) const daemonStatusIndex = invocations.findIndex( (entry) => entry.args.join(" ") === "daemon status" ) const configInitIndex = invocations.findIndex((entry) => entry.args.join(" ").startsWith("config init ") ) const daemonStopIndex = invocations.findIndex((entry) => entry.args.join(" ") === "daemon stop") assert.notEqual(daemonStartIndex, -1) assert.notEqual(daemonStatusIndex, -1) assert.notEqual(configInitIndex, -1) assert.notEqual(daemonStopIndex, -1) assert.ok(daemonStartIndex < daemonStatusIndex) assert.ok(daemonStatusIndex < configInitIndex) assert.ok(configInitIndex < daemonStopIndex) assert.equal(invocations[daemonStartIndex]!.daemonToken, null) assert.equal(invocations[daemonStatusIndex]!.daemonToken, "fake-daemon-token") assert.equal(invocations[configInitIndex]!.daemonToken, "fake-daemon-token") assert.equal(invocations[daemonStopIndex]!.daemonToken, "fake-daemon-token") } finally { await rm(tempDir, { recursive: true, force: true }) } }) test("scenario execution keeps Legion daemon running when keep nodes is enabled", async () => { const tempDir = await mkdtemp(join(tmpdir(), "tribes-supertest-runner-")) try { const invocationLogPath = join(tempDir, "legion-invocations.jsonl") const fakeCliPath = join(tempDir, "fake-legion-cli.mjs") await writeFile(fakeCliPath, buildFakeLegionCli(invocationLogPath)) const config = resolveRuntimeConfig( "daemon-lifecycle-keep-nodes", { LEGION_UNLOCK_PASSWORD: "unlock-secret", SUPERTEST_ARTIFACT_ROOT: join(tempDir, "artifacts"), SUPERTEST_CERT_MODE: "self-signed", SUPERTEST_KEEP_NODES: "1", SUPERTEST_LEGION_CLI_ENTRY: fakeCliPath, SUPERTEST_LEGION_REPO: process.cwd() }, process.cwd(), new Date("2026-04-09T12:34:56.000Z") ) await new ScenarioExecution(config, { name: "daemon-lifecycle-keep-nodes", description: "daemon lifecycle keep nodes test", estimatedDuration: "1m", phases: [], requiredProviders: [], execute: async () => {} }).run() const invocations = (await readFile(invocationLogPath, "utf8")) .trim() .split("\n") .map((line) => JSON.parse(line) as { args: string[]; daemonToken: string | null }) assert.equal( invocations.some((entry) => entry.args.join(" ") === "daemon stop"), false ) assert.equal(invocations[0]!.args.join(" "), "daemon start") assert.equal(invocations[0]!.daemonToken, null) assert.ok( invocations.some( (entry) => entry.args.join(" ") === "daemon status" && entry.daemonToken === "fake-daemon-token" ) ) } finally { await rm(tempDir, { recursive: true, force: true }) } }) function buildFakeLegionCli(invocationLogPath: string): string { return ` import { appendFileSync, mkdirSync, writeFileSync } from "node:fs" import { join } from "node:path" const args = process.argv.slice(2) appendFileSync(${JSON.stringify(invocationLogPath)}, JSON.stringify({ args, daemonToken: process.env.LEGION_DAEMON_TOKEN || null }) + "\\n") if (args[0] === "daemon" && args[1] === "start") { console.log("export LEGION_DAEMON_TOKEN='fake-daemon-token'") process.exit(0) } if (args[0] === "daemon" && args[1] === "status") { if (process.env.LEGION_DAEMON_TOKEN === "fake-daemon-token") { console.log("daemon: running") process.exit(0) } console.error("daemon: missing token") process.exit(1) } if (args[0] === "daemon" && args[1] === "stop") { process.exit(0) } if (args[0] === "config" && args[1] === "init") { if (process.env.LEGION_DAEMON_TOKEN !== "fake-daemon-token") { console.error("config init requires daemon token") process.exit(1) } mkdirSync(process.env.LEGION_STATE_DIR, { recursive: true }) writeFileSync(join(process.env.LEGION_STATE_DIR, "tribes-state.json"), JSON.stringify({ trackedServers: [] })) console.log(JSON.stringify({ ok: true })) process.exit(0) } if (args[0] === "node" && args[1] === "list") { console.log("[]") process.exit(0) } if (args[0] === "provider" && args[1] === "list") { console.log("[]") process.exit(0) } if (args[0] === "logs") { if (args.includes("--json")) { console.log(JSON.stringify({ ok: true, entries: [] })) } else { console.log("No local log entries matched.") } process.exit(0) } console.error("unexpected fake Legion args: " + args.join(" ")) process.exit(1) ` } test("Hetzner capacity retry policy waits three minutes and retries once", () => { assert.equal(HETZNER_CAPACITY_MAX_ATTEMPTS, 2) assert.equal(HETZNER_CAPACITY_RETRY_DELAY_MS, 180_000) }) test("extractGuixBuildLogPathsFromText parses log paths from streamed output", () => { const text = `building /gnu/store/foo-tribes.drv... View build log at '/var/log/guix/drvs/5s/xgpl8x6bi687kr204c1z35gxdxl77j-tribes.drv.gz'. cannot build derivation ...` assert.deepEqual(extractGuixBuildLogPathsFromText(text), [ "/var/log/guix/drvs/5s/xgpl8x6bi687kr204c1z35gxdxl77j-tribes.drv.gz" ]) }) test("extractGuixBuildLogPaths returns unique guix build log paths from error text", () => { const error = new Error(`Legion command failed: nodes add --materialize stdout: ... stderr: View build log at '/var/log/guix/drvs/8k/xd0qsk4wv8z4zsc4nd2dn1ggi11a75-tribes.drv.gz'. cannot build derivation ... View build log at '/var/log/guix/drvs/8k/xd0qsk4wv8z4zsc4nd2dn1ggi11a75-tribes.drv.gz'. View build log at '/var/log/guix/drvs/ab/cdef1234-profile.drv.gz'.`) assert.deepEqual(extractGuixBuildLogPaths(error), [ "/var/log/guix/drvs/8k/xd0qsk4wv8z4zsc4nd2dn1ggi11a75-tribes.drv.gz", "/var/log/guix/drvs/ab/cdef1234-profile.drv.gz" ]) }) test("buildCompareSystemGenerationsScript inspects the newest two generations read-only", () => { const script = buildCompareSystemGenerationsScript() assert.doesNotMatch(script, /tribes-compare-system-generations/) assert.doesNotMatch(script, /command -v guix|\bguix\s/) assert.match(script, /\/var\/guix\/profiles\/system-\*-link/) assert.match(script, /tail -n 2/) assert.match(script, /readlink -f \/run\/current-system/) assert.match(script, /configuration\.scm/) assert.match(script, /compare-system-generations-readonly-ok/) }) test("buildVictoriaMetricsLatestSnapshotScript queries latest samples for all metrics", () => { const script = buildVictoriaMetricsLatestSnapshotScript() assert.match(script, /127\.0\.0\.1:8428\/api\/v1\/query/) assert.match(script, /query=\{__name__=~"\.\+"\}/) assert.match(script, /curl -fsS/) }) test("buildTribesLogRoundTripRpcExpression logs through the running node group leader", () => { const expression = buildTribesLogRoundTripRpcExpression("[source=core] hello") assert.match(expression, /old_group_leader = Process\.group_leader\(\)/) assert.match(expression, /Process\.group_leader\(self\(\), Process\.whereis\(:user\)\)/) assert.match(expression, /Logger\.info\("\[source=core\] hello"\)/) assert.match(expression, /Process\.group_leader\(self\(\), old_group_leader\)/) }) test("buildLocalBootKeyStateScript follows deployed system facts", () => { const script = buildLocalBootKeyStateScript() assert.match(script, /\/etc\/tribes\/system-facts\.json/) assert.match(script, /localBootKeyFile/) assert.match(script, /\/boot\/nbde\/local-boot\.key/) assert.doesNotMatch(script, /\/etc\/legion\/nbde\/local-boot\.key/) }) test("buildSshAccessCommand renders a reusable SSH command for a tracked node", () => { const server: LegionTrackedServer = { id: "st-20260413-hx-a", sshUsername: "root", sshPrivateKey: "unused", publicIp: "188.245.174.206" } assert.equal( buildSshAccessCommand(server, "/tmp/kept-node.ed25519"), "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /tmp/kept-node.ed25519 root@188.245.174.206" ) }) test("assertDegradedNbdeShape accepts Legion two-node degraded NBDE state", () => { assert.doesNotThrow(() => assertDegradedNbdeShape( { id: "st-20260611-hx-c", sshUsername: "root", sshPrivateKey: "unused", publicIp: "203.0.113.10", nbde: { mode: "degraded", localBootKeyPresent: true, peerTangNodeIds: [] } }, "Join node st-20260611-hx-c" ) ) }) test("assertDegradedNbdeShape rejects clustered peer bindings", () => { assert.throws( () => assertDegradedNbdeShape( { id: "st-20260611-hx-c", sshUsername: "root", sshPrivateKey: "unused", publicIp: "203.0.113.10", nbde: { mode: "degraded", localBootKeyPresent: true, peerTangNodeIds: ["st-20260611-hx-a"] } }, "Join node st-20260611-hx-c" ), /should not have peer Tang bindings/ ) }) test("buildNodeCreateArgs includes dev Guix channel overrides", () => { assert.deepEqual( buildNodeCreateArgs({ id: "st-20260508-node-a", provider: "hetzner", acmeEmail: "hostmaster@tribe-one.org", channelUrl: "https://git.teralink.net/tribes/guix-tribes.git", channelBranch: "supertest-dev", channelCommit: "1d74e0ccccf5bb1277b101b791db4a5e6af7a1ad", channelIntroductionCommit: "7c4f9d3b3477945ca75d22baa237c44895f2e454", channelIntroductionSigner: "F29B A6DA 96E5 EC29 FDDE D994 8F4F 75B3 B19D 4784" }), [ "node", "create", "--name", "st-20260508-node-a", "--provider", "hetzner", "--acme-email", "hostmaster@tribe-one.org", "--channel-url", "https://git.teralink.net/tribes/guix-tribes.git", "--channel-branch", "supertest-dev", "--channel-commit", "1d74e0ccccf5bb1277b101b791db4a5e6af7a1ad", "--channel-introduction-commit", "7c4f9d3b3477945ca75d22baa237c44895f2e454", "--channel-introduction-signer", "F29B A6DA 96E5 EC29 FDDE D994 8F4F 75B3 B19D 4784" ] ) }) test("buildNodeCreateArgs includes manual connection options", () => { assert.deepEqual( buildNodeCreateArgs({ id: "st-20260609-manual-a", provider: "manual", acmeEmail: "hostmaster@tribe-one.org", instance: "manual:x86_64:ubuntu-compatible", manualHostIp: "203.0.113.10", manualUsername: "ubuntu", manualPasswordEnvName: "SUPERTEST_MANUAL_PASSWORD" }), [ "node", "create", "--name", "st-20260609-manual-a", "--provider", "manual", "--acme-email", "hostmaster@tribe-one.org", "--instance", "manual:x86_64:ubuntu-compatible", "--public-ip", "203.0.113.10", "--ssh-user", "ubuntu", "--password-env", "SUPERTEST_MANUAL_PASSWORD" ] ) }) test("extractReportedNodePubkey accepts JSON helper output", () => { assert.equal( extractReportedNodePubkey( '{"node":{"pubkey":"e1ad75fa88b59593360e3fa14727a68787dea4d8e981eec3983c0f7779fe04c3"},"bootstrap":{"status":"ready"},"health":"ok"}' ), "e1ad75fa88b59593360e3fa14727a68787dea4d8e981eec3983c0f7779fe04c3" ) }) test("extractReportedNodePubkey rejects non-JSON output", () => { assert.equal( extractReportedNodePubkey("e1ad75fa88b59593360e3fa14727a68787dea4d8e981eec3983c0f7779fe04c3\n"), null ) }) test("isRunInterruptedError recognizes interrupted run errors", () => { assert.equal(isRunInterruptedError(new RunInterruptedError("SIGINT")), true) assert.equal(isRunInterruptedError(new Error("plain failure")), false) })