Files
adolf/test/scripts/package-mac-dist.test.ts
alvis bedb527145
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Vendor OpenClaw source as Adolf fork baseline
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
2026-07-05 09:36:54 +00:00

425 lines
15 KiB
TypeScript

// Package Mac Dist tests cover package mac dist script behavior.
import { spawnSync } from "node:child_process";
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, 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/package-mac-dist.sh";
function makePlist(): string {
const dir = mkdtempSync(path.join(tmpdir(), "openclaw-dist-plist-"));
tempDirs.push(dir);
const plist = path.join(dir, "Info.plist");
writeFileSync(
plist,
[
'<?xml version="1.0" encoding="UTF-8"?>',
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
'<plist version="1.0">',
"<dict>",
"<key>CFBundleShortVersionString</key>",
"<string>1.2.3</string>",
"</dict>",
"</plist>",
"",
].join("\n"),
"utf8",
);
return plist;
}
function runHelper(script: string) {
return spawnSync("bash", ["-lc", script], {
cwd: process.cwd(),
encoding: "utf8",
});
}
function getPackageManagerHelperBlock(): string {
const script = readFileSync(scriptPath, "utf8");
const start = script.indexOf("DIST_PNPM_CMD=()");
const end = script.indexOf("ensure_sparkle_build_deps()");
expect(start).toBeGreaterThanOrEqual(0);
expect(end).toBeGreaterThan(start);
return script.slice(start, end);
}
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});
describe("package-mac-dist plist validation", () => {
it("fails closed for required Info.plist reads", () => {
const script = readFileSync(scriptPath, "utf8");
const readBlock = script.slice(
script.indexOf("VERSION="),
script.indexOf('ZIP="$ROOT_DIR/dist/OpenClaw-$VERSION.zip"'),
);
expect(script).toContain('source "$ROOT_DIR/scripts/lib/plistbuddy.sh"');
expect(readBlock).toContain(
'VERSION="$(plist_print_required "$APP/Contents/Info.plist" CFBundleShortVersionString)"',
);
expect(readBlock).toContain(
'BUNDLE_VERSION="$(plist_print_required "$APP/Contents/Info.plist" CFBundleVersion)"',
);
expect(readBlock).toContain(
'ACTUAL_BUNDLE_ID="$(plist_print_required "$APP/Contents/Info.plist" CFBundleIdentifier)"',
);
expect(readBlock).toContain(
'ACTUAL_FEED_URL="$(plist_print_required "$APP/Contents/Info.plist" SUFeedURL)"',
);
expect(readBlock).not.toContain("PlistBuddy");
expect(readBlock).not.toContain("|| echo");
});
it("requires the release bundle id to match the configured bundle id", () => {
const script = readFileSync(scriptPath, "utf8");
const releaseBlock = script.slice(
script.indexOf('if [[ "$BUILD_CONFIG" == "release" ]]'),
script.indexOf('if [[ "$NOTARIZE" == "1" ]]'),
);
expect(releaseBlock).toContain('if [[ "$ACTUAL_BUNDLE_ID" != "$BUNDLE_ID" ]]');
expect(releaseBlock).toContain("expected '$BUNDLE_ID'");
expect(releaseBlock).not.toContain("*.debug");
});
it("does not mask canonical Sparkle build failures for release packaging", () => {
const script = readFileSync(scriptPath, "utf8");
expect(script).toContain("ensure_sparkle_build_deps()");
expect(script).toContain(
"run_dist_pnpm install --frozen-lockfile --config.node-linker=hoisted >&2",
);
expect(script).toContain(
'(cd "$ROOT_DIR" && node --import tsx "$ROOT_DIR/scripts/sparkle-build.ts" canonical-build "$1")',
);
expect(script).toContain('if [[ "$SPARKLE_BUILD_DEPS_RETRIED" == "1" ]]');
expect(script).toContain("require_canonical_sparkle_build()");
expect(script).toContain(
'CANONICAL_APP_BUILD="$(require_canonical_sparkle_build "$APP_VERSION_INPUT")"',
);
expect(script).toContain('CANONICAL_APP_BUILD="$(require_canonical_sparkle_build "$VERSION")"');
expect(script).not.toContain(
'canonical_sparkle_build "$APP_VERSION_INPUT" 2>/dev/null || true',
);
expect(script).not.toContain('canonical_sparkle_build "$VERSION" 2>/dev/null || true');
});
it("checks Swift before Sparkle metadata or dependency bootstrap work", () => {
const script = readFileSync(scriptPath, "utf8");
const swiftIndex = script.indexOf("\nrequire_swift_toolchain\n");
const versionIndex = script.indexOf('if [[ -z "$APP_VERSION_INPUT" ]]');
const appBuildIndex = script.indexOf(
'if [[ -z "${APP_BUILD:-}" && "$BUILD_CONFIG" == "release" ]]',
);
const packageAppIndex = script.indexOf('"$ROOT_DIR/scripts/package-mac-app.sh"');
const preSwiftBlock = script.slice(0, swiftIndex);
expect(script).toContain('source "$ROOT_DIR/scripts/lib/swift-toolchain.sh"');
expect(swiftIndex).toBeGreaterThanOrEqual(0);
expect(versionIndex).toBeGreaterThan(swiftIndex);
expect(appBuildIndex).toBeGreaterThan(versionIndex);
expect(packageAppIndex).toBeGreaterThan(appBuildIndex);
expect(preSwiftBlock).not.toContain("node -p");
});
it("fails on old Swift before reading package metadata", () => {
const toolsDir = mkdtempSync(path.join(tmpdir(), "openclaw-dist-swift-tools-"));
tempDirs.push(toolsDir);
writeFileSync(
path.join(toolsDir, "swift"),
[
"#!/usr/bin/env bash",
"echo 'swift-driver version: 1.115.1 Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)'",
"",
].join("\n"),
"utf8",
);
chmodSync(path.join(toolsDir, "swift"), 0o755);
writeFileSync(
path.join(toolsDir, "node"),
[
"#!/usr/bin/env bash",
"echo 'node should not run before Swift preflight' >&2",
"exit 42",
"",
].join("\n"),
"utf8",
);
chmodSync(path.join(toolsDir, "node"), 0o755);
const result = runHelper(`
set -euo pipefail
PATH=${JSON.stringify(`${toolsDir}:/usr/bin:/bin`)}
BUILD_CONFIG=release bash ${scriptPath}
`);
expect(result.status).toBe(1);
expect(result.stderr).toContain("OpenClaw macOS app packaging requires Swift tools 6.2+");
expect(result.stderr).toContain("Current Swift is 6.0");
expect(result.stderr).not.toContain("node should not run before Swift preflight");
});
it("prefers repo Corepack pnpm over a global pnpm shim", () => {
const helperBlock = getPackageManagerHelperBlock();
const tempRoot = mkdtempSync(path.join(tmpdir(), "openclaw-dist-pnpm-root-"));
const outerRoot = mkdtempSync(path.join(tmpdir(), "openclaw-dist-pnpm-outer-"));
const toolsDir = mkdtempSync(path.join(tmpdir(), "openclaw-dist-pnpm-tools-"));
const logPath = path.join(tempRoot, "pnpm.log");
tempDirs.push(tempRoot, outerRoot, toolsDir);
writeFileSync(
path.join(tempRoot, "package.json"),
'{\n "packageManager": "pnpm@11.2.2+sha512.test"\n}\n',
);
writeFileSync(
path.join(outerRoot, "package.json"),
'{\n "packageManager": "pnpm@11.8.0+sha512.test"\n}\n',
);
writeFileSync(
path.join(toolsDir, "pnpm"),
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'printf "global|%s|%s\\n" "$PWD" "$*" >> "$OPENCLAW_TEST_LOG"',
'if [[ "${1:-}" == "--version" ]]; then echo "11.8.0"; fi',
"",
].join("\n"),
"utf8",
);
writeFileSync(
path.join(toolsDir, "corepack"),
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'printf "corepack|%s|%s\\n" "$PWD" "$*" >> "$OPENCLAW_TEST_LOG"',
'if [[ "${1:-}" == "pnpm" && "${2:-}" == "--version" ]]; then',
' if grep -q "pnpm@11.2.2" package.json 2>/dev/null; then echo "11.2.2"; else echo "11.8.0"; fi',
"fi",
"",
].join("\n"),
"utf8",
);
chmodSync(path.join(toolsDir, "pnpm"), 0o755);
chmodSync(path.join(toolsDir, "corepack"), 0o755);
const result = runHelper(`
set -euo pipefail
ROOT_DIR=${JSON.stringify(tempRoot)}
OPENCLAW_TEST_LOG=${JSON.stringify(logPath)}
export OPENCLAW_TEST_LOG
PATH=${JSON.stringify(`${toolsDir}:/usr/bin:/bin`)}
cd ${JSON.stringify(outerRoot)}
${helperBlock}
run_dist_pnpm --version
`);
expect(result.status).toBe(0);
expect(result.stdout).toBe("11.2.2\n");
expect(readFileSync(logPath, "utf8").trim().split("\n")).toEqual([
`corepack|${tempRoot}|pnpm --version`,
`corepack|${tempRoot}|pnpm --version`,
]);
});
it("keeps dependency bootstrap output out of captured Sparkle build values", () => {
const script = readFileSync(scriptPath, "utf8");
const helpers = script.slice(
script.indexOf("DIST_PNPM_CMD=()"),
script.indexOf("correction_build_from_exact_tag()"),
);
const dir = mkdtempSync(path.join(tmpdir(), "openclaw-dist-sparkle-"));
tempDirs.push(dir);
const tools = path.join(dir, "tools");
const marker = path.join(dir, "installed");
const fakeNode = path.join(tools, "node");
const fakePnpm = path.join(tools, "pnpm");
mkdirSync(tools, { recursive: true });
writeFileSync(
fakeNode,
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'if [[ "$PWD" != "$OPENCLAW_ROOT" ]]; then',
' echo "node ran outside repo root: $PWD" >&2',
" exit 1",
"fi",
'if [[ ! -f "$OPENCLAW_MARKER" ]]; then',
' echo "Cannot find package tsx" >&2',
" exit 1",
"fi",
'echo "ExperimentalWarning: tsx loader changed" >&2',
"echo 2026060200",
"",
].join("\n"),
"utf8",
);
chmodSync(fakeNode, 0o755);
writeFileSync(
fakePnpm,
[
"#!/usr/bin/env bash",
"set -euo pipefail",
"echo 'Already up to date'",
'touch "$OPENCLAW_MARKER"',
"",
].join("\n"),
"utf8",
);
chmodSync(fakePnpm, 0o755);
const result = runHelper(`
set -euo pipefail
ROOT_DIR=${JSON.stringify(process.cwd())}
OPENCLAW_ROOT=${JSON.stringify(process.cwd())}
OPENCLAW_MARKER=${JSON.stringify(marker)}
PATH=${JSON.stringify(tools)}:/usr/bin:/bin
export OPENCLAW_MARKER OPENCLAW_ROOT PATH
${helpers}
require_canonical_sparkle_build 2026.6.2
`);
expect(result.status).toBe(0);
expect(result.stdout).toBe("2026060200\n");
expect(result.stderr).toContain("Ensuring deps for Sparkle build metadata");
expect(result.stderr).toContain("Already up to date");
expect(result.stderr).toContain("ExperimentalWarning: tsx loader changed");
});
it("stops when dependency bootstrap fails during Sparkle build retry", () => {
const script = readFileSync(scriptPath, "utf8");
const helpers = script.slice(
script.indexOf("DIST_PNPM_CMD=()"),
script.indexOf("correction_build_from_exact_tag()"),
);
const dir = mkdtempSync(path.join(tmpdir(), "openclaw-dist-sparkle-"));
tempDirs.push(dir);
const tools = path.join(dir, "tools");
const marker = path.join(dir, "installed");
const fakeNode = path.join(tools, "node");
const fakePnpm = path.join(tools, "pnpm");
mkdirSync(tools, { recursive: true });
writeFileSync(
fakeNode,
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'if [[ "$PWD" != "$OPENCLAW_ROOT" ]]; then',
' echo "node ran outside repo root: $PWD" >&2',
" exit 1",
"fi",
'if [[ ! -f "$OPENCLAW_MARKER" ]]; then',
' echo "Cannot find package tsx" >&2',
" exit 1",
"fi",
'echo "node reran after failed install" >&2',
"echo 2026060200",
"",
].join("\n"),
"utf8",
);
chmodSync(fakeNode, 0o755);
writeFileSync(
fakePnpm,
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'touch "$OPENCLAW_MARKER"',
'echo "pnpm failed" >&2',
"exit 42",
"",
].join("\n"),
"utf8",
);
chmodSync(fakePnpm, 0o755);
const result = runHelper(`
set -euo pipefail
ROOT_DIR=${JSON.stringify(process.cwd())}
OPENCLAW_ROOT=${JSON.stringify(process.cwd())}
OPENCLAW_MARKER=${JSON.stringify(marker)}
PATH=${JSON.stringify(tools)}:/usr/bin:/bin
export OPENCLAW_MARKER OPENCLAW_ROOT PATH
${helpers}
require_canonical_sparkle_build 2026.6.2
`);
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr).toContain("pnpm failed");
expect(result.stderr).not.toContain("node reran after failed install");
});
it("cleans the temporary notary zip when notarization exits early", () => {
const script = readFileSync(scriptPath, "utf8");
const notaryBlock = script.slice(
script.indexOf('if [[ "$NOTARIZE" == "1" ]]'),
script.indexOf('if [[ "$SKIP_DMG" != "1" ]]'),
);
expect(script).toContain("cleanup_notary_zip()");
expect(notaryBlock).toContain("NOTARY_ZIP_PENDING_CLEANUP=1");
expect(notaryBlock).toContain("trap cleanup_notary_zip EXIT");
expect(notaryBlock).toContain(
'STAPLE_APP_PATH="$APP" "$ROOT_DIR/scripts/notarize-mac-artifact.sh" "$NOTARY_ZIP"',
);
expect(notaryBlock).toContain('rm -f "$NOTARY_ZIP"');
expect(notaryBlock).toContain("NOTARY_ZIP_PENDING_CLEANUP=0");
expect(notaryBlock).toContain("trap - EXIT");
});
it("fails closed when required dSYM outputs are missing", () => {
const script = readFileSync(scriptPath, "utf8");
const dsymBlock = script.slice(script.indexOf('if [[ "$SKIP_DSYM" != "1" ]]'));
expect(dsymBlock).toContain('for arch in "${DSYM_ARCHS[@]}"');
expect(dsymBlock).toContain('if [[ ! -d "$BUILD_ROOT/$arch" ]]; then');
expect(dsymBlock).toContain('MISSING_DSYM_ARCHS+=("$arch")');
expect(dsymBlock).toContain("Error: dSYM not found for architecture(s):");
expect(dsymBlock).not.toContain('find "$BUILD_ROOT/arm64"');
expect(dsymBlock).not.toContain('find "$BUILD_ROOT/x86_64"');
expect(dsymBlock).toContain("Error: missing DWARF binaries for dSYM merge");
expect(dsymBlock).toContain("Error: dSYM not found");
expect(dsymBlock).toContain("exit 1");
expect(script).toContain('if ! cp -R "$1" "$TMP_DSYM"; then');
expect(dsymBlock).toContain("cleanup_tmp_dsym");
expect(dsymBlock).toContain('copy_dsym_to_tmp "${DSYM_PATHS[0]}"');
expect(dsymBlock).not.toContain('cp -R "${DSYM_PATHS[0]}" "$TMP_DSYM"');
expect(dsymBlock).toContain(
'if ! /usr/bin/lipo -create "${DWARF_INPUTS[@]}" -output "$DWARF_OUT"; then',
);
expect(dsymBlock).toContain('if ! ditto -c -k --keepParent "$TMP_DSYM" "$DSYM_ZIP"; then');
expect(dsymBlock).toContain('rm -rf "$TMP_DSYM"');
expect(dsymBlock).not.toContain("WARN:");
expect(dsymBlock).not.toContain("continuing");
});
it.runIf(process.platform === "darwin")(
"prints required plist keys and fails when a key is missing",
() => {
const plist = makePlist();
const result = runHelper(`
set -euo pipefail
source scripts/lib/plistbuddy.sh
plist_print_required ${JSON.stringify(plist)} CFBundleShortVersionString
plist_print_required ${JSON.stringify(plist)} CFBundleVersion
`);
expect(result.status).toBe(1);
expect(result.stdout).toContain("1.2.3");
expect(result.stderr).toContain("Does Not Exist");
},
);
});