Files
lab/bastion/src/bastion/tests/state.test.ts
Michal e87edfcfbd
Some checks failed
CI/CD / lint (pull_request) Failing after 11s
CI/CD / test (pull_request) Failing after 9s
CI/CD / typecheck (pull_request) Failing after 22s
CI/CD / build (pull_request) Has been skipped
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
feat: PXE debug boot mode for rescue/diagnostics
New `labctl provision debug <target>` command that PXE boots a machine
into Fedora rescue mode (inst.rescue) for live debugging. Auto-clears
after one boot so next reboot returns to normal.

Adds debug state to BastionState, dispatch routing, API endpoints,
labd command routing, and CLI with rescue workflow guide.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:25:44 +01:00

143 lines
4.3 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { StateManager } from "../src/services/state.js";
describe("StateManager", () => {
let testDir: string;
let stateFile: string;
let state: StateManager;
beforeEach(() => {
testDir = join(tmpdir(), `bastion-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
mkdirSync(testDir, { recursive: true });
stateFile = join(testDir, "state.json");
state = new StateManager(stateFile);
});
afterEach(() => {
rmSync(testDir, { recursive: true, force: true });
});
it("creates empty state on first load", () => {
const loaded = state.load();
expect(loaded).toEqual({
discovered: {},
install_queue: {},
installed: {},
debug: {},
});
});
it("init creates the state file", () => {
expect(existsSync(stateFile)).toBe(false);
state.init();
expect(existsSync(stateFile)).toBe(true);
const content = JSON.parse(readFileSync(stateFile, "utf-8"));
expect(content).toEqual({
discovered: {},
install_queue: {},
installed: {},
debug: {},
});
});
it("saves and loads state correctly", () => {
state.init();
state.update((s) => {
s.discovered["aa:bb:cc:dd:ee:ff"] = {
mac: "aa:bb:cc:dd:ee:ff",
product: "TestBox",
board: "TestBoard",
serial: "SN123",
manufacturer: "TestCorp",
cpu_model: "Test CPU",
cpu_cores: 8,
memory_gb: 32,
arch: "x86_64",
disks: [{ name: "sda", size_gb: 500, model: "TestDisk" }],
nics: [{ name: "eth0", mac: "aa:bb:cc:dd:ee:ff", state: "UP" }],
first_seen: "2025-01-01T00:00:00Z",
last_seen: "2025-01-01T00:00:00Z",
};
s.install_queue["11:22:33:44:55:66"] = {
hostname: "worker-1",
disk: "/dev/sda",
role: "worker",
queued_at: "2025-01-01T01:00:00Z",
};
});
// Load in a fresh StateManager to verify persistence
const state2 = new StateManager(stateFile);
const loaded = state2.load();
expect(loaded.discovered["aa:bb:cc:dd:ee:ff"]?.product).toBe("TestBox");
expect(loaded.discovered["aa:bb:cc:dd:ee:ff"]?.cpu_cores).toBe(8);
expect(loaded.install_queue["11:22:33:44:55:66"]?.hostname).toBe("worker-1");
expect(loaded.installed).toEqual({});
});
it("uses atomic writes (tmp file + rename)", () => {
state.init();
// After save, there should be no .tmp file left behind
state.update((s) => {
s.installed["aa:bb:cc:dd:ee:ff"] = {
hostname: "node1",
role: "worker",
ip: "10.0.0.1",
installed_at: "2025-01-01T00:00:00Z",
};
});
const tmpFile = `${stateFile}.tmp`;
expect(existsSync(tmpFile)).toBe(false);
expect(existsSync(stateFile)).toBe(true);
// Verify data was written correctly
const raw = readFileSync(stateFile, "utf-8");
const parsed = JSON.parse(raw);
expect(parsed.installed["aa:bb:cc:dd:ee:ff"].hostname).toBe("node1");
});
});
describe("PID file handling", () => {
let testDir: string;
beforeEach(() => {
testDir = join(tmpdir(), `bastion-pid-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
mkdirSync(testDir, { recursive: true });
});
afterEach(() => {
rmSync(testDir, { recursive: true, force: true });
});
it("handles stale PID file from previous run", () => {
const pidFile = join(testDir, "bastion.pid");
// Simulate a stale PID file with a dead process
writeFileSync(pidFile, "999999999");
// Should be readable
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
expect(pid).toBe(999999999);
});
it("handles corrupted PID file gracefully", () => {
const pidFile = join(testDir, "bastion.pid");
writeFileSync(pidFile, "not-a-number\n");
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
expect(isNaN(pid)).toBe(true);
});
it("handles missing bastion directory", () => {
const missingDir = join(testDir, "nonexistent", "deep");
mkdirSync(missingDir, { recursive: true });
expect(existsSync(missingDir)).toBe(true);
});
});