// Build And Run Mac tests cover build and run mac script behavior. import { spawnSync } from "node:child_process"; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; const scriptPath = "scripts/build-and-run-mac.sh"; const tempRoots: string[] = []; function runStopExistingLocalApp(params: { fakeLsof?: string; fakePgrep: string }) { const root = mkdtempSync(join(tmpdir(), "openclaw-build-run-mac-test-")); tempRoots.push(root); const binDir = join(root, "bin"); const killCallsPath = join(root, "kill-calls.txt"); const pgrepCallsPath = join(root, "pgrep-calls.txt"); mkdirSync(binDir); for (const [name, body] of [ ["pgrep", params.fakePgrep], [ "lsof", params.fakeLsof ?? [ "#!/usr/bin/env bash", "pid=''", "while [[ $# -gt 0 ]]; do", ' if [[ "$1" == "-p" ]]; then pid="$2"; shift 2; continue; fi', " shift", "done", 'printf "p%s\\n" "$pid"', 'printf "n/worktree/apps/macos\\n"', "exit 0", ].join("\n"), ], [ "sed", [ "#!/usr/bin/env bash", 'if [[ "$1" == "-n" && "$2" == "s/^n//p" ]]; then', " /usr/bin/sed -n 's/^n//p'", "else", ' /usr/bin/sed "$@"', "fi", ].join("\n"), ], ["head", ["#!/usr/bin/env bash", 'exec /usr/bin/head "$@"'].join("\n")], ["sleep", "#!/usr/bin/env bash\nexit 0\n"], ] as const) { const toolPath = join(binDir, name); writeFileSync(toolPath, body); chmodSync(toolPath, 0o755); } const script = readFileSync(scriptPath, "utf8"); const stopFunction = script.slice( script.indexOf("process_cwd_matches()"), script.indexOf('printf "\\n▶️ Building'), ); const harnessPath = join(root, "stop-existing-local-app.sh"); writeFileSync( harnessPath, [ "#!/usr/bin/env bash", "set -euo pipefail", 'BIN_ABS="/worktree/apps/macos/.build-local/debug/OpenClaw"', 'BIN=".build-local/debug/OpenClaw"', 'APP_CWD="/worktree/apps/macos"', "kill() {", ' printf "%s\\n" "$*" >> "$OPENCLAW_TEST_KILL_CALLS"', ' touch "$OPENCLAW_TEST_KILLED_MARKER"', " return 0", "}", stopFunction, "stop_existing_local_app", ].join("\n"), ); chmodSync(harnessPath, 0o755); const result = spawnSync("bash", [harnessPath], { encoding: "utf8", env: { ...process.env, OPENCLAW_TEST_KILLED_MARKER: join(root, "killed"), OPENCLAW_TEST_KILL_CALLS: killCallsPath, OPENCLAW_TEST_PGREP_CALLS: pgrepCallsPath, OPENCLAW_TEST_PGREP_COUNT: join(root, "pgrep-count.txt"), PATH: `${binDir}:${process.env.PATH ?? ""}`, }, }); const killCalls = existsSync(killCallsPath) ? readFileSync(killCallsPath, "utf8") : ""; const pgrepCalls = existsSync(pgrepCallsPath) ? readFileSync(pgrepCallsPath, "utf8") : ""; return { killCalls, pgrepCalls, result }; } afterEach(() => { for (const root of tempRoots.splice(0)) { rmSync(root, { force: true, recursive: true }); } }); describe("scripts/build-and-run-mac.sh", () => { it("prints help before build or launch side effects", () => { const result = spawnSync("bash", [scriptPath, "--help"], { cwd: process.cwd(), encoding: "utf8", }); expect(result.status).toBe(0); expect(result.stdout).toContain("Usage: build-and-run-mac.sh"); expect(result.stdout).toContain("Build, stop, and relaunch"); expect(result.stderr).toBe(""); }); it("rejects unknown options before build or launch side effects", () => { const result = spawnSync("bash", [scriptPath, "--wat"], { cwd: process.cwd(), encoding: "utf8", }); expect(result.status).toBe(1); expect(result.stdout).toBe(""); expect(result.stderr.trim()).toBe("ERROR: Unknown build-and-run-mac option: --wat"); }); it("keeps launch logs isolated unless an explicit log path is provided", () => { const script = readFileSync(scriptPath, "utf8"); expect(script).toContain('cd "$APP_DIR"'); expect(script).toContain( 'LOG_PATH="${OPENCLAW_MAC_RUN_LOG:-$(mktemp "${TMPDIR:-/tmp}/openclaw-${PRODUCT}.XXXXXX.log")}"', ); expect(script).toContain('nohup "$BIN_ABS" >"$LOG_PATH" 2>&1 &'); expect(script).toContain('printf "Started $PRODUCT (PID $PID). Logs: $LOG_PATH\\n"'); expect(script).not.toContain("/tmp/openclaw.log"); }); it("stops only the local debug app binary before relaunching", () => { const script = readFileSync(scriptPath, "utf8"); const { killCalls, pgrepCalls, result } = runStopExistingLocalApp({ fakePgrep: [ "#!/usr/bin/env bash", `printf '%s\\n' "$*" >> "$OPENCLAW_TEST_PGREP_CALLS"`, 'count="$(cat "$OPENCLAW_TEST_PGREP_COUNT" 2>/dev/null || echo 0)"', 'next="$((count + 1))"', 'printf "%s\\n" "$next" > "$OPENCLAW_TEST_PGREP_COUNT"', 'if [[ "$2" == "/worktree/apps/macos/.build-local/debug/OpenClaw" ]]; then exit 1; fi', 'if [[ "$2" == ".build-local/debug/OpenClaw" && "$count" == "1" ]]; then echo 321; exit 0; fi', "exit 1", ].join("\n"), }); expect(result.status).toBe(0); expect(killCalls).toBe("321\n"); expect(pgrepCalls).toContain("-f /worktree/apps/macos/.build-local/debug/OpenClaw"); expect(pgrepCalls).toContain("-f .build-local/debug/OpenClaw"); expect(script).toContain('BIN_ABS="$(pwd)/$BIN"'); expect(script).toContain('pgrep -f "$BIN_ABS"'); expect(script).toContain('pgrep -f "$BIN"'); expect(script).toContain('kill "$pid"'); expect(script).not.toContain('killall -q "$PRODUCT"'); expect(script).not.toContain("pkill"); }); it("fails when the scoped local app process survives cleanup", () => { const { result } = runStopExistingLocalApp({ fakePgrep: [ "#!/usr/bin/env bash", 'if [[ "$2" == ".build-local/debug/OpenClaw" ]]; then echo 321; exit 0; fi', "exit 1", ].join("\n"), }); expect(result.status).toBe(1); }); });