// Validation tests for Asahi provisioning artifacts. // // Tests that can run WITHOUT Apple Silicon hardware: // 1. Shellcheck the generated firstboot script // 2. Verify the built rootfs ZIP structure // 3. Mount the rootfs and verify injected files // 4. Validate installer_data.json against the Asahi installer's Python parser // 5. Verify partition layout arithmetic // // Prerequisites: // - Run scripts/build-asahi-rootfs.sh first (creates asahi-repo/) // - shellcheck installed (dnf install ShellCheck) // - python3 installed // - root for loop mount (sudo) // // Run: sudo pnpm run test:integration:asahi-validate import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { existsSync, lstatSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs"; import { execSync, spawnSync } from "node:child_process"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { renderFirstbootScript } from "../../src/bastion/src/templates/asahi-firstboot.sh.js"; const PROJECT_ROOT = join(import.meta.dirname, "..", ".."); const ASAHI_REPO = join(PROJECT_ROOT, "asahi-repo"); const ASAHI_CACHE = join(PROJECT_ROOT, ".asahi-cache"); const IS_ROOT = process.getuid?.() === 0; function run(cmd: string, opts?: { timeout?: number }): string { const full = IS_ROOT ? cmd : `sudo ${cmd}`; return execSync(full, { encoding: "utf-8", stdio: "pipe", timeout: opts?.timeout ?? 60_000 }); } function hasBuiltArtifacts(): boolean { return existsSync(join(ASAHI_REPO, "fedora-asahi-lab.zip")) && existsSync(join(ASAHI_REPO, "installer_data.json")); } describe("asahi script validation", () => { it("firstboot script passes shellcheck", () => { const script = renderFirstbootScript({ hostname: "test-node", role: "infra", serverIp: "10.0.0.1", httpPort: 8080, sshKeys: ["ssh-ed25519 AAAA... user@host"], adminUser: "testadmin", mac: "aa:bb:cc:dd:ee:ff", }); const tmpFile = join(tmpdir(), `asahi-shellcheck-${Date.now()}.sh`); writeFileSync(tmpFile, script); try { const result = spawnSync("shellcheck", [ "-s", "bash", "-e", "SC2086,SC2164", // allow unquoted variables (intentional in some LVM commands) tmpFile, ], { encoding: "utf-8", stdio: "pipe", timeout: 30_000 }); if (result.status !== 0) { console.log("Shellcheck warnings/errors:"); console.log(result.stdout); } // Allow warnings (exit 1 for warnings), fail on errors (exit 2+) expect(result.status).toBeLessThan(2); } finally { try { rmSync(tmpFile); } catch { /* ignore */ } } }); it("firstboot script for worker role passes shellcheck", () => { const script = renderFirstbootScript({ hostname: "worker-node", role: "worker", serverIp: "10.0.0.1", httpPort: 8080, sshKeys: [], adminUser: "michal", mac: "00:11:22:33:44:55", }); const tmpFile = join(tmpdir(), `asahi-shellcheck-worker-${Date.now()}.sh`); writeFileSync(tmpFile, script); try { const result = spawnSync("shellcheck", ["-s", "bash", "-e", "SC2086,SC2164", tmpFile], { encoding: "utf-8", stdio: "pipe", timeout: 30_000 }); if (result.status !== 0) console.log(result.stdout); expect(result.status).toBeLessThan(2); } finally { try { rmSync(tmpFile); } catch { /* ignore */ } } }); it("firstboot script for vanilla role passes shellcheck", () => { const script = renderFirstbootScript({ hostname: "vanilla-node", role: "vanilla", serverIp: "10.0.0.1", httpPort: 8080, sshKeys: ["ssh-rsa AAAA... user@host"], adminUser: "admin", mac: "ff:ee:dd:cc:bb:aa", }); const tmpFile = join(tmpdir(), `asahi-shellcheck-vanilla-${Date.now()}.sh`); writeFileSync(tmpFile, script); try { const result = spawnSync("shellcheck", ["-s", "bash", "-e", "SC2086,SC2164", tmpFile], { encoding: "utf-8", stdio: "pipe", timeout: 30_000 }); if (result.status !== 0) console.log(result.stdout); expect(result.status).toBeLessThan(2); } finally { try { rmSync(tmpFile); } catch { /* ignore */ } } }); }); describe("asahi installer_data.json validation", () => { let installerData: Record; beforeAll(() => { if (!hasBuiltArtifacts()) { throw new Error("Run scripts/build-asahi-rootfs.sh first to generate artifacts"); } installerData = JSON.parse(readFileSync(join(ASAHI_REPO, "installer_data.json"), "utf-8")); }); it("has os_list with one entry", () => { const osList = installerData["os_list"] as unknown[]; expect(osList).toBeInstanceOf(Array); expect(osList.length).toBe(1); }); it("has required top-level fields", () => { const os = (installerData["os_list"] as Record[])[0]!; expect(os["name"]).toBeDefined(); expect(os["default_os_name"]).toBeDefined(); expect(os["boot_object"]).toBeDefined(); expect(os["next_object"]).toBeDefined(); expect(os["package"]).toBe("fedora-asahi-lab.zip"); expect(os["supported_fw"]).toBeInstanceOf(Array); expect((os["supported_fw"] as string[]).length).toBeGreaterThan(0); }); it("has 4 partitions (EFI + Boot + Root + Data)", () => { const os = (installerData["os_list"] as Record[])[0]!; const partitions = os["partitions"] as Record[]; expect(partitions).toHaveLength(4); expect(partitions[0]!["name"]).toBe("EFI"); expect(partitions[1]!["name"]).toBe("Boot"); expect(partitions[2]!["name"]).toBe("Root"); expect(partitions[3]!["name"]).toBe("Data"); }); it("EFI partition has correct format", () => { const os = (installerData["os_list"] as Record[])[0]!; const efi = (os["partitions"] as Record[])[0]!; expect(efi["type"]).toBe("EFI"); expect(efi["format"]).toBe("fat"); expect(efi["copy_firmware"]).toBe(true); // Size should be ~500MB in bytes const size = parseInt(String(efi["size"]).replace("B", ""), 10); expect(size).toBeGreaterThanOrEqual(500 * 1024 * 1024); }); it("Boot partition references boot.img", () => { const os = (installerData["os_list"] as Record[])[0]!; const boot = (os["partitions"] as Record[])[1]!; expect(boot["type"]).toBe("Linux"); expect(boot["image"]).toBe("boot.img"); }); it("Root partition does NOT expand", () => { const os = (installerData["os_list"] as Record[])[0]!; const root = (os["partitions"] as Record[])[2]!; expect(root["type"]).toBe("Linux"); expect(root["image"]).toBe("root.img"); expect(root["expand"]).toBe(false); }); it("Data partition expands for LVM", () => { const os = (installerData["os_list"] as Record[])[0]!; const data = (os["partitions"] as Record[])[3]!; expect(data["type"]).toBe("Linux"); expect(data["expand"]).toBe(true); expect(data["image"]).toBeUndefined(); // No image — empty partition for LVM }); it("partition sizes use bytes format (NB suffix)", () => { const os = (installerData["os_list"] as Record[])[0]!; const partitions = os["partitions"] as Record[]; for (const p of partitions) { const size = String(p["size"]); expect(size).toMatch(/^\d+B$/); } }); it("validates against Asahi installer Python parser", () => { // Download the Asahi installer and run its validation logic on our config const validation = spawnSync("python3", ["-c", ` import json, sys with open("${join(ASAHI_REPO, "installer_data.json")}") as f: data = json.load(f) errors = [] os_list = data.get("os_list", []) if not os_list: errors.append("Empty os_list") for os_entry in os_list: required = ["name", "default_os_name", "boot_object", "next_object", "package", "supported_fw", "partitions"] for field in required: if field not in os_entry: errors.append(f"Missing field: {field}") partitions = os_entry.get("partitions", []) if not partitions: errors.append("No partitions defined") has_efi = False has_root_image = False expand_count = 0 for p in partitions: if "name" not in p or "type" not in p or "size" not in p: errors.append(f"Partition missing name/type/size: {p}") if p.get("type") == "EFI": has_efi = True if p.get("format") != "fat": errors.append("EFI partition must be FAT format") if p.get("image"): has_root_image = True if p.get("expand"): expand_count += 1 # Validate size format size_str = str(p.get("size", "")) if not size_str.endswith("B") or not size_str[:-1].isdigit(): errors.append(f"Invalid size format: {size_str} (expected NB)") if not has_efi: errors.append("No EFI partition found") if not has_root_image: errors.append("No partition with root image found") if expand_count > 1: errors.append(f"Multiple expanding partitions ({expand_count}) — only one should expand") # Verify supported_fw is a list of strings fw = os_entry.get("supported_fw", []) if not isinstance(fw, list) or not all(isinstance(v, str) for v in fw): errors.append("supported_fw must be a list of strings") if errors: print("ERRORS:") for e in errors: print(f" - {e}") sys.exit(1) else: print("OK: installer_data.json is valid") `], { encoding: "utf-8", stdio: "pipe", timeout: 10_000 }); if (validation.status !== 0) { console.log(validation.stdout); console.log(validation.stderr); } expect(validation.stdout).toContain("OK"); expect(validation.status).toBe(0); }); }); describe("asahi rootfs ZIP validation", () => { beforeAll(() => { if (!hasBuiltArtifacts()) { throw new Error("Run scripts/build-asahi-rootfs.sh first to generate artifacts"); } }); it("ZIP contains required files", () => { const result = spawnSync("unzip", ["-l", join(ASAHI_REPO, "fedora-asahi-lab.zip")], { encoding: "utf-8", stdio: "pipe", timeout: 10_000 }); expect(result.stdout).toContain("boot.img"); expect(result.stdout).toContain("root.img"); expect(result.stdout).toContain("esp/"); }); it("boot.img is ~1GB", () => { const result = spawnSync("unzip", ["-l", join(ASAHI_REPO, "fedora-asahi-lab.zip")], { encoding: "utf-8", stdio: "pipe", timeout: 10_000 }); const bootLine = result.stdout.split("\n").find(l => l.includes("boot.img") && !l.includes("/")); expect(bootLine).toBeDefined(); const size = parseInt(bootLine!.trim().split(/\s+/)[0]!, 10); expect(size).toBeGreaterThan(500 * 1024 * 1024); // > 500MB expect(size).toBeLessThan(2 * 1024 * 1024 * 1024); // < 2GB }); it("root.img is > 3GB", () => { const result = spawnSync("unzip", ["-l", join(ASAHI_REPO, "fedora-asahi-lab.zip")], { encoding: "utf-8", stdio: "pipe", timeout: 10_000 }); const rootLine = result.stdout.split("\n").find(l => l.includes("root.img")); expect(rootLine).toBeDefined(); const size = parseInt(rootLine!.trim().split(/\s+/)[0]!, 10); expect(size).toBeGreaterThan(3 * 1024 * 1024 * 1024); // > 3GB }); it("rootfs contains lab-firstboot.sh", () => { const mountDir = join(tmpdir(), `asahi-rootfs-check-${Date.now()}`); const extractDir = join(tmpdir(), `asahi-rootfs-extract-${Date.now()}`); mkdirSync(mountDir); mkdirSync(extractDir); try { // Extract root.img from ZIP run(`unzip -o -j "${join(ASAHI_REPO, "fedora-asahi-lab.zip")}" root.img -d "${extractDir}"`); // Mount and check run(`mount -o loop,ro "${join(extractDir, "root.img")}" "${mountDir}"`); // Verify firstboot script expect(existsSync(join(mountDir, "usr/local/bin/lab-firstboot.sh"))).toBe(true); const script = readFileSync(join(mountDir, "usr/local/bin/lab-firstboot.sh"), "utf-8"); expect(script).toContain("#!/bin/bash"); expect(script).toContain("labvg"); expect(script).toContain("pvcreate"); // Verify systemd service expect(existsSync(join(mountDir, "etc/systemd/system/lab-firstboot.service"))).toBe(true); const service = readFileSync(join(mountDir, "etc/systemd/system/lab-firstboot.service"), "utf-8"); expect(service).toContain("lab-firstboot.sh"); // Verify service is enabled (symlink exists) const symlinkPath = join(mountDir, "etc/systemd/system/multi-user.target.wants/lab-firstboot.service"); let symlinkExists = false; try { lstatSync(symlinkPath); symlinkExists = true; } catch { /* not found */ } expect(symlinkExists).toBe(true); // Verify SSH keys expect(existsSync(join(mountDir, "root/.ssh/authorized_keys"))).toBe(true); // Verify lvm2 + xfsprogs are in the image const hasLvm = existsSync(join(mountDir, "usr/bin/pvcreate")) || existsSync(join(mountDir, "usr/sbin/pvcreate")); const hasXfs = existsSync(join(mountDir, "usr/bin/mkfs.xfs")) || existsSync(join(mountDir, "usr/sbin/mkfs.xfs")); expect(hasLvm).toBe(true); expect(hasXfs).toBe(true); } finally { run(`umount "${mountDir}" 2>/dev/null || true`); rmSync(mountDir, { recursive: true, force: true }); rmSync(extractDir, { recursive: true, force: true }); } }, 120_000); });