#!/usr/bin/env node const assert = require("assert"); const cp = require("child_process"); const fs = require("fs"); const os = require("os"); const path = require("path"); const repo = path.resolve(__dirname, ".."); const releaseRoot = path.resolve( process.env.DISASMER_PUBLIC_RELEASE_DIR || path.join(repo, "target/public-release-dryrun") ); const acceptanceRoot = path.join(repo, "target/acceptance"); const manifestPath = path.join(releaseRoot, "public-release-manifest.json"); const reportPath = path.join(acceptanceRoot, "cli-happy-path-live.json"); const serviceEndpoint = "https://disasmer.michelpaulissen.com:9443"; const serviceAddr = process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR || "disasmer.michelpaulissen.com:9443"; const oidcIssuer = process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_ISSUER_URL; const oidcCode = process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_CODE; const enabled = process.env.DISASMER_CLI_HAPPY_PATH_LIVE === "1"; const commands = []; function requireEnabled() { if (!enabled) { throw new Error( "DISASMER_CLI_HAPPY_PATH_LIVE=1 is required because this runs the live CLI happy path" ); } if (!oidcIssuer || !oidcCode) { throw new Error( "DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_ISSUER_URL and DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_CODE are required" ); } } function readJson(file) { return JSON.parse(fs.readFileSync(file, "utf8")); } function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }); } function nonInteractiveGitEnv(extra = {}) { return { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: process.env.GIT_ASKPASS || "/bin/false", SSH_ASKPASS: process.env.SSH_ASKPASS || "/bin/false", GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND || "ssh -o BatchMode=yes -o NumberOfPasswordPrompts=0", ...extra, }; } function commandEnv(command, env) { if (command !== "git") return env; return nonInteractiveGitEnv(env); } function run(command, args, options = {}) { commands.push([command, ...args].join(" ")); return cp.execFileSync(command, args, { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15 * 60 * 1000, ...options, env: commandEnv(command, options.env), }); } function runJson(command, args, options = {}) { const output = run(command, args, options); return parseJsonOutput(output); } function parseJsonOutput(output) { const trimmed = output.trim(); if (!trimmed) { throw new Error("expected JSON output, got empty stdout"); } try { return JSON.parse(trimmed); } catch (_) { // Commands may print progress before their final JSON report. Try each // trailing block so both compact and pretty-printed JSON are accepted. } const lines = trimmed.split(/\r?\n/).filter(Boolean); for (let index = lines.length - 1; index >= 0; index -= 1) { try { return JSON.parse(lines.slice(index).join("\n")); } catch (_) { // Keep scanning for the start of the trailing JSON value. } } throw new Error(`could not parse JSON output:\n${trimmed}`); } function executable(root, name) { return path.join(root, "bin", process.platform === "win32" ? `${name}.exe` : name); } function binaryArchive(manifest) { const platform = `${os.platform()}-${os.arch()}`; const asset = manifest.assets.find( (candidate) => candidate.name.startsWith("disasmer-public-binaries-") && candidate.name.includes(platform) ); assert(asset, `missing released binary archive for ${platform}`); assert(fs.existsSync(asset.file), `missing released binary archive ${asset.file}`); return asset.file; } function publicRepoUrl(manifest) { const url = process.env.DISASMER_PUBLIC_REPO_URL || process.env.DISASMER_PUBLIC_REPO_REMOTE || manifest.public_repo_url || manifest.public_repo_remote; assert(url, "public repository URL is required"); assert(url.includes("git.michelpaulissen.com")); return url; } function waitForJsonLine(child, label) { return new Promise((resolve, reject) => { let stdout = ""; let stderr = ""; const timer = setTimeout(() => { reject(new Error(`${label} did not produce a JSON line\n${stderr}`)); }, 120000); child.stdout.on("data", (chunk) => { stdout += chunk.toString(); try { const parsed = parseJsonOutput(stdout); clearTimeout(timer); resolve(parsed); return; } catch (_) { // Keep waiting for a complete JSON value. } }); child.stderr.on("data", (chunk) => { stderr += chunk.toString(); }); child.once("error", (error) => { clearTimeout(timer); reject(error); }); child.once("exit", (code) => { if (code !== 0) { clearTimeout(timer); reject(new Error(`${label} exited with ${code}\n${stderr}`)); } }); }); } async function stopChild(child) { if (!child || child.exitCode !== null) return; child.kill("SIGTERM"); await new Promise((resolve) => child.once("exit", resolve)); } async function main() { requireEnabled(); const manifest = readJson(manifestPath); assert.strictEqual(manifest.kind, "disasmer-public-release-dryrun"); assert.strictEqual(manifest.default_operator_endpoint, serviceEndpoint); assert.strictEqual(serviceAddr, "disasmer.michelpaulissen.com:9443"); const workRoot = fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-cli-happy-")); const installDir = path.join(workRoot, "install"); const checkout = path.join(workRoot, "public-repo"); ensureDir(installDir); run("tar", ["-xzf", binaryArchive(manifest), "-C", installDir]); run("git", ["clone", "--depth", "1", publicRepoUrl(manifest), checkout]); const disasmer = executable(installDir, "disasmer"); const disasmerNode = executable(installDir, "disasmer-node"); const projectDir = path.join(checkout, "examples/launch-build-demo"); const suffix = String(Date.now()); const tenant = `cli-${suffix}`; const project = `project-${suffix}`; const user = "user"; const cliNode = `cli-node-${suffix}`; const workerNode = `worker-${suffix}`; const scope = [ "--coordinator", serviceEndpoint, "--tenant", tenant, "--project-id", project, "--user", user, "--json", ]; const login = runJson( disasmer, [ "login", "--browser", "--json", "--complete-browser-code", oidcCode, "--oidc-issuer-url", oidcIssuer, "--tenant", tenant, "--project-id", project, "--user", user, ], { cwd: projectDir } ); assert.strictEqual(login.plan.coordinator, serviceEndpoint); assert.strictEqual(login.boundary.scoped_cli_session_received, true); const projectInit = runJson( disasmer, [ "project", "init", ...scope, "--new-project", project, "--name", "CLI Happy Path", "--yes", ], { cwd: projectDir } ); assert.strictEqual(projectInit.command, "project init"); assert.strictEqual(projectInit.project_config_written, true); assert.strictEqual(projectInit.coordinator_response.type, "project_created"); const projectList = runJson(disasmer, ["project", "list", ...scope], { cwd: projectDir, }); assert(projectList.project_count >= 1); const projectSelect = runJson( disasmer, ["project", "select", ...scope, project], { cwd: projectDir } ); assert.strictEqual(projectSelect.command, "project select"); const inspection = runJson(disasmer, ["inspect", "--project", ".", "--json"], { cwd: projectDir, }); const environments = inspection.metadata && Array.isArray(inspection.metadata.environments) ? inspection.metadata.environments : []; assert( environments.some((environment) => environment.name === "linux") ); assert(Array.isArray(inspection.source_provider_statuses)); assert(inspection.source_provider_manifest); const cliGrant = runJson( disasmer, ["node", "enroll", ...scope, "--grant", `cli-grant-${suffix}`], { cwd: projectDir } ); assert.strictEqual(cliGrant.command, "node enroll"); const attach = runJson( disasmer, [ "node", "attach", "--coordinator", serviceEndpoint, "--tenant", tenant, "--project-id", project, "--node", cliNode, "--public-key", `${cliNode}-public-key`, "--enrollment-grant", cliGrant.enrollment_grant.grant, "--json", ], { cwd: projectDir } ); assert.strictEqual(attach.command, "node attach"); assert.strictEqual(attach.boundary.used_enrollment_exchange, true); const workerGrant = runJson( disasmer, ["node", "enroll", ...scope, "--grant", `worker-grant-${suffix}`], { cwd: projectDir } ); const workerArgs = [ "--coordinator", serviceEndpoint, "--tenant", tenant, "--project-id", project, "--node", workerNode, "--public-key", `${workerNode}-public-key`, "--enrollment-grant", workerGrant.enrollment_grant.grant, "--worker", "--assignment-poll-ms", "500", "--emit-ready", ]; commands.push([disasmerNode, ...workerArgs].join(" ")); const worker = cp.spawn(disasmerNode, workerArgs, { cwd: projectDir }); try { const ready = await waitForJsonLine(worker, "CLI happy-path worker ready"); assert.strictEqual(ready.node_status, "ready"); const runReport = runJson(disasmer, ["run", "build", "--project", ".", "--json"], { cwd: projectDir, }); assert.strictEqual(runReport.command, "run"); assert.strictEqual(runReport.status, "task_launched"); assert.strictEqual(runReport.task_launch.type, "task_launched"); assert.strictEqual(runReport.task_launch.placement.node, workerNode); const completion = await waitForJsonLine(worker, "CLI happy-path worker completion"); assert.strictEqual(completion.node_status, "completed"); const processStatus = runJson( disasmer, ["process", "status", ...scope, "--process", "vp-current"], { cwd: projectDir } ); assert.strictEqual(processStatus.command, "process status"); assert(processStatus.current_task_count >= 1); const logs = runJson(disasmer, ["logs", ...scope, "--process", "vp-current"], { cwd: projectDir, }); assert(logs.log_entries.length >= 1); const artifacts = runJson( disasmer, ["artifact", "list", ...scope, "--process", "vp-current"], { cwd: projectDir } ); assert(artifacts.artifacts.length >= 1); const artifact = artifacts.artifacts[0].artifact; const download = runJson(disasmer, ["artifact", "download", ...scope, artifact], { cwd: projectDir, }); assert.strictEqual(download.download_session.link_issued, true); const restart = runJson( disasmer, ["process", "restart", ...scope, "--process", "vp-current", "--yes"], { cwd: projectDir } ); assert.strictEqual(restart.restart_request.accepted, true); const cancel = runJson( disasmer, ["process", "cancel", ...scope, "--process", "vp-current", "--yes"], { cwd: projectDir } ); assert.strictEqual(cancel.cancel_request.accepted, true); const report = { kind: "disasmer-cli-happy-path-live", release_name: manifest.release_name, source_commit: manifest.source_commit, service_addr: serviceAddr, default_operator_endpoint: serviceEndpoint, public_repository_url: publicRepoUrl(manifest), tenant, project, cli_node: cliNode, worker_node: workerNode, signed_in: true, cli_installed_and_login_ran: true, project_initialized_or_selected: true, inspect_discovered_linux: true, node_attached_with_cli: true, run_launched_task: true, work_ran_on_attached_node: true, status_and_logs_seen: true, artifact_download_requested: true, process_restart_requested: true, process_cancel_requested: true, commands, acceptance_result: "passed", }; ensureDir(path.dirname(reportPath)); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); console.log(`CLI happy-path live smoke passed: ${reportPath}`); } finally { await stopChild(worker); } } main().catch((error) => { console.error(error.stack || error.message); process.exit(1); });