From dee3c20479a2eaccc7926055a15e0b5a020f7082 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 22 Jul 2026 23:42:04 +0100 Subject: [PATCH] Fix Lume SSH readiness probe --- scripts/lume/lib.sh | 11 ++++++++-- test/lume-scripts.test.ts | 43 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/scripts/lume/lib.sh b/scripts/lume/lib.sh index eeb6bce..78396aa 100755 --- a/scripts/lume/lib.sh +++ b/scripts/lume/lib.sh @@ -106,11 +106,18 @@ wait_for_ssh() { local ssh_exit for attempt in $(seq 1 60); do - # Use 'if' to suppress set -e so a failed SSH attempt does not abort the loop. + # Lume can return zero while reporting that the guest has no IP address yet. + # A probe of `true` is only ready when it is both successful and silent. if ssh_output="$(lume ssh "${LUME_VM_NAME}" --user "${GUEST_USER}" --password "${GUEST_PASSWORD}" --timeout 10 "true" 2>&1)"; then + ssh_exit=0 + else + ssh_exit=$? + fi + + if [[ "${ssh_exit}" -eq 0 ]] && [[ -z "${ssh_output}" ]]; then return 0 fi - ssh_exit=$? + log "wait_for_ssh attempt ${attempt}/60: exit=${ssh_exit} output=[${ssh_output}]" sleep 5 done diff --git a/test/lume-scripts.test.ts b/test/lume-scripts.test.ts index b57f03c..ac1739d 100644 --- a/test/lume-scripts.test.ts +++ b/test/lume-scripts.test.ts @@ -41,6 +41,7 @@ describe("Lume pool scripts", () => { expect(reconcile).toContain('spawn_detached'); expect(reconcile).toContain('"${SCRIPT_DIR}/run-slot.sh" --slot "${slot}"'); expect(read("scripts/lume/lib.sh")).toContain("default_guest_runner_path"); + expect(read("scripts/lume/lib.sh")).toContain('[[ -z "${ssh_output}" ]]'); expect(read("scripts/lume/lib.sh")).toContain('local runner_version="${RUNNER_VERSION}"'); expect(read("scripts/lume/lib.sh")).toContain("RUNNER_PATH=${runner_path}"); expect(read("scripts/lume/lib.sh")).toContain("RUNNER_VERSION=${runner_version}"); @@ -123,6 +124,14 @@ describe("Lume pool scripts", () => { expect(helper).toContain("request_runner_token"); }); + test("does not treat Lume SSH error output as readiness when it exits zero", () => { + const result = runWaitForSshProbe("Error: VM 'macos-runner-slot-01' has no IP address. Wait for it to boot completely."); + + expect(result.status).toBe(1); + expect(result.stderr).toContain("wait_for_ssh attempt 1/60: exit=0"); + expect(result.stderr).toContain("has no IP address"); + }); + test("provisions pinned Sparkle tools in the Lume base VM", () => { const provisionBase = read("scripts/lume/provision-base-vm.sh"); const sparkleInstaller = read("scripts/guest/install-sparkle-tools.sh"); @@ -243,3 +252,37 @@ function runRuntimeInstaller(options: { function writeExecutable(filePath: string, contents: string): void { fs.writeFileSync(filePath, `${contents}\n`, { encoding: "utf8", mode: 0o755 }); } + +function runWaitForSshProbe(output: string): { status: number | null; stderr: string } { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "lume-ssh-probe-")); + writeExecutable( + path.join(directory, "lume"), + ["#!/bin/bash", `printf '%s\\n' ${JSON.stringify(output)}`, "exit 0"].join("\n"), + ); + writeExecutable(path.join(directory, "seq"), ["#!/bin/bash", "printf '1\\n'"].join("\n")); + writeExecutable(path.join(directory, "sleep"), ["#!/bin/bash", "exit 0"].join("\n")); + + const result = spawnSync( + "bash", + [ + "-c", + [ + 'source "$1"', + 'log() { printf "%s\\n" "$*" >&2; }', + 'LUME_VM_NAME=macos-runner-slot-01', + 'GUEST_USER=lume', + 'GUEST_PASSWORD=test-password', + "wait_for_ssh", + ].join("\n"), + "bash", + path.resolve("scripts/lume/lib.sh"), + ], + { + encoding: "utf8", + env: { ...process.env, PATH: `${directory}:${process.env.PATH ?? ""}` }, + }, + ); + + fs.rmSync(directory, { force: true, recursive: true }); + return { status: result.status, stderr: result.stderr }; +}