import { spawnSync } from "node:child_process"; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; const tempDirs: string[] = []; const scriptPath = "scripts/codesign-mac-app.sh"; function makeTempDir(prefix: string): string { const dir = mkdtempSync(path.join(tmpdir(), prefix)); tempDirs.push(dir); return dir; } function entitlementTemps(dir: string): string[] { return readdirSync(dir).filter((name) => name.startsWith("openclaw-entitlements")); } function runCodesign(args: string[], tempRoot: string) { return spawnSync("bash", [scriptPath, ...args], { cwd: process.cwd(), encoding: "utf8", env: { ...process.env, TMPDIR: tempRoot, }, }); } function installFakeCodesign(binDir: string) { const fakeCodesign = path.join(binDir, "codesign"); writeFileSync( fakeCodesign, `#!/usr/bin/env bash set -euo pipefail entitlements="" target="" while [ "$#" -gt 0 ]; do case "$1" in --entitlements) shift entitlements="$1" ;; esac target="$1" shift || true done if [ -z "$target" ]; then echo "missing codesign target" >&2 exit 2 fi if [ -n "$entitlements" ]; then count_file="$CODESIGN_CAPTURE_DIR/count" count=0 if [ -f "$count_file" ]; then count="$(cat "$count_file")" fi count=$((count + 1)) printf '%s' "$count" >"$count_file" copy="$CODESIGN_CAPTURE_DIR/entitlements-$count.plist" cp "$entitlements" "$copy" printf 'entitled\\t%s\\t%s\\t%s\\n' "$target" "$entitlements" "$copy" >>"$CODESIGN_LOG" else printf 'plain\\t%s\\n' "$target" >>"$CODESIGN_LOG" fi `, ); chmodSync(fakeCodesign, 0o755); } afterEach(() => { for (const dir of tempDirs.splice(0)) { rmSync(dir, { recursive: true, force: true }); } }); describe("codesign-mac-app temp file hygiene", () => { it("does not generate unused entitlement plist files", () => { const script = readFileSync(scriptPath, "utf8"); expect(script).toContain('ENT_TMP_APP="$ENT_TMP_DIR/app.plist"'); expect(script).not.toContain("ENT_TMP_BASE"); expect(script).not.toContain("ENT_TMP_RUNTIME"); expect(script).not.toContain("base.plist"); expect(script).not.toContain("runtime.plist"); }); it("does not allocate entitlement temp files for help output", () => { const tempRoot = makeTempDir("openclaw-codesign-help-"); const result = runCodesign(["--help"], tempRoot); expect(result.status).toBe(0); expect(result.stdout).toContain("Usage: scripts/codesign-mac-app.sh"); expect(entitlementTemps(tempRoot)).toEqual([]); }); it("does not allocate entitlement temp files before app validation", () => { const tempRoot = makeTempDir("openclaw-codesign-missing-"); const missingApp = path.join(tempRoot, "Missing.app"); const result = runCodesign([missingApp], tempRoot); expect(result.status).toBe(1); expect(result.stderr).toContain("App bundle not found"); expect(entitlementTemps(tempRoot)).toEqual([]); }); it("cleans entitlement temp files when signing fails", () => { const tempRoot = makeTempDir("openclaw-codesign-fail-"); const app = path.join(tempRoot, "Fake.app"); mkdirSync(path.join(app, "Contents", "MacOS"), { recursive: true }); const result = spawnSync("bash", [scriptPath, app], { cwd: process.cwd(), encoding: "utf8", env: { ...process.env, ALLOW_ADHOC_SIGNING: "1", TMPDIR: tempRoot, }, }); expect(result.status).not.toBe(0); expect(entitlementTemps(tempRoot)).toEqual([]); }); it("passes generated app entitlements to signing commands and cleans them", () => { const tempRoot = makeTempDir("openclaw-codesign-success-"); const app = path.join(tempRoot, "Fake.app"); const binDir = path.join(tempRoot, "bin"); const captureDir = path.join(tempRoot, "capture"); const logPath = path.join(captureDir, "codesign.log"); mkdirSync(path.join(app, "Contents", "MacOS"), { recursive: true }); mkdirSync(binDir); mkdirSync(captureDir); writeFileSync(path.join(app, "Contents", "MacOS", "OpenClaw"), "#!/bin/sh\n"); installFakeCodesign(binDir); const result = spawnSync("bash", [scriptPath, app], { cwd: process.cwd(), encoding: "utf8", env: { ...process.env, CODESIGN_CAPTURE_DIR: captureDir, CODESIGN_LOG: logPath, PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`, SIGN_IDENTITY: "-", SKIP_TEAM_ID_CHECK: "1", TMPDIR: tempRoot, }, }); expect(result.status).toBe(0); expect(result.stdout).toContain(`Codesign complete for ${app}`); const signLines = readFileSync(logPath, "utf8").trim().split("\n"); expect(signLines).toHaveLength(2); expect(signLines[0]).toContain(`${path.join(app, "Contents", "MacOS", "OpenClaw")}\t`); expect(signLines[1]).toContain(`${app}\t`); for (const line of signLines) { const [, , entitlementPath, copiedEntitlementsPath] = line.split("\t"); const copiedEntitlements = readFileSync(copiedEntitlementsPath, "utf8"); expect(entitlementPath).toContain("openclaw-entitlements"); expect(existsSync(entitlementPath)).toBe(false); expect(copiedEntitlements).toContain("com.apple.security.automation.apple-events"); expect(copiedEntitlements).toContain("com.apple.security.device.camera"); } expect(entitlementTemps(tempRoot)).toEqual([]); }); });