import { spawnSync } from "node:child_process"; import { chmodSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { rootCertificates } from "node:tls"; import { afterEach, expect, test } from "vitest"; const binary = process.env.THT_INSTALLATION_TEST_CLI; const roots: string[] = []; afterEach(() => roots.splice(0).forEach((path) => rmSync(path, { recursive: true, force: true }))); function run(...args: string[]) { return spawnSync(binary!, [...args, "--json"], { encoding: "utf8", env: { ...process.env, PATH: "" }, input: "" }); } test.skipIf(!binary)("operator prepares, completes and repeatedly validates before any stack exists", () => { const root = realpathSync(mkdtempSync(join(tmpdir(), "application-documents-"))); roots.push(root); const workspace = join(root, "workspaces"), directory = join(root, "installation"); expect(run("workspace", "prepare", "--directory", workspace, "--id", "practice", "--name", "Practice").status).toBe(0); expect(run("installation", "prepare", "--directory", directory).status).toBe(0); const installation = join(directory, "thothii-installation.yaml"); const validate = () => run("--installation", installation, "installation", "validate", "--workspaces", workspace); expect(validate().status).toBe(1); // Visible placeholders cannot be approved. expect(run("installation", "credentials", "--directory", directory).status).toBe(0); for (const name of ["thothii-installation.yaml", "operator.env", "database-bootstrap.yaml"]) { const path = join(directory, name); writeFileSync(path, readFileSync(path, "utf8").replaceAll("CHANGE_ME", "practice")); } for (const [name, contents] of Object.entries({ "secrets.env": "OPENAI_API_KEY=PRIVATE_PROVIDER_VALUE\n", "database-password": "PRIVATE_DATABASE_VALUE", "git-credentials": "", "git-ca.pem": rootCertificates[0] })) { writeFileSync(join(directory, "secrets", name), contents, { mode: 0o600 }); } const before = readFileSync(installation, "utf8"); for (let index = 0; index < 2; index++) { const checked = validate(); expect(checked.stderr).toBe(""); expect(checked.stdout).not.toContain("PRIVATE_"); expect(checked.status, checked.stdout).toBe(0); expect(JSON.parse(checked.stdout).deferred_checks).toContain("release-assets"); } expect(readFileSync(installation, "utf8")).toBe(before); writeFileSync(installation, before.replace("interaction: openai/gpt-4.1-mini", "interaction: openai/nonexistent")); expect(validate().status).toBe(1); writeFileSync(installation, before); const authFile = join(directory, "secrets/pi-auth.json"); writeFileSync(authFile, "not-json"); expect(validate().status).toBe(1); writeFileSync(installation, before.replace("{mode: secret_env, apiKeyEnv: OPENAI_API_KEY}", "{mode: pi_auth}")); writeFileSync(authFile, JSON.stringify({ openai: { unrelated: true } })); expect(validate().status).toBe(1); writeFileSync(authFile, JSON.stringify({ " OpenAI ": { type: "api_key", key: "PRIVATE_PI_KEY" } })); expect(validate().status).toBe(1); writeFileSync(authFile, JSON.stringify({ openai: { type: "api_key", key: "PRIVATE_PI_KEY" } })); expect(validate().status).toBe(0); writeFileSync(installation, before); writeFileSync(authFile, "{}"); const bootstrap = join(directory, "database-bootstrap.yaml"); const originalBootstrap = readFileSync(bootstrap, "utf8"); writeFileSync(join(workspace, "practice", "private-password"), "PRIVATE_DATABASE_VALUE", { mode: 0o600 }); writeFileSync(bootstrap, originalBootstrap.replace(join(directory, "secrets/database-password"), join(workspace, "practice/private-password"))); expect(validate().status).toBe(1); writeFileSync(bootstrap, originalBootstrap); chmodSync(join(directory, "secrets/database-password"), 0o644); expect(validate().status).toBe(1); chmodSync(join(directory, "secrets/database-password"), 0o600); const env = join(directory, "operator.env"); writeFileSync(env, readFileSync(env, "utf8") + "THOTH_HTTP_PORT=8081\n"); expect(validate().status).toBe(1); }, 15_000);