mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
Tooling tests for the PR operation lock, PR git maintenance, CI platform checkout support, E2E temp-state directories, macOS app packaging, run-vitest, and Vitest fork OS diagnostics polled ready files, PID files, and the process table against 2-50 s deadlines, so a fixture process that outlasted the bound on a loaded host failed the test. Waits now await the owned signal: retained child close promises registered at fork, the CI checkout fixture's existing ready() publication watcher, launcher write-before-exit contracts read directly once the launcher has exited, and fixture receipts or stdout readiness where the product owns the child. Escaped-group extinction keeps a signal-bound census, and vitest-fork-os-diagnostics drops out of the timeout-race baseline. The run-vitest conflict with main's #162584/#162613/#162627 keeps main's it.for structure and withinTest(context.signal) and adds this lane's native close promise and readiness gate. Waits needing a host-to-child release channel, Windows census budget-expiry assertions, and an operation-lock rescue that must outlive test abort stay unchanged as follow-ups. No product source, test timeout, product timeout, or assertion meaning changed. Part of the polling audit from #162274. Proof on Blacksmith Testbox: delay probes fail the original bytes and pass these bytes, forced-abort probes reach cleanup, 20 standalone runs per touched file (run-vitest re-proven 20/20 on the resolved head), 3/3 replays of each owning shard, root-test tsgo, type-aware lint, base-aware timeout-race ratchet; Codex autoreview and ClawSweeper clean.
2121 lines
82 KiB
TypeScript
2121 lines
82 KiB
TypeScript
import { spawn, spawnSync } from "node:child_process";
|
|
import {
|
|
chmodSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
realpathSync,
|
|
statSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { availableParallelism } from "node:os";
|
|
import path from "node:path";
|
|
import { createInterface } from "node:readline";
|
|
import { minimatch } from "minimatch";
|
|
import * as tar from "tar";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { awaitGateBeforeSettlement, createDeferred, withinTest } from "../helpers/promise.js";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
import { createMacScriptTest } from "./mac-script-fixture.test-support.js";
|
|
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
const scriptPath = "scripts/package-mac-app.sh";
|
|
const swiftScriptPath = "scripts/lib/mac-swift-build.sh";
|
|
|
|
describe.skipIf(process.platform === "win32")("cloud-worker app packaging identity", () => {
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
const initialization = script.slice(
|
|
script.indexOf('CLOUD_WORKER_HOST="${OPENCLAW_MAC_CLOUD_WORKER_HOST:-0}"'),
|
|
script.indexOf("PKG_VERSION="),
|
|
);
|
|
|
|
function resolveVariant(overrides: NodeJS.ProcessEnv) {
|
|
return spawnSync(
|
|
"/bin/bash",
|
|
[
|
|
"-c",
|
|
`set -euo pipefail\nROOT_DIR="$1"\n${initialization}\nprintf '%s\\n' "$APP_DESTINATION" "$BUNDLE_ID"`,
|
|
"package-identity",
|
|
process.cwd(),
|
|
],
|
|
{ encoding: "utf8", env: { PATH: "/usr/bin:/bin", ...overrides } },
|
|
);
|
|
}
|
|
|
|
it.each([false, true])(
|
|
"keeps the cloud variant %s separate from the ordinary output",
|
|
(cloud) => {
|
|
const result = resolveVariant({ OPENCLAW_MAC_CLOUD_WORKER_HOST: cloud ? "1" : "0" });
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout.trim().split("\n")).toEqual([
|
|
path.join(process.cwd(), "dist", cloud ? "OpenClawCloudWorker.app" : "OpenClaw.app"),
|
|
cloud ? "ai.openclaw.cloud-worker" : "ai.openclaw.mac.debug",
|
|
]);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{ BUNDLE_ID: "ai.openclaw.mac" },
|
|
{ ALLOW_ADHOC_SIGNING: "1" },
|
|
{ SIGN_IDENTITY: "-" },
|
|
{ DISABLE_LIBRARY_VALIDATION: "1" },
|
|
{ SKIP_TEAM_ID_CHECK: "1" },
|
|
{ OPENCLAW_PACKAGE_APP_ROOT: path.join(process.cwd(), "dist/OpenClaw.app") },
|
|
])(
|
|
"rejects a cloud build that weakens its identity or replaces the ordinary app: %j",
|
|
(override) => {
|
|
const result = resolveVariant({ OPENCLAW_MAC_CLOUD_WORKER_HOST: "1", ...override });
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("ERROR:");
|
|
},
|
|
);
|
|
|
|
it.runIf(process.platform === "darwin").each([false, true])(
|
|
"stamps cloud capability only in its dedicated bundle: %s",
|
|
(cloud) => {
|
|
const app = tempDirs.make("openclaw-cloud-bundle-");
|
|
mkdirSync(path.join(app, "Contents"));
|
|
writeFileSync(
|
|
path.join(app, "Contents/Info.plist"),
|
|
readFileSync("apps/macos/Sources/OpenClaw/Resources/Info.plist"),
|
|
);
|
|
const stamp = script.slice(
|
|
script.indexOf(
|
|
'plist_set_string_required "$APP_ROOT/Contents/Info.plist" CFBundleIdentifier',
|
|
),
|
|
script.indexOf(
|
|
'plist_set_string_required "$APP_ROOT/Contents/Info.plist" CFBundleShortVersionString',
|
|
),
|
|
);
|
|
const result = spawnSync(
|
|
"/bin/bash",
|
|
[
|
|
"-c",
|
|
`set -euo pipefail\nsource "$1"\nAPP_ROOT="$2"\nBUNDLE_ID="$3"\nCLOUD_WORKER_HOST="$4"\n${stamp}\n/usr/bin/plutil -convert json -o - "$APP_ROOT/Contents/Info.plist"`,
|
|
"package-stamp",
|
|
path.resolve("scripts/lib/plistbuddy.sh"),
|
|
app,
|
|
cloud ? "ai.openclaw.cloud-worker" : "ai.openclaw.mac.debug",
|
|
cloud ? "1" : "0",
|
|
],
|
|
{ encoding: "utf8" },
|
|
);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const plist = JSON.parse(result.stdout);
|
|
expect(plist.CFBundleIdentifier).toBe(
|
|
cloud ? "ai.openclaw.cloud-worker" : "ai.openclaw.mac.debug",
|
|
);
|
|
expect(plist.CFBundleName).toBe(cloud ? "OpenClaw Cloud Worker" : "OpenClaw");
|
|
expect(plist.OpenClawCloudWorkerHostVersion).toBe(cloud ? 1 : undefined);
|
|
expect(plist.CFBundleURLTypes).toEqual(cloud ? undefined : expect.any(Array));
|
|
},
|
|
);
|
|
});
|
|
|
|
describe.skipIf(process.platform === "win32" || availableParallelism() < 2)(
|
|
"parallel macOS Swift build ownership",
|
|
() => {
|
|
const test = createMacScriptTest();
|
|
test.for(["success", "failure", "wrong-source", "cancel", "cleanup-failure"])(
|
|
"joins architecture workers and preserves assembly safety: %s",
|
|
{ timeout: 15_000 },
|
|
async (mode, { mac, onTestFinished, signal }) => {
|
|
const root = mac.createTempDir("openclaw-swift-parallel-");
|
|
const stage = path.join(root, "stage");
|
|
const scripts = path.join(root, "scripts/lib");
|
|
mkdirSync(scripts, { recursive: true });
|
|
mkdirSync(stage);
|
|
const mountParent = path.join(root, "Darwin private temp");
|
|
mkdirSync(mountParent);
|
|
const getconf = path.join(root, "getconf");
|
|
writeFileSync(
|
|
getconf,
|
|
`#!/bin/bash
|
|
[[ "$*" == DARWIN_USER_TEMP_DIR ]] || exit 2
|
|
printf '%s\\n' '${mountParent.replaceAll("'", "'\\''")}'
|
|
`,
|
|
);
|
|
chmodSync(getconf, 0o755);
|
|
const commit = "b".repeat(40);
|
|
writeFileSync(
|
|
path.join(scripts, "mac-swift-build.sh"),
|
|
`#!/bin/bash
|
|
set -euo pipefail
|
|
exec "${process.execPath}" "${path.join(root, "worker.mjs")}" "$@"
|
|
`,
|
|
);
|
|
writeFileSync(
|
|
path.join(root, "worker.mjs"),
|
|
`
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { spawn } from 'node:child_process';
|
|
const [operation, root, arch, config, jobs, commit, skip, work, mount] = process.argv.slice(2);
|
|
const mode = ${JSON.stringify(mode)};
|
|
const event = (value) => fs.appendFileSync(path.join(root, 'events'), value + '\\n');
|
|
if (!mount || path.dirname(path.dirname(mount)) !== fs.realpathSync(path.join(root, 'Darwin private temp'))) {
|
|
throw new Error('snapshot mount must use the OS temp location, independently of work or TMPDIR');
|
|
}
|
|
if (operation === 'cleanup') {
|
|
if (fs.readFileSync(path.join(root, 'mount-' + arch), 'utf8') !== mount) throw new Error('cleanup lost the build mount');
|
|
event('cleanup:' + arch);
|
|
if (mode === 'cleanup-failure') process.exit(55);
|
|
fs.rmdirSync(mount);
|
|
process.exit(0);
|
|
}
|
|
fs.mkdirSync(mount);
|
|
fs.writeFileSync(path.join(root, 'mount-' + arch), mount);
|
|
event('build:' + arch + ':' + jobs);
|
|
const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { stdio: 'ignore' });
|
|
fs.writeFileSync(path.join(root, 'pid-' + arch), String(child.pid));
|
|
const exited = new Promise(resolve => child.on('exit', resolve));
|
|
process.on('SIGTERM', async () => { child.kill(); await exited; event('stopped:' + arch); process.exit(143); });
|
|
const released = new Promise(resolve => process.once('SIGUSR2', resolve));
|
|
console.log('ready:' + arch + ':' + process.pid);
|
|
await released;
|
|
event('barrier:' + arch);
|
|
if (mode === 'cancel' || (mode === 'failure' && arch === 'arm64')) await new Promise(() => {});
|
|
child.kill(); await exited;
|
|
if (mode === 'failure') process.exit(42);
|
|
fs.writeFileSync(path.join(work, 'peekaboo-commit'), mode === 'wrong-source' ? 'wrong' : commit);
|
|
`,
|
|
);
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
const cleanup = script.slice(
|
|
script.indexOf("cleanup_package_build() {"),
|
|
script.indexOf("PNPM_CMD=()"),
|
|
);
|
|
const build = script.slice(
|
|
script.indexOf('echo "🔨 Building $PRODUCT'),
|
|
script.indexOf('BIN_PRIMARY="$(bin_for_arch'),
|
|
);
|
|
// Exercise the real parent wait/signal/cleanup flow; only the heavy graph is a fixture.
|
|
const launcher = `set -euo pipefail
|
|
ROOT_DIR=${JSON.stringify(root)}
|
|
APP_STAGE_DIR=${JSON.stringify(stage)}
|
|
SWIFT_BUILD_RESULTS=""
|
|
SWIFT_BUILD_PID=""
|
|
PRODUCT=OpenClaw
|
|
BUILD_CONFIG=release
|
|
PEEKABOO_LOCKED_SOURCE_COMMIT=${commit}
|
|
SKIP_MLX_TTS=0
|
|
BUILD_ARCHS=(arm64 x86_64)
|
|
node() { exec "${process.execPath}" ${JSON.stringify(path.resolve("scripts/build-mac-swift.mts"))} "\${@:2}"; }
|
|
${cleanup}
|
|
${build}
|
|
touch "$ROOT_DIR/assembled"
|
|
`;
|
|
const child = spawn("/bin/bash", ["-c", launcher], {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
env: {
|
|
...process.env,
|
|
PATH: `${root}:${process.env.PATH}`,
|
|
TMPDIR: path.join(root, "unavailable"),
|
|
},
|
|
});
|
|
let stderr = "";
|
|
child.stderr.on("data", (chunk: Buffer) => {
|
|
stderr += chunk.toString();
|
|
});
|
|
const closed = mac.lifetime.track(
|
|
new Promise<number | null>((resolve, reject) => {
|
|
child.once("error", reject);
|
|
child.once("close", resolve);
|
|
}),
|
|
);
|
|
const output = createInterface({ input: child.stdout });
|
|
const workers = new Map<string, number>();
|
|
const ready = createDeferred();
|
|
output.on("line", (line) => {
|
|
const match = /^ready:(arm64|x86_64):(\d+)$/.exec(line);
|
|
if (match) {
|
|
workers.set(match[1]!, Number(match[2]));
|
|
if (workers.size === 2) {
|
|
ready.resolve();
|
|
}
|
|
}
|
|
});
|
|
onTestFinished(async () => {
|
|
try {
|
|
if (child.exitCode === null && child.signalCode === null) {
|
|
child.kill("SIGTERM");
|
|
}
|
|
await closed;
|
|
} finally {
|
|
output.close();
|
|
}
|
|
});
|
|
await withinTest(
|
|
awaitGateBeforeSettlement(ready.promise, closed, "architecture barrier did not open"),
|
|
signal,
|
|
);
|
|
// Each worker installs its release handler before publishing its PID.
|
|
for (const pid of workers.values()) {
|
|
process.kill(pid, "SIGUSR2");
|
|
}
|
|
if (mode === "cancel") {
|
|
child.kill("SIGTERM");
|
|
}
|
|
const code = await withinTest(closed, signal);
|
|
expect(code, stderr).toBe(
|
|
mode === "success" ? 0 : mode === "cancel" ? 143 : mode === "cleanup-failure" ? 2 : 1,
|
|
);
|
|
expect(existsSync(path.join(root, "assembled"))).toBe(mode === "success");
|
|
expect(existsSync(stage)).toBe(mode === "cleanup-failure");
|
|
expect(readdirSync(mountParent)).toHaveLength(mode === "cleanup-failure" ? 1 : 0);
|
|
const events = readFileSync(path.join(root, "events"), "utf8").trim().split("\n");
|
|
expect(events.filter((event) => event.startsWith("cleanup:")).toSorted()).toEqual([
|
|
"cleanup:arm64",
|
|
"cleanup:x86_64",
|
|
]);
|
|
for (const arch of ["arm64", "x86_64"]) {
|
|
const mount = readFileSync(path.join(root, `mount-${arch}`), "utf8");
|
|
expect(existsSync(mount)).toBe(mode === "cleanup-failure");
|
|
if (mode === "cleanup-failure") {
|
|
expect(statSync(path.dirname(mount)).mode & 0o777).toBe(0o700);
|
|
}
|
|
const pid = Number(readFileSync(path.join(root, `pid-${arch}`), "utf8"));
|
|
expect(() => process.kill(pid, 0)).toThrow();
|
|
expect(
|
|
existsSync(path.join(root, "apps/macos/.build", `.openclaw-package-${arch}.lock`)),
|
|
).toBe(mode === "cleanup-failure");
|
|
}
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
describe("packaged worker freshness", () => {
|
|
it.skipIf(process.platform === "win32")(
|
|
"keeps private app staging out of package contents and removes it after use",
|
|
async () => {
|
|
const root = tempDirs.make("openclaw-package-stage-");
|
|
const dist = path.join(root, "dist");
|
|
const output = tempDirs.make("openclaw-package-stage-output-");
|
|
const previousApp = path.join(dist, "OpenClaw.app/Contents/MacOS/OpenClaw");
|
|
const { files, packageManager, version } = JSON.parse(
|
|
readFileSync("package.json", "utf8"),
|
|
) as {
|
|
files: string[];
|
|
packageManager: string;
|
|
version: string;
|
|
};
|
|
mkdirSync(path.dirname(previousApp), { recursive: true });
|
|
writeFileSync(previousApp, "previous signed app\n");
|
|
writeFileSync(path.join(dist, "entry.js"), "export {};\n");
|
|
writeFileSync(
|
|
path.join(root, "package.json"),
|
|
JSON.stringify({ name: "openclaw", version, packageManager, files }),
|
|
);
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
const allocationStart = script.indexOf("# pnpm build owns the Control UI");
|
|
const allocationEnd = script.indexOf('echo "🔨 Building $PRODUCT', allocationStart);
|
|
expect(allocationStart).toBeGreaterThanOrEqual(0);
|
|
expect(allocationEnd).toBeGreaterThan(allocationStart);
|
|
const allocated = spawnSync(
|
|
"/bin/bash",
|
|
[
|
|
"-c",
|
|
`set -euo pipefail
|
|
ROOT_DIR="$1"
|
|
APP_DESTINATION="$ROOT_DIR/dist/OpenClaw.app"
|
|
APP_BUNDLE_NAME=OpenClaw.app
|
|
${script.slice(allocationStart, allocationEnd)}
|
|
printf '%s' "$APP_STAGE_DIR"
|
|
`,
|
|
"package-stage",
|
|
root,
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
env: { HOME: root, PATH: "/usr/bin:/bin", TMPDIR: path.join(root, "unavailable") },
|
|
},
|
|
);
|
|
expect(allocated.status, allocated.stderr).toBe(0);
|
|
const stage = allocated.stdout;
|
|
const swiftResults = path.join(stage, "swift-builds");
|
|
mkdirSync(path.join(swiftResults, "arm64"), { recursive: true });
|
|
writeFileSync(path.join(swiftResults, "arm64/peekaboo-commit"), "private stage canary\n");
|
|
writeFileSync(path.join(swiftResults, "cleanup-complete"), "verified\n");
|
|
mkdirSync(path.join(stage, "OpenClaw.app/Contents/MacOS"), { recursive: true });
|
|
writeFileSync(path.join(stage, "OpenClaw.app/Contents/MacOS/OpenClaw"), "candidate app\n");
|
|
try {
|
|
expect(statSync(stage).dev).toBe(statSync(dist).dev);
|
|
expect(statSync(stage).mode & 0o777).toBe(0o700);
|
|
const packed = spawnSync(
|
|
"npm",
|
|
["pack", "--silent", "--ignore-scripts", "--offline", "--pack-destination", output],
|
|
{ cwd: root, encoding: "utf8", env: { HOME: root, PATH: process.env.PATH } },
|
|
);
|
|
expect(packed.status, packed.stderr).toBe(0);
|
|
const entries: string[] = [];
|
|
await tar.t({
|
|
file: path.join(output, `openclaw-${version}.tgz`),
|
|
onentry: (entry) => {
|
|
if (entry.type !== "Directory") {
|
|
entries.push(entry.path);
|
|
}
|
|
},
|
|
});
|
|
expect(entries.toSorted()).toEqual(["package/dist/entry.js", "package/package.json"]);
|
|
} finally {
|
|
const cleanup = script.slice(
|
|
script.indexOf("cleanup_package_build() {"),
|
|
script.indexOf("PNPM_CMD=()"),
|
|
);
|
|
const cleaned = spawnSync(
|
|
"/bin/bash",
|
|
[
|
|
"-c",
|
|
`set -euo pipefail
|
|
APP_STAGE_DIR="$1"
|
|
SWIFT_BUILD_RESULTS="$APP_STAGE_DIR/swift-builds"
|
|
SWIFT_BUILD_PID=""
|
|
${cleanup}
|
|
`,
|
|
"package-stage-cleanup",
|
|
stage,
|
|
],
|
|
{ encoding: "utf8", env: { HOME: root, PATH: "/usr/bin:/bin" } },
|
|
);
|
|
expect(cleaned.status, cleaned.stderr).toBe(0);
|
|
expect(existsSync(stage)).toBe(false);
|
|
expect(readFileSync(previousApp, "utf8")).toBe("previous signed app\n");
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each(["dist/OpenClaw-proof.app", "dist/.openclaw-package.fixture/OpenClaw.app"])(
|
|
"bounds expanded package exclusions to the app root %s",
|
|
(app) => {
|
|
const manifest = JSON.parse(readFileSync("package.json", "utf8")) as { files: string[] };
|
|
const exclusions = manifest.files
|
|
.filter((entry) => entry.startsWith("!"))
|
|
.map((entry) => entry.slice(1));
|
|
const entries = [app, `${app}/Contents`, `${app}/Contents/MacOS/OpenClaw`, "dist/entry.js"];
|
|
// npm 12 expands files globs into individual ignore rules. Exclude the app
|
|
// directory, which also excludes its contents, not every payload file separately.
|
|
const matches = entries.filter((entry) =>
|
|
exclusions.some((pattern) => minimatch(entry, pattern, { dot: true })),
|
|
);
|
|
expect(matches).toEqual([app]);
|
|
},
|
|
);
|
|
|
|
it("rebuilds dirty JavaScript even when the old SKIP_TSC shortcut is requested", () => {
|
|
const root = tempDirs.make("openclaw-package-worker-freshness-");
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
const start = script.indexOf('if [[ "${SKIP_TSC:-0}"');
|
|
const end = script.indexOf('node - "$ROOT_DIR/dist/build-info.json"', start);
|
|
const result = spawnSync(
|
|
process.platform === "win32" ? "bash" : "/bin/bash",
|
|
[
|
|
"-c",
|
|
`
|
|
set -euo pipefail
|
|
run_pnpm() { printf '%s\\n' 'fresh dirty worker' > "$HOME/worker.js"; }
|
|
${script.slice(start, end)}
|
|
`,
|
|
],
|
|
{ encoding: "utf8", env: { HOME: root, PATH: "/usr/bin:/bin", SKIP_TSC: "1" } },
|
|
);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(existsSync(path.join(root, "worker.js"))).toBe(true);
|
|
});
|
|
});
|
|
|
|
function makePlist(): string {
|
|
const dir = tempDirs.make("openclaw-plistbuddy-");
|
|
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>CFBundleIdentifier</key>",
|
|
"<string>old.bundle</string>",
|
|
"</dict>",
|
|
"</plist>",
|
|
"",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
return plist;
|
|
}
|
|
|
|
function runHelper(script: string, shell = process.platform === "win32" ? "bash" : "/bin/bash") {
|
|
// Login/logout hooks can replace the helper's exit status on headless hosts.
|
|
return spawnSync(shell, ["-c", script], {
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
});
|
|
}
|
|
|
|
function scriptBlock(startMarker: string, endMarker: string, file = scriptPath): string {
|
|
const script = readFileSync(file, "utf8");
|
|
const start = script.indexOf(startMarker);
|
|
const end = script.indexOf(endMarker, start);
|
|
expect(start).toBeGreaterThanOrEqual(0);
|
|
expect(end).toBeGreaterThan(start);
|
|
return script.slice(start, end);
|
|
}
|
|
|
|
function getPackageManagerHelperBlock(): string {
|
|
return scriptBlock("PNPM_CMD=()", "merge_framework_machos()");
|
|
}
|
|
|
|
function getMergeFrameworkMachOsBlock(): string {
|
|
return scriptBlock(
|
|
"merge_framework_machos()",
|
|
'PEEKABOO_SOURCE_COMMIT="$(resolve_peekaboo_source_commit)"',
|
|
);
|
|
}
|
|
|
|
function runSwiftToolchainHarness(options: {
|
|
swiftVersion: string;
|
|
selectedDeveloperDir: "command-line-tools" | "xcode";
|
|
xcodeVersion?: string;
|
|
xcodebuildFailure?: string;
|
|
}) {
|
|
const root = tempDirs.make("openclaw-package-swift-root-");
|
|
const toolsDir = path.join(root, "tools");
|
|
const commandLineToolsDir = path.join(root, "Library", "Developer", "CommandLineTools");
|
|
const xcodeDeveloperDir = path.join(root, "Applications", "Xcode.app", "Contents", "Developer");
|
|
const selectedDeveloperDir =
|
|
options.selectedDeveloperDir === "xcode" ? xcodeDeveloperDir : commandLineToolsDir;
|
|
|
|
mkdirSync(toolsDir, { recursive: true });
|
|
mkdirSync(commandLineToolsDir, { recursive: true });
|
|
const xcodebuild = path.join(xcodeDeveloperDir, "usr", "bin", "xcodebuild");
|
|
mkdirSync(path.dirname(xcodebuild), { recursive: true });
|
|
writeFileSync(
|
|
xcodebuild,
|
|
`#!/bin/bash
|
|
[[ "$*" == "-version" ]] || exit 2
|
|
${
|
|
options.xcodebuildFailure
|
|
? `printf '%s\\n' ${JSON.stringify(options.xcodebuildFailure)} >&2; exit 1`
|
|
: `echo ${JSON.stringify(`Xcode ${options.xcodeVersion ?? "26.4"}`)}`
|
|
}
|
|
`,
|
|
);
|
|
chmodSync(xcodebuild, 0o755);
|
|
writeFileSync(
|
|
path.join(toolsDir, "xcrun"),
|
|
[
|
|
"#!/bin/bash",
|
|
'[[ "${1:-}" == "xcodebuild" && "${2:-}" == "-version" ]] || exit 2',
|
|
'developer_dir="${DEVELOPER_DIR:-$MOCK_SELECTED_DEVELOPER_DIR}"',
|
|
'xcodebuild="$developer_dir/usr/bin/xcodebuild"',
|
|
'if [[ ! -x "$xcodebuild" ]]; then',
|
|
' echo "xcrun: error: unable to find utility xcodebuild" >&2',
|
|
" exit 1",
|
|
"fi",
|
|
'exec "$xcodebuild" "${@:2}"',
|
|
"",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
writeFileSync(
|
|
path.join(toolsDir, "swift"),
|
|
[
|
|
"#!/bin/bash",
|
|
`echo 'swift-driver version: 1.120.0 Apple Swift version ${options.swiftVersion} (swiftlang-${options.swiftVersion} clang-1700.0.13.5)'`,
|
|
"",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
for (const tool of ["xcrun", "swift"]) {
|
|
chmodSync(path.join(toolsDir, tool), 0o755);
|
|
}
|
|
|
|
return runHelper(`
|
|
set -euo pipefail
|
|
PATH=${JSON.stringify(`${toolsDir}:/usr/bin:/bin`)}
|
|
export MOCK_SELECTED_DEVELOPER_DIR=${JSON.stringify(selectedDeveloperDir)}
|
|
unset DEVELOPER_DIR
|
|
${readFileSync("scripts/lib/swift-toolchain.sh", "utf8")}
|
|
require_swift_toolchain
|
|
`);
|
|
}
|
|
|
|
function getSparkleBuildHelperBlock(): string {
|
|
return scriptBlock(
|
|
"sparkle_canonical_build_from_version()",
|
|
'source "$ROOT_DIR/scripts/lib/mac-swift-build.sh"',
|
|
);
|
|
}
|
|
|
|
function getPeekabooSourceCommitHelperBlock(): string {
|
|
return scriptBlock(
|
|
"resolve_peekaboo_source_commit() {",
|
|
"sparkle_canonical_build_from_version()",
|
|
);
|
|
}
|
|
|
|
function runPeekabooSourceCommitHarness(packageResolved: string, expectedRevision?: string) {
|
|
const root = tempDirs.make("openclaw-package-peekaboo-source-");
|
|
const resolvedFile = path.join(root, "apps", "macos", "Package.resolved");
|
|
mkdirSync(path.dirname(resolvedFile), { recursive: true });
|
|
writeFileSync(resolvedFile, packageResolved, "utf8");
|
|
|
|
return runHelper(`
|
|
set -euo pipefail
|
|
ROOT_DIR=${JSON.stringify(root)}
|
|
${expectedRevision ? `export OPENCLAW_EXPECTED_PEEKABOO_SOURCE_COMMIT=${JSON.stringify(expectedRevision)}` : "unset OPENCLAW_EXPECTED_PEEKABOO_SOURCE_COMMIT"}
|
|
${getPeekabooSourceCommitHelperBlock()}
|
|
resolve_peekaboo_source_commit
|
|
`);
|
|
}
|
|
|
|
function getSourceProvenanceStampBlock(): string {
|
|
return scriptBlock(
|
|
'plist_set_string_required "$APP_ROOT/Contents/Info.plist" OpenClawBuildTimestamp',
|
|
'plist_set_or_add_string "$APP_ROOT/Contents/Info.plist" SUFeedURL',
|
|
);
|
|
}
|
|
|
|
function runSourceProvenanceStampHarness(corruptKey?: string) {
|
|
const openClawCommit = "a".repeat(40);
|
|
const peekabooCommit = "b".repeat(40);
|
|
const corruptCommit = "c".repeat(40);
|
|
const result = runHelper(`
|
|
set -euo pipefail
|
|
stamped_openclaw=
|
|
stamped_peekaboo=
|
|
plist_set_string_required() {
|
|
case "$2" in
|
|
OpenClawGitCommit) stamped_openclaw="$3" ;;
|
|
PeekabooSourceCommit) stamped_peekaboo="$3" ;;
|
|
esac
|
|
}
|
|
plist_print_required() {
|
|
local value
|
|
case "$2" in
|
|
OpenClawGitCommit) value="$stamped_openclaw" ;;
|
|
PeekabooSourceCommit) value="$stamped_peekaboo" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
if [[ "$2" == ${JSON.stringify(corruptKey ?? "")} ]]; then
|
|
value=${JSON.stringify(corruptCommit)}
|
|
fi
|
|
printf '%s' "$value"
|
|
}
|
|
APP_ROOT=/tmp/OpenClaw.app
|
|
ROOT_DIR=/unused
|
|
node() { echo fixture-build-id; }
|
|
plist_set_or_add_string() { :; }
|
|
BUILD_TS=2026-08-13T00:00:00.000Z
|
|
BUILD_GIT_COMMIT=${JSON.stringify(openClawCommit)}
|
|
PEEKABOO_SOURCE_COMMIT=${JSON.stringify(peekabooCommit)}
|
|
BUILD_CONFIG=release
|
|
${getSourceProvenanceStampBlock()}
|
|
printf '%s\n%s\n' "$stamped_openclaw" "$stamped_peekaboo"
|
|
`);
|
|
|
|
return { result, openClawCommit, peekabooCommit };
|
|
}
|
|
|
|
function getMLXTTSHelperBuildBlock(): string {
|
|
return scriptBlock(
|
|
"helper_build_path_for_arch() {",
|
|
"sparkle_framework_for_arch()",
|
|
swiftScriptPath,
|
|
);
|
|
}
|
|
|
|
function getSwiftPackageResolutionBlock(): string {
|
|
const block = scriptBlock(
|
|
"run_with_locked_swift_packages()",
|
|
"build_swift_architecture() {",
|
|
swiftScriptPath,
|
|
);
|
|
// The shared EXIT cleanup also needs the packager preamble's unallocated app stage.
|
|
return `${block}\nBUILD_PATH="$ROOT_DIR/build"\nSWIFT_WORK_ROOT="$ROOT_DIR/work"\nmkdir -p "$SWIFT_WORK_ROOT"\n`;
|
|
}
|
|
|
|
function getCompiledPeekabooHelperBlock(): string {
|
|
return scriptBlock("compiled_peekaboo_commit() {", "swiftpm_resource_sources()", swiftScriptPath);
|
|
}
|
|
|
|
function fixtureGit(cwd: string, ...args: string[]) {
|
|
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
expect(result.status, result.stderr).toBe(0);
|
|
return result.stdout.trim();
|
|
}
|
|
|
|
function initializeRepository(cwd: string) {
|
|
for (const args of [
|
|
["init", "-q"],
|
|
["config", "user.name", "Fixture"],
|
|
["config", "user.email", "fixture@example.invalid"],
|
|
["add", "."],
|
|
["commit", "-qm", "fixture"],
|
|
]) {
|
|
fixtureGit(cwd, ...args);
|
|
}
|
|
return fixtureGit(cwd, "rev-parse", "HEAD");
|
|
}
|
|
|
|
function runRealCompiledPeekabooHarness(
|
|
mutation:
|
|
| "assume-unchanged"
|
|
| "corrupt-object"
|
|
| "dirty-gitlink"
|
|
| "export-subst"
|
|
| "gitlink-sibling"
|
|
| "ignored"
|
|
| "nested-gitlink"
|
|
| "none"
|
|
| "replacement-ref",
|
|
expectedOverride?: string,
|
|
) {
|
|
const root = tempDirs.make(`openclaw-compiled-peekaboo-real-${mutation}-`);
|
|
const buildPath = path.join(root, "build");
|
|
const checkout = path.join(buildPath, "checkouts", "Peekaboo");
|
|
const sourcePath = path.join(checkout, "Core", "Sources", "Fixture.swift");
|
|
mkdirSync(path.dirname(sourcePath), { recursive: true });
|
|
writeFileSync(path.join(checkout, "Package.swift"), "// swift-tools-version: 6.2\n", "utf8");
|
|
writeFileSync(sourcePath, 'let fixture = "$Format:%H$"\n', "utf8");
|
|
writeFileSync(path.join(checkout, ".gitattributes"), "Core/Sources/Fixture.swift export-subst\n");
|
|
let head = initializeRepository(checkout);
|
|
|
|
if (
|
|
mutation === "dirty-gitlink" ||
|
|
mutation === "gitlink-sibling" ||
|
|
mutation === "nested-gitlink"
|
|
) {
|
|
const gitlinkPath = mutation === "gitlink-sibling" ? "Vendor" : "Dependencies/Vendor";
|
|
const gitlinkCheckout = path.join(checkout, gitlinkPath);
|
|
mkdirSync(gitlinkCheckout, { recursive: true });
|
|
writeFileSync(path.join(gitlinkCheckout, "README.md"), "initialized submodule\n");
|
|
const gitlinkHead = initializeRepository(gitlinkCheckout);
|
|
writeFileSync(
|
|
path.join(checkout, ".gitmodules"),
|
|
`[submodule "fixture"]\n\tpath = ${gitlinkPath}\n\turl = https://example.invalid/vendor.git\n`,
|
|
"utf8",
|
|
);
|
|
fixtureGit(checkout, "add", ".gitmodules");
|
|
fixtureGit(
|
|
checkout,
|
|
"update-index",
|
|
"--add",
|
|
"--cacheinfo",
|
|
`160000,${gitlinkHead},${gitlinkPath}`,
|
|
);
|
|
fixtureGit(checkout, "commit", "-qm", "gitlink");
|
|
head = fixtureGit(checkout, "rev-parse", "HEAD");
|
|
}
|
|
|
|
if (mutation === "assume-unchanged") {
|
|
fixtureGit(checkout, "update-index", "--assume-unchanged", path.relative(checkout, sourcePath));
|
|
writeFileSync(sourcePath, "let fixture = 2\n", "utf8");
|
|
} else if (mutation === "corrupt-object") {
|
|
const treeId = fixtureGit(checkout, "rev-parse", "HEAD^{tree}");
|
|
const objectPath = path.join(checkout, ".git", "objects", treeId.slice(0, 2), treeId.slice(2));
|
|
chmodSync(objectPath, 0o644);
|
|
writeFileSync(objectPath, "corrupt object\n");
|
|
} else if (mutation === "dirty-gitlink") {
|
|
writeFileSync(path.join(checkout, "Dependencies", "Vendor", "README.md"), "dirty submodule\n");
|
|
} else if (mutation === "export-subst") {
|
|
writeFileSync(sourcePath, `let fixture = "${head}"\n`, "utf8");
|
|
} else if (mutation === "gitlink-sibling") {
|
|
const sibling = path.join(checkout, "Core", "Vendor", "Injected.swift");
|
|
mkdirSync(path.dirname(sibling), { recursive: true });
|
|
writeFileSync(sibling, "let injected = true\n", "utf8");
|
|
} else if (mutation === "ignored") {
|
|
writeFileSync(path.join(checkout, ".git", "info", "exclude"), "Hidden.swift\n", "utf8");
|
|
writeFileSync(path.join(checkout, "Hidden.swift"), "let hidden = true\n", "utf8");
|
|
} else if (mutation === "replacement-ref") {
|
|
const approvedHead = head;
|
|
writeFileSync(sourcePath, 'let fixture = "replacement"\n', "utf8");
|
|
fixtureGit(checkout, "commit", "-qam", "replacement");
|
|
const replacementHead = fixtureGit(checkout, "rev-parse", "HEAD");
|
|
fixtureGit(checkout, "replace", approvedHead, replacementHead);
|
|
fixtureGit(checkout, "checkout", "-q", "--detach", approvedHead);
|
|
writeFileSync(sourcePath, 'let fixture = "replacement"\n', "utf8");
|
|
head = approvedHead;
|
|
}
|
|
|
|
return runHelper(`
|
|
set -euo pipefail
|
|
${getCompiledPeekabooHelperBlock()}
|
|
compiled_peekaboo_commit ${JSON.stringify(buildPath)} ${JSON.stringify(expectedOverride ?? head)}
|
|
`);
|
|
}
|
|
|
|
function getStopPackagedAppBlock(): string {
|
|
return scriptBlock("running_packaged_app_pids()", 'if [[ -n "${SIGN_IDENTITY:-}" ]]');
|
|
}
|
|
|
|
function getSwiftCompatibilityBlock(): string {
|
|
return scriptBlock(
|
|
'echo "📦 Copying Swift 6.2 compatibility libraries"',
|
|
'echo "🖼 Compiling app icon"',
|
|
);
|
|
}
|
|
|
|
function getSwiftPMResourceBundleBlock(): string {
|
|
return scriptBlock('echo "📦 Copying SwiftPM resource bundles"', "running_packaged_app_pids()");
|
|
}
|
|
|
|
function getSwiftPMResourcePatchBlock(): string {
|
|
return scriptBlock(
|
|
"swiftpm_resource_sources()",
|
|
"cleanup_swift_architecture() {",
|
|
swiftScriptPath,
|
|
);
|
|
}
|
|
|
|
const swiftPMResourceBundles = [
|
|
"GRDB_GRDB.bundle",
|
|
"OpenClaw_OpenClaw.bundle",
|
|
"OpenClawKit_OpenClawKit.bundle",
|
|
"OpenClawKit_OpenClawChatUI.bundle",
|
|
"KeyboardShortcuts_KeyboardShortcuts.bundle",
|
|
"SwiftMath_SwiftMath.bundle",
|
|
] as const;
|
|
|
|
const mlxTTSResourceFiles = [
|
|
"mlx-swift_Cmlx.bundle/Contents/Info.plist",
|
|
"mlx-swift_Cmlx.bundle/Contents/Resources/default.metallib",
|
|
"swift-crypto_Crypto.bundle/Contents/Info.plist",
|
|
"swift-crypto_Crypto.bundle/Contents/Resources/PrivacyInfo.xcprivacy",
|
|
"swift-transformers_Hub.bundle/Contents/Info.plist",
|
|
"swift-transformers_Hub.bundle/Contents/Resources/gpt2_tokenizer_config.json",
|
|
"swift-transformers_Hub.bundle/Contents/Resources/t5_tokenizer_config.json",
|
|
] as const;
|
|
|
|
function runSwiftPMResourceBundleHarness(
|
|
options: { missingBundle?: string; missingMetallib?: boolean; skipMLXTTS?: boolean } = {},
|
|
) {
|
|
const root = tempDirs.make("openclaw-package-resources-root-");
|
|
const buildRoot = path.join(root, "build");
|
|
const helperBuildRoot = path.join(root, "helper build");
|
|
const appRoot = path.join(root, "OpenClaw.app");
|
|
const buildProducts = path.join(buildRoot, "arm64", "debug");
|
|
const helperBuildProducts = path.join(helperBuildRoot, "arm64", "out", "Products", "Debug");
|
|
|
|
mkdirSync(path.join(appRoot, "Contents", "Resources"), { recursive: true });
|
|
for (const bundle of swiftPMResourceBundles) {
|
|
if (bundle === options.missingBundle) {
|
|
continue;
|
|
}
|
|
const source = path.join(buildProducts, bundle);
|
|
mkdirSync(source, { recursive: true });
|
|
writeFileSync(path.join(source, "marker"), bundle, "utf8");
|
|
}
|
|
for (const file of mlxTTSResourceFiles) {
|
|
if (
|
|
file.startsWith(`${options.missingBundle}/`) ||
|
|
(options.missingMetallib && file.endsWith("/default.metallib"))
|
|
) {
|
|
continue;
|
|
}
|
|
const source = path.join(helperBuildProducts, file);
|
|
mkdirSync(path.dirname(source), { recursive: true });
|
|
writeFileSync(source, file, "utf8");
|
|
}
|
|
|
|
const result = runHelper(`
|
|
set -euo pipefail
|
|
BUILD_ROOT=${JSON.stringify(buildRoot)}
|
|
MLX_TTS_HELPER_BUILD_ROOT=${JSON.stringify(helperBuildRoot)}
|
|
APP_ROOT=${JSON.stringify(appRoot)}
|
|
PRIMARY_ARCH=arm64
|
|
BUILD_CONFIG=debug
|
|
SKIP_MLX_TTS=${options.skipMLXTTS ? "1" : "0"}
|
|
build_path_for_arch() {
|
|
echo "$BUILD_ROOT/$1"
|
|
}
|
|
helper_build_path_for_arch() {
|
|
echo "$MLX_TTS_HELPER_BUILD_ROOT/$1"
|
|
}
|
|
helper_products_for_arch() {
|
|
[[ "$#" -eq 1 && "$1" == "$PRIMARY_ARCH" ]] || return 1
|
|
printf '%s\\n' ${JSON.stringify(helperBuildProducts)}
|
|
}
|
|
${getSwiftPMResourceBundleBlock()}
|
|
`);
|
|
|
|
return { appRoot, result };
|
|
}
|
|
|
|
function runSwiftPMResourcePatchHarness(failRestore = false) {
|
|
const root = tempDirs.make("openclaw-package-resource-patch-");
|
|
const workRoot = tempDirs.make("openclaw-resource-backups-");
|
|
const backupRoot = path.join(workRoot, "resource-backups");
|
|
const buildPath = path.join(root, "build");
|
|
const checkoutRoot = path.join(buildPath, "checkouts");
|
|
const keyboardShortcuts = path.join(
|
|
checkoutRoot,
|
|
"KeyboardShortcuts/Sources/KeyboardShortcuts/Utilities.swift",
|
|
);
|
|
const swiftMathFont = path.join(
|
|
checkoutRoot,
|
|
"SwiftMath/Sources/SwiftMath/MathBundle/MathFont.swift",
|
|
);
|
|
const swiftMathLegacyFont = path.join(
|
|
checkoutRoot,
|
|
"SwiftMath/Sources/SwiftMath/MathRender/MTFont.swift",
|
|
);
|
|
const fixtures = new Map([
|
|
[
|
|
keyboardShortcuts,
|
|
[
|
|
"import Foundation",
|
|
"extension String {",
|
|
" var localized: String {",
|
|
" NSLocalizedString(self, bundle: .module, comment: self)",
|
|
" }",
|
|
"}",
|
|
"",
|
|
"extension Data {",
|
|
"}",
|
|
"",
|
|
].join("\n"),
|
|
],
|
|
[
|
|
swiftMathFont,
|
|
[
|
|
"import Foundation",
|
|
"#if os(macOS)",
|
|
"import AppKit",
|
|
"#endif",
|
|
"",
|
|
"/// Now available for everyone to use",
|
|
'let first = Bundle.module.url(forResource: "mathFonts", withExtension: "bundle")',
|
|
'let second = Bundle.module.url(forResource: "mathFonts", withExtension: "bundle")',
|
|
"",
|
|
].join("\n"),
|
|
],
|
|
[
|
|
swiftMathLegacyFont,
|
|
'let font = Bundle.module.url(forResource: "mathFonts", withExtension: "bundle")\n',
|
|
],
|
|
]);
|
|
|
|
for (const [file, contents] of fixtures) {
|
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
writeFileSync(file, contents, "utf8");
|
|
}
|
|
|
|
const result = runHelper(`
|
|
set -euo pipefail
|
|
SWIFT_WORK_ROOT=${JSON.stringify(workRoot)}
|
|
BUILD_PATH=${JSON.stringify(buildPath)}
|
|
${getSwiftPMResourcePatchBlock()}
|
|
patch_swiftpm_resource_lookups ${JSON.stringify(buildPath)}
|
|
grep -q keyboardShortcutsPackagedResources ${JSON.stringify(keyboardShortcuts)}
|
|
test "$(grep -c swiftMathPackagedResources ${JSON.stringify(swiftMathFont)})" -eq 3
|
|
grep -q swiftMathPackagedResources ${JSON.stringify(swiftMathLegacyFont)}
|
|
${failRestore ? "mv() { printf 'restore failed\\n' >&2; return 13; }" : ""}
|
|
cleanup_status=0
|
|
restore_swiftpm_resource_sources || cleanup_status=$?
|
|
exit "$cleanup_status"
|
|
`);
|
|
|
|
return { backupRoot, fixtures, result };
|
|
}
|
|
|
|
function runStopPackagedAppHarness() {
|
|
const root = tempDirs.make("openclaw-package-stop-root-");
|
|
const toolsDir = tempDirs.make("openclaw-package-stop-tools-");
|
|
|
|
const appRoot = path.join(root, "dist", "OpenClaw.app");
|
|
const appBinary = path.join(appRoot, "Contents", "MacOS", "OpenClaw");
|
|
const lsofPath = path.join(toolsDir, "lsof");
|
|
const pgrepPath = path.join(toolsDir, "pgrep");
|
|
const sleepPath = path.join(toolsDir, "sleep");
|
|
|
|
writeFileSync(
|
|
lsofPath,
|
|
["#!/bin/bash", `printf 'n%s\\n' ${JSON.stringify(appBinary)}`].join("\n"),
|
|
"utf8",
|
|
);
|
|
writeFileSync(pgrepPath, "#!/bin/bash\nprintf '123\\n'\n", "utf8");
|
|
writeFileSync(sleepPath, "#!/bin/bash\nexit 0\n", "utf8");
|
|
chmodSync(lsofPath, 0o755);
|
|
chmodSync(pgrepPath, 0o755);
|
|
chmodSync(sleepPath, 0o755);
|
|
|
|
return runHelper(`
|
|
set -euo pipefail
|
|
APP_DESTINATION=${JSON.stringify(appRoot)}
|
|
PRODUCT=OpenClaw
|
|
PATH=${JSON.stringify(`${toolsDir}:/usr/bin:/bin`)}
|
|
kill() {
|
|
return 0
|
|
}
|
|
${getStopPackagedAppBlock()}
|
|
stop_packaged_app_if_running
|
|
`);
|
|
}
|
|
|
|
function runSwiftCompatibilityHarness() {
|
|
const root = tempDirs.make("openclaw-package-swift-root-");
|
|
const toolsDir = tempDirs.make("openclaw-package-swift-tools-");
|
|
const developerDir = path.join(root, "Xcode.app", "Contents", "Developer");
|
|
const appRoot = path.join(root, "OpenClaw.app");
|
|
const xcodeSelectPath = path.join(toolsDir, "xcode-select");
|
|
|
|
writeFileSync(
|
|
xcodeSelectPath,
|
|
["#!/bin/bash", `printf '%s\\n' ${JSON.stringify(developerDir)}`].join("\n"),
|
|
"utf8",
|
|
);
|
|
chmodSync(xcodeSelectPath, 0o755);
|
|
|
|
return runHelper(`
|
|
set -euo pipefail
|
|
APP_ROOT=${JSON.stringify(appRoot)}
|
|
BUILD_CONFIG=release
|
|
PATH=${JSON.stringify(`${toolsDir}:/usr/bin:/bin`)}
|
|
mkdir -p "$APP_ROOT/Contents/Frameworks"
|
|
${getSwiftCompatibilityBlock()}
|
|
`);
|
|
}
|
|
|
|
function runSwiftPackageResolutionHarness() {
|
|
const root = tempDirs.make("openclaw-swift-resolve-root-");
|
|
const toolsDir = tempDirs.make("openclaw-swift-resolve-tools-");
|
|
const resolvedFile = path.join(root, "apps", "macos", "Package.resolved");
|
|
const swiftPath = path.join(toolsDir, "swift");
|
|
|
|
mkdirSync(path.dirname(resolvedFile), { recursive: true });
|
|
writeFileSync(resolvedFile, "locked\n", { encoding: "utf8", flag: "wx" });
|
|
writeFileSync(
|
|
swiftPath,
|
|
["#!/bin/bash", `printf 'changed\\n' > ${JSON.stringify(resolvedFile)}`].join("\n"),
|
|
"utf8",
|
|
);
|
|
chmodSync(swiftPath, 0o755);
|
|
|
|
const result = runHelper(`
|
|
set -euo pipefail
|
|
ROOT_DIR=${JSON.stringify(root)}
|
|
PATH=${JSON.stringify(`${toolsDir}:/usr/bin:/bin`)}
|
|
${getSwiftPackageResolutionBlock()}
|
|
run_with_locked_swift_packages swift package --scratch-path "$ROOT_DIR/apps/macos/.build/arm64" resolve
|
|
`);
|
|
|
|
return { result, resolvedFile };
|
|
}
|
|
|
|
describe("package-mac-app plist stamping", () => {
|
|
it("gates only release packaging on clean matching source and verifies the embedded commit", () => {
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
const sourceCheck = script.indexOf(
|
|
'/bin/bash "$ROOT_DIR/scripts/apple-release-source-check.sh"',
|
|
);
|
|
const build = script.indexOf('node "$ROOT_DIR/scripts/build-mac-swift.mts"');
|
|
const embeddedRead = script.indexOf(
|
|
'plist_print_required "$APP_ROOT/Contents/Info.plist" OpenClawGitCommit',
|
|
);
|
|
const bridgeSourceRead = script.indexOf(
|
|
'plist_print_required "$APP_ROOT/Contents/Info.plist" PeekabooSourceCommit',
|
|
);
|
|
const signing = script.indexOf('"$ROOT_DIR/scripts/codesign-mac-app.sh"');
|
|
const releaseBranch = script.lastIndexOf(
|
|
'if [[ "$BUILD_CONFIG" == "release" ]]; then',
|
|
sourceCheck,
|
|
);
|
|
const releaseBranchEnd = script.indexOf("\nfi", sourceCheck);
|
|
|
|
expect(script).toContain('BUILD_CONFIG="${BUILD_CONFIG:-debug}"');
|
|
expect(sourceCheck).toBeGreaterThan(releaseBranch);
|
|
expect(sourceCheck).toBeLessThan(releaseBranchEnd);
|
|
expect(sourceCheck).toBeLessThan(build);
|
|
expect(script).toContain('--expected-commit "$BUILD_GIT_COMMIT"');
|
|
expect(embeddedRead).toBeGreaterThan(sourceCheck);
|
|
expect(embeddedRead).toBeLessThan(signing);
|
|
expect(bridgeSourceRead).toBeGreaterThan(sourceCheck);
|
|
expect(bridgeSourceRead).toBeLessThan(signing);
|
|
expect(script).toContain(
|
|
'plist_set_string_required "$APP_ROOT/Contents/Info.plist" PeekabooSourceCommit "$PEEKABOO_SOURCE_COMMIT"',
|
|
);
|
|
expect(script).not.toContain(
|
|
'plist_set_string_required "$APP_ROOT/Contents/Info.plist" PeekabooSourceCommit "$BUILD_GIT_COMMIT"',
|
|
);
|
|
});
|
|
|
|
it("stamps and validates independent OpenClaw and Peekaboo source revisions", () => {
|
|
const { result, openClawCommit, peekabooCommit } = runSourceProvenanceStampHarness();
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toBe(`${openClawCommit}\n${peekabooCommit}\n`);
|
|
expect(result.stderr).toBe("");
|
|
});
|
|
|
|
it.each([
|
|
{ key: "OpenClawGitCommit", diagnostic: "Release app OpenClaw source mismatch" },
|
|
{ key: "PeekabooSourceCommit", diagnostic: "Release app Peekaboo source mismatch" },
|
|
])("fails release validation independently for a wrong $key", ({ key, diagnostic }) => {
|
|
const { result } = runSourceProvenanceStampHarness(key);
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(diagnostic);
|
|
});
|
|
|
|
it("requires the locked Peekaboo pin to match the requested elevation release source", () => {
|
|
const requestedRevision = "b".repeat(40);
|
|
const packageResolved = readFileSync("apps/macos/Package.resolved", "utf8");
|
|
const parsed = JSON.parse(packageResolved) as {
|
|
pins: Array<{ identity: string; state: { revision?: string } }>;
|
|
};
|
|
const pinnedRevision = parsed.pins.find((pin) => pin.identity === "peekaboo")?.state.revision;
|
|
expect(pinnedRevision).toMatch(/^[0-9a-f]{40}$/);
|
|
|
|
const matching = runPeekabooSourceCommitHarness(packageResolved, pinnedRevision!);
|
|
expect(matching.status, matching.stderr).toBe(0);
|
|
expect(matching.stdout).toBe(pinnedRevision);
|
|
|
|
const mismatched = runPeekabooSourceCommitHarness(packageResolved, requestedRevision);
|
|
expect(mismatched.status).toBe(1);
|
|
expect(mismatched.stderr).toContain("does not match requested release source");
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
title: "is missing",
|
|
packageResolved: '{"pins":[]}',
|
|
diagnostic: "exactly one 'peekaboo' pin",
|
|
},
|
|
{
|
|
title: "has a malformed revision",
|
|
packageResolved:
|
|
'{"pins":[{"identity":"peekaboo","state":{"revision":"A2FB16764A7D1C53BF696127C287BA32703F614F"}}]}',
|
|
diagnostic: "40-character lowercase hexadecimal revision",
|
|
},
|
|
{
|
|
title: "is invalid JSON",
|
|
packageResolved: "not-json",
|
|
diagnostic: "Could not parse Peekaboo source revision",
|
|
},
|
|
])("fails closed when the Peekaboo package pin $title", ({ packageResolved, diagnostic }) => {
|
|
const result = runPeekabooSourceCommitHarness(packageResolved);
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(diagnostic);
|
|
});
|
|
|
|
it("keeps dependency installation lockfile-safe", () => {
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
const installBlock = script.slice(
|
|
script.indexOf('if [[ "${SKIP_PNPM_INSTALL:-0}" != "1" ]]'),
|
|
script.indexOf('if [[ -z "${APP_BUILD:-}" ]]'),
|
|
);
|
|
|
|
expect(installBlock).toContain("run_pnpm install --frozen-lockfile");
|
|
expect(installBlock).toContain("--config.node-linker=hoisted");
|
|
expect(installBlock).not.toContain("--no-frozen-lockfile");
|
|
});
|
|
|
|
it.each(["primary", "secondary"])(
|
|
"merges framework architectures when %s file output exceeds the pipe buffer",
|
|
(verboseFramework) => {
|
|
const root = tempDirs.make("openclaw-package-framework-pipe-");
|
|
const primary = path.join(root, "primary.framework");
|
|
const secondary = path.join(root, "secondary.framework");
|
|
const destination = path.join(root, "destination.framework");
|
|
for (const framework of [primary, secondary, destination]) {
|
|
mkdirSync(framework);
|
|
writeFileSync(
|
|
path.join(framework, "Fixture"),
|
|
framework === secondary ? "arm64 x86_64\n" : "arm64\n",
|
|
);
|
|
writeFileSync(path.join(framework, "Info.plist"), "resource\n");
|
|
}
|
|
const description = path.join(root, "file-output");
|
|
// A matching first line followed by more than a pipe can buffer makes an
|
|
// early-exiting grep kill the producer, without depending on scheduling.
|
|
writeFileSync(
|
|
description,
|
|
"Mach-O universal binary with 2 architectures\n" + "architecture detail\n".repeat(65536),
|
|
);
|
|
const helper = getMergeFrameworkMachOsBlock()
|
|
.replaceAll("/usr/bin/file", "fixture_file")
|
|
.replaceAll("/usr/bin/lipo", "fixture_lipo");
|
|
const result = runHelper(`
|
|
set -euo pipefail
|
|
fixture_file() {
|
|
if [[ "$1" == */Info.plist ]]; then
|
|
printf 'XML document\\n'
|
|
elif [[ "$1" == */${verboseFramework}.framework/Fixture ]]; then
|
|
cat ${JSON.stringify(description)}
|
|
else
|
|
printf 'Mach-O 64-bit executable\\n'
|
|
fi
|
|
}
|
|
fixture_lipo() {
|
|
case "$1" in
|
|
-info) printf 'Architectures in the fat file: %s are: %s\\n' "$2" "$(cat "$2")" ;;
|
|
-thin)
|
|
[[ "$2" == x86_64 && "$4" == -output ]] || return 2
|
|
printf '%s\\n' "$2" > "$5" ;;
|
|
-create)
|
|
[[ "$4" == -output ]] || return 2
|
|
cat "$2" "$3" > "$5" ;;
|
|
*) return 2 ;;
|
|
esac
|
|
}
|
|
${helper}
|
|
merge_framework_machos ${JSON.stringify(primary)} ${JSON.stringify(destination)} ${JSON.stringify(secondary)}
|
|
`);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(readFileSync(path.join(destination, "Fixture"), "utf8")).toBe("arm64\nx86_64\n");
|
|
expect(readFileSync(path.join(destination, "Info.plist"), "utf8")).toBe("resource\n");
|
|
},
|
|
);
|
|
|
|
it.runIf(process.platform === "darwin").each(["arm64e"] as const)(
|
|
"merges framework Mach-O binaries with %s slices and glob metacharacters in the checkout path",
|
|
(secondaryArchitecture) => {
|
|
const root = tempDirs.make("openclaw-package-framework-[fixture]-");
|
|
const primary = path.join(root, "Primary.framework");
|
|
const secondary = path.join(root, "Secondary.framework");
|
|
const destination = path.join(root, "Destination.framework");
|
|
const relativeBinary = path.join("Versions", "A", "OpenClawFixture");
|
|
|
|
for (const framework of [primary, secondary, destination]) {
|
|
mkdirSync(path.dirname(path.join(framework, relativeBinary)), { recursive: true });
|
|
}
|
|
|
|
// Inert mach_header_64 dylibs (mach-o/loader.h and mach/machine.h).
|
|
// Own the CPU/subtype bytes so host executables and compiler SDKs cannot
|
|
// change this fixture's slice set; real file/lipo still classify and merge it.
|
|
const thinMachO = (cpu: number, subtype: number) => {
|
|
const bytes = Buffer.alloc(32);
|
|
[0xfeedfacf, cpu, subtype, 6, 0, 0, 0, 0].forEach((value, index) =>
|
|
bytes.writeUInt32LE(value, index * 4),
|
|
);
|
|
return bytes;
|
|
};
|
|
const intel = thinMachO(0x01000007, 3);
|
|
const arm = thinMachO(0x0100000c, 2);
|
|
const primaryBinary = path.join(primary, relativeBinary);
|
|
const secondaryBinary = path.join(secondary, relativeBinary);
|
|
const destinationBinary = path.join(destination, relativeBinary);
|
|
const armBinary = path.join(root, "arm-slice");
|
|
writeFileSync(primaryBinary, intel);
|
|
writeFileSync(armBinary, arm);
|
|
const universal = spawnSync(
|
|
"/usr/bin/lipo",
|
|
["-create", primaryBinary, armBinary, "-output", secondaryBinary],
|
|
{ encoding: "utf8" },
|
|
);
|
|
expect(universal.status, universal.stderr).toBe(0);
|
|
writeFileSync(destinationBinary, intel);
|
|
|
|
const result = runHelper(`
|
|
set -euo pipefail
|
|
${getMergeFrameworkMachOsBlock()}
|
|
merge_framework_machos ${JSON.stringify(primary)} ${JSON.stringify(destination)} ${JSON.stringify(secondary)}
|
|
/usr/bin/lipo -archs ${JSON.stringify(destinationBinary)}
|
|
`);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout.trim().split(/\s+/u).toSorted()).toEqual(
|
|
["x86_64", secondaryArchitecture].toSorted(),
|
|
);
|
|
for (const [arch, bytes] of [
|
|
["x86_64", intel],
|
|
[secondaryArchitecture, arm],
|
|
] as const) {
|
|
const extracted = path.join(root, `merged-${arch}`);
|
|
const slice = spawnSync(
|
|
"/usr/bin/lipo",
|
|
["-thin", arch, destinationBinary, "-output", extracted],
|
|
{ encoding: "utf8" },
|
|
);
|
|
expect(slice.status, slice.stderr).toBe(0);
|
|
expect(readFileSync(extracted)).toEqual(bytes);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("builds and locates the MLX helper with SwiftBuild without a legacy output alias", () => {
|
|
const arch = "arm64";
|
|
const tempRoot = tempDirs.make("openclaw-package-mlx-metal-");
|
|
const metalPath = path.join(tempRoot, "metal");
|
|
const invocationPath = path.join(tempRoot, "swift-args");
|
|
const helperBuildRoot = path.join(tempRoot, "build");
|
|
const helperBuildProducts = path.join(helperBuildRoot, arch, "out", "Products", "Release");
|
|
mkdirSync(helperBuildProducts, { recursive: true });
|
|
writeFileSync(path.join(helperBuildProducts, "openclaw-mlx-tts"), arch);
|
|
writeFileSync(metalPath, "#!/bin/sh\nexit 1\n");
|
|
chmodSync(metalPath, 0o755);
|
|
|
|
const result = runHelper(`
|
|
set -euo pipefail
|
|
PATH=/usr/bin:/bin
|
|
xcrun() {
|
|
case "$*" in
|
|
"--find swift") printf '%s\\n' ${JSON.stringify(path.join(tempRoot, "swift"))} ;;
|
|
"metal --version") return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
swift() {
|
|
printf '%s\\n' "$@" >> ${JSON.stringify(invocationPath)}
|
|
printf '\\n' >> ${JSON.stringify(invocationPath)}
|
|
for argument in "$@"; do
|
|
if [[ "$argument" == "--show-bin-path" ]]; then
|
|
printf '%s\\n' ${JSON.stringify(helperBuildProducts)}
|
|
fi
|
|
done
|
|
}
|
|
MLX_TTS_HELPER_ROOT=${JSON.stringify(path.join(tempRoot, "helper"))}
|
|
MLX_TTS_HELPER_BUILD_ROOT=${JSON.stringify(helperBuildRoot)}
|
|
MLX_TTS_HELPER_PRODUCT=openclaw-mlx-tts
|
|
BUILD_CONFIG=release
|
|
SWIFT_BUILD_JOBS=2
|
|
SWIFT_BUILD_RESULTS=${JSON.stringify(tempRoot)}
|
|
mkdir -p "$SWIFT_BUILD_RESULTS/${arch}"
|
|
${getMLXTTSHelperBuildBlock()}
|
|
build_mlx_tts_helper ${arch}
|
|
build_mlx_tts_helper ${arch} --show-bin-path > "$SWIFT_BUILD_RESULTS/${arch}/helper-products"
|
|
swift() { echo unexpected-build >&2; return 99; }
|
|
cat "$(helper_bin_for_arch ${arch})"
|
|
`);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toBe(arch);
|
|
const buildArgs = [
|
|
"build",
|
|
"--build-system",
|
|
"swiftbuild",
|
|
"--package-path",
|
|
path.join(tempRoot, "helper"),
|
|
"-c",
|
|
"release",
|
|
"--product",
|
|
"openclaw-mlx-tts",
|
|
"--build-path",
|
|
path.join(helperBuildRoot, arch),
|
|
"--arch",
|
|
arch,
|
|
"--jobs",
|
|
"2",
|
|
];
|
|
const invocations = readFileSync(invocationPath, "utf8")
|
|
.trim()
|
|
.split("\n\n")
|
|
.map((call) => call.split("\n"));
|
|
expect(invocations).toEqual([buildArgs, [...buildArgs, "--show-bin-path"]]);
|
|
});
|
|
|
|
it("skips the MLX TTS helper build and copy when OPENCLAW_SKIP_MLX_TTS=1", () => {
|
|
const script = readFileSync(scriptPath, "utf8") + readFileSync(swiftScriptPath, "utf8");
|
|
|
|
// Both the per-arch build and the bundle copy are gated on the same flag so
|
|
// a skipped build never tries to copy a helper binary that was not built.
|
|
expect(script).toContain(
|
|
'if [[ "$SKIP_MLX_TTS" == "1" ]]; then\n echo "🔇 Skipping $MLX_TTS_HELPER_PRODUCT (OPENCLAW_SKIP_MLX_TTS=1)',
|
|
);
|
|
expect(script).toContain(
|
|
'if [[ "$SKIP_MLX_TTS" == "1" ]]; then\n echo "🔇 Skipping MLX TTS helper copy (OPENCLAW_SKIP_MLX_TTS=1)',
|
|
);
|
|
});
|
|
|
|
it("refuses OPENCLAW_SKIP_MLX_TTS for release builds but allows it for dev builds", () => {
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
|
|
// Run the real guard snippet from the script (not a copy) so the release
|
|
// safety invariant stays coupled to source: release bundles must ship the
|
|
// voice helper, which notarization later verifies.
|
|
const guardStart = script.indexOf('SKIP_MLX_TTS="${OPENCLAW_SKIP_MLX_TTS:-0}"');
|
|
const guardEnd = script.indexOf("BUILD_TS=", guardStart);
|
|
expect(guardStart).toBeGreaterThanOrEqual(0);
|
|
expect(guardEnd).toBeGreaterThan(guardStart);
|
|
const guard = script.slice(guardStart, guardEnd);
|
|
|
|
const released = runHelper(
|
|
`set -euo pipefail\nexport OPENCLAW_SKIP_MLX_TTS=1\nBUILD_CONFIG=release\n${guard}\necho reached-build`,
|
|
);
|
|
expect(released.status).toBe(1);
|
|
expect(released.stderr).toContain("not allowed for release builds");
|
|
expect(released.stdout).not.toContain("reached-build");
|
|
|
|
const dev = runHelper(
|
|
`set -euo pipefail\nexport OPENCLAW_SKIP_MLX_TTS=1\nBUILD_CONFIG=debug\n${guard}\necho reached-build`,
|
|
);
|
|
expect(dev.status, dev.stderr).toBe(0);
|
|
expect(dev.stdout).toContain("reached-build");
|
|
});
|
|
|
|
it("prefers repo Corepack pnpm over a global pnpm shim", () => {
|
|
const helperBlock = getPackageManagerHelperBlock();
|
|
const tempRoot = tempDirs.make("openclaw-package-pnpm-root-");
|
|
const outerRoot = tempDirs.make("openclaw-package-pnpm-outer-");
|
|
const toolsDir = tempDirs.make("openclaw-package-pnpm-tools-");
|
|
const logPath = path.join(tempRoot, "pnpm.log");
|
|
symlinkSync("/bin/bash", path.join(toolsDir, "bash"));
|
|
symlinkSync("/usr/bin/grep", path.join(toolsDir, "grep"));
|
|
|
|
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"),
|
|
[
|
|
"#!/bin/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"),
|
|
[
|
|
"#!/bin/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)}
|
|
cd ${JSON.stringify(outerRoot)}
|
|
${helperBlock}
|
|
run_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("checks the selected Swift toolchain before dependency install work", () => {
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
const installIndex = script.indexOf('if [[ "${SKIP_PNPM_INSTALL:-0}" != "1" ]]');
|
|
const preInstallBlock = script.slice(0, installIndex);
|
|
|
|
expect(script).toContain('source "$ROOT_DIR/scripts/lib/swift-toolchain.sh"');
|
|
expect(preInstallBlock).toContain("\nrequire_swift_toolchain\n");
|
|
});
|
|
|
|
it("fails with an actionable error when Swift tools are too old", () => {
|
|
const result = runSwiftToolchainHarness({
|
|
swiftVersion: "6.0.3",
|
|
selectedDeveloperDir: "xcode",
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("OpenClaw macOS app packaging requires Swift tools 6.3+");
|
|
expect(result.stderr).toContain("Current Swift is 6.0");
|
|
});
|
|
|
|
it("rejects Command Line Tools even when they provide Swift 6.3", () => {
|
|
const result = runSwiftToolchainHarness({
|
|
swiftVersion: "6.3.1",
|
|
selectedDeveloperDir: "command-line-tools",
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("requires a full Xcode developer directory");
|
|
expect(result.stderr).toContain(
|
|
"Command Line Tools do not include the required SwiftUI macro plugins",
|
|
);
|
|
expect(result.stderr).toContain(
|
|
"sudo xcode-select -s /Applications/Xcode.app/Contents/Developer",
|
|
);
|
|
expect(result.stderr).toContain("DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer");
|
|
});
|
|
|
|
it("rejects Xcode 26.3 even when it exposes a Swift 6.3 binary", () => {
|
|
const result = runSwiftToolchainHarness({
|
|
swiftVersion: "6.3.1",
|
|
selectedDeveloperDir: "xcode",
|
|
xcodeVersion: "26.3",
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("OpenClaw macOS app packaging requires Xcode 26.4+");
|
|
expect(result.stderr).toContain("current Xcode is 26.3");
|
|
});
|
|
|
|
it("preserves the native Xcode failure before generic selection guidance", () => {
|
|
const diagnostic = "xcodebuild: error: SDK metadata is unavailable";
|
|
const result = runSwiftToolchainHarness({
|
|
swiftVersion: "6.3.1",
|
|
selectedDeveloperDir: "xcode",
|
|
xcodebuildFailure: diagnostic,
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
const diagnosticIndex = result.stderr.indexOf(diagnostic);
|
|
const guidanceIndex = result.stderr.indexOf(
|
|
"ERROR: OpenClaw macOS app packaging requires a full Xcode developer directory",
|
|
);
|
|
expect(diagnosticIndex).toBeGreaterThanOrEqual(0);
|
|
expect(guidanceIndex).toBeGreaterThan(diagnosticIndex);
|
|
});
|
|
|
|
it("runs Sparkle build metadata derivation from the repository root", () => {
|
|
const helperBlock = getSparkleBuildHelperBlock();
|
|
const tempRoot = tempDirs.make("openclaw-package-sparkle-root-");
|
|
const toolsDir = tempDirs.make("openclaw-package-sparkle-tools-");
|
|
|
|
const nodePath = path.join(toolsDir, "node");
|
|
writeFileSync(
|
|
nodePath,
|
|
[
|
|
"#!/bin/bash",
|
|
"set -euo pipefail",
|
|
'if [[ "$PWD" != "$OPENCLAW_ROOT" ]]; then',
|
|
' echo "node ran outside repo root: $PWD" >&2',
|
|
" exit 1",
|
|
"fi",
|
|
"echo 2026060290",
|
|
"",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
chmodSync(nodePath, 0o755);
|
|
|
|
const result = runHelper(`
|
|
set -euo pipefail
|
|
ROOT_DIR=${JSON.stringify(tempRoot)}
|
|
OPENCLAW_ROOT=${JSON.stringify(tempRoot)}
|
|
PATH=${JSON.stringify(`${toolsDir}:/usr/bin:/bin`)}
|
|
export OPENCLAW_ROOT PATH
|
|
cd /tmp
|
|
${helperBlock}
|
|
sparkle_canonical_build_from_version 2026.6.2
|
|
`);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stdout).toBe("2026060290\n");
|
|
expect(result.stderr).toBe("");
|
|
});
|
|
|
|
it("does not kill unrelated OpenClaw processes during packaging", () => {
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
const stopBlock = script.slice(
|
|
script.indexOf("running_packaged_app_pids()"),
|
|
script.indexOf('echo "🔏 Signing bundle'),
|
|
);
|
|
|
|
expect(script).not.toContain("killall -q OpenClaw");
|
|
expect(stopBlock).toContain('local app_binary="$APP_DESTINATION/Contents/MacOS/OpenClaw"');
|
|
expect(stopBlock).toContain('pgrep -x "$PRODUCT"');
|
|
expect(stopBlock).toContain('grep -Fx "$app_binary"');
|
|
expect(stopBlock).toContain(
|
|
'[[ "$command_line" == "$app_binary" || "$command_line" == "$app_binary "* ]]',
|
|
);
|
|
});
|
|
|
|
it.runIf(process.platform === "darwin").each(["configured", "unset"] as const)(
|
|
"passes an explicit signing identity and honors %s TMPDIR during runtime verification",
|
|
(tempMode) => {
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
const start = script.indexOf('if [[ -n "${SIGN_IDENTITY:-}" ]]');
|
|
expect(start).toBeGreaterThanOrEqual(0);
|
|
const signingBlock = script.slice(start);
|
|
const tempRoot = tempDirs.make("openclaw-package-signing-identity-");
|
|
const scriptsDir = path.join(tempRoot, "scripts");
|
|
const signerPath = path.join(scriptsDir, "codesign-mac-app.sh");
|
|
const appStage = path.join(tempRoot, "stage");
|
|
const appRoot = path.join(appStage, "OpenClaw.app");
|
|
const callerHome = path.join(tempRoot, "caller-home");
|
|
const callerTemp = path.join(tempRoot, "caller temp [*]");
|
|
const eventsPath = path.join(tempRoot, "events");
|
|
const observationsPath = path.join(tempRoot, "worker-scratch.jsonl");
|
|
const identity = "Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)";
|
|
for (const directory of [scriptsDir, appRoot, callerHome, callerTemp]) {
|
|
mkdirSync(directory, { recursive: true });
|
|
}
|
|
writeFileSync(path.join(appRoot, "candidate"), "verified replacement");
|
|
writeFileSync(
|
|
signerPath,
|
|
'#!/bin/bash\nset -euo pipefail\n[[ -d "$1" ]]\nprintf "identity=%s\\n" "${SIGN_IDENTITY-<unset>}"\nprintf "sign\\n" >> "${0%/*}/../events"\n',
|
|
);
|
|
chmodSync(signerPath, 0o755);
|
|
{
|
|
const node = path.join(appRoot, "Contents/Resources/runtime/bin/bun");
|
|
mkdirSync(path.dirname(node), { recursive: true });
|
|
writeFileSync(
|
|
node,
|
|
`#!/bin/bash\nexec '${process.execPath.replaceAll("'", "'\\''")}' "$@"\n`,
|
|
);
|
|
chmodSync(node, 0o755);
|
|
}
|
|
writeFileSync(
|
|
path.join(scriptsDir, "verify-mac-runtime.mjs"),
|
|
`import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
const scratch = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'worker-proof-')));
|
|
try {
|
|
fs.writeFileSync(path.join(scratch, 'created-by-worker'), 'scratch');
|
|
fs.appendFileSync(${JSON.stringify(observationsPath)}, JSON.stringify({ home: process.env.HOME, scratch, callerCanary: process.env.OPENCLAW_TEST_CALLER_CANARY ?? null }) + '\\n');
|
|
fs.appendFileSync(${JSON.stringify(eventsPath)}, 'worker:' + path.basename(process.argv[2]) + '\\n');
|
|
} finally {
|
|
fs.rmSync(scratch, { recursive: true, force: true });
|
|
}
|
|
`,
|
|
);
|
|
|
|
const result = spawnSync(
|
|
"/bin/bash",
|
|
[
|
|
"-c",
|
|
`
|
|
set -euo pipefail
|
|
ROOT_DIR="$1"
|
|
APP_STAGE_DIR="$ROOT_DIR/stage"
|
|
APP_ROOT="$APP_STAGE_DIR/OpenClaw.app"
|
|
APP_DESTINATION="$ROOT_DIR/OpenClaw.app"
|
|
BUILD_ARCHS=(${process.arch === "arm64" ? "arm64" : "x86_64"})
|
|
source "$3"
|
|
stop_packaged_app_if_running() { printf 'stop\\n' >> "$ROOT_DIR/events"; }
|
|
codesign() {
|
|
[[ "$1" == --verify && "$2" == --deep && "$3" == --strict && -d "$4" ]]
|
|
printf 'verify\\n' >> "$ROOT_DIR/events"
|
|
}
|
|
SIGN_IDENTITY="$2"
|
|
export SIGN_IDENTITY
|
|
${signingBlock}
|
|
printf 'published\\n' >> "$ROOT_DIR/events"
|
|
`,
|
|
"package-signing",
|
|
tempRoot,
|
|
identity,
|
|
path.resolve("scripts/lib/mac-app-bundle.sh"),
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
env: {
|
|
HOME: callerHome,
|
|
PATH: "/usr/bin:/bin",
|
|
OPENCLAW_TEST_CALLER_CANARY: "must-not-reach-worker",
|
|
...(tempMode === "configured" ? { TMPDIR: callerTemp } : {}),
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain("Signing bundle with explicit SIGN_IDENTITY");
|
|
expect(result.stdout).toContain(`identity=${identity}`);
|
|
expect(result.stderr).toBe("");
|
|
expect(readFileSync(eventsPath, "utf8").trim().split("\n")).toEqual([
|
|
"sign",
|
|
"verify",
|
|
"worker:runtime",
|
|
"verify",
|
|
"stop",
|
|
"published",
|
|
]);
|
|
expect(readFileSync(path.join(tempRoot, "OpenClaw.app/candidate"), "utf8")).toBe(
|
|
"verified replacement",
|
|
);
|
|
const observations = readFileSync(observationsPath, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => JSON.parse(line) as { home: string; scratch: string; callerCanary: null });
|
|
expect(observations).toHaveLength(1);
|
|
for (const observation of observations) {
|
|
expect(observation.home).toBe(appStage);
|
|
expect(observation.callerCanary).toBeNull();
|
|
expect(path.dirname(observation.scratch)).toBe(
|
|
realpathSync(tempMode === "configured" ? callerTemp : "/tmp"),
|
|
);
|
|
expect(existsSync(observation.scratch)).toBe(false);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("fails when the packaged app survives forced shutdown", () => {
|
|
const result = runStopPackagedAppHarness();
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("ERROR: Packaged OpenClaw bundle did not exit: 123");
|
|
});
|
|
|
|
it("fails release packaging when the Swift compatibility library is missing", () => {
|
|
const result = runSwiftCompatibilityHarness();
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("ERROR: Swift compatibility library not found");
|
|
});
|
|
|
|
it("keeps mac packaging script checks in the macOS CI lane", () => {
|
|
const pkg = JSON.parse(readFileSync("package.json", "utf8")) as {
|
|
scripts?: Record<string, string>;
|
|
};
|
|
const macosCi = [1, 2, 3].map((part) => pkg.scripts?.[`test:macos:ci:${part}`] ?? "").join(" ");
|
|
|
|
expect(macosCi).toContain("src/gateway/worker-environments/workspace-rsync-path.test.ts");
|
|
expect(macosCi).toContain("test/scripts/package-mac-app.test.ts");
|
|
expect(macosCi).toContain("test/scripts/package-mac-dist.test.ts");
|
|
expect(macosCi).toContain("test/scripts/create-dmg.test.ts");
|
|
expect(macosCi).toContain("test/scripts/codesign-mac-app.test.ts");
|
|
expect(macosCi).toContain("test/scripts/notarize-mac-artifact.test.ts");
|
|
expect(macosCi).toContain("test/scripts/mac-elevation-host.test.ts");
|
|
expect(macosCi).toContain("test/scripts/mac-elevation-artifact.test.ts");
|
|
});
|
|
|
|
it("copies complete main and MLX helper SwiftPM bundles into packaged app resources", () => {
|
|
const { appRoot, result } = runSwiftPMResourceBundleHarness();
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe("");
|
|
for (const bundle of swiftPMResourceBundles) {
|
|
expect(
|
|
readFileSync(path.join(appRoot, "Contents", "Resources", bundle, "marker"), "utf8"),
|
|
).toBe(bundle);
|
|
expect(existsSync(path.join(appRoot, bundle))).toBe(false);
|
|
}
|
|
for (const file of mlxTTSResourceFiles) {
|
|
expect(readFileSync(path.join(appRoot, "Contents", "Resources", file), "utf8")).toBe(file);
|
|
expect(existsSync(path.join(appRoot, file))).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("routes dependency resource lookups into signed app resources and restores sources", () => {
|
|
const { backupRoot, fixtures, result } = runSwiftPMResourcePatchHarness();
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe("");
|
|
for (const [file, contents] of fixtures) {
|
|
expect(readFileSync(file, "utf8")).toBe(contents);
|
|
}
|
|
expect(readdirSync(backupRoot)).toEqual([]);
|
|
});
|
|
|
|
it("fails cleanup instead of deleting backups when resource restoration fails", () => {
|
|
const { backupRoot, fixtures, result } = runSwiftPMResourcePatchHarness(true);
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toBe("restore failed\n");
|
|
const backups = readdirSync(backupRoot).map((file) =>
|
|
readFileSync(path.join(backupRoot, file), "utf8"),
|
|
);
|
|
expect(backups).toHaveLength(fixtures.size);
|
|
expect(backups).toEqual(expect.arrayContaining([...fixtures.values()]));
|
|
});
|
|
|
|
it("fails closed when a required SwiftPM resource bundle is missing", () => {
|
|
const missingBundle = "OpenClawKit_OpenClawKit.bundle";
|
|
const { result } = runSwiftPMResourceBundleHarness({ missingBundle });
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("ERROR: Required SwiftPM resource bundle not found at");
|
|
expect(result.stderr).toContain(missingBundle);
|
|
});
|
|
|
|
it("fails closed when the MLX helper compiled shaders are missing", () => {
|
|
const { result } = runSwiftPMResourceBundleHarness({ missingMetallib: true });
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("mlx-swift_Cmlx.bundle/Contents/Resources/default.metallib");
|
|
});
|
|
|
|
it("omits incomplete MLX helper resources when the helper is skipped for a dev build", () => {
|
|
const { appRoot, result } = runSwiftPMResourceBundleHarness({
|
|
skipMLXTTS: true,
|
|
missingMetallib: true,
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
for (const bundle of swiftPMResourceBundles) {
|
|
expect(
|
|
readFileSync(path.join(appRoot, "Contents", "Resources", bundle, "marker"), "utf8"),
|
|
).toBe(bundle);
|
|
}
|
|
for (const file of mlxTTSResourceFiles) {
|
|
expect(existsSync(path.join(appRoot, "Contents", "Resources", file))).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("compiles app localizations into signed resources", () => {
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
|
|
expect(script).toContain(
|
|
'node --import tsx "$ROOT_DIR/scripts/apple-app-i18n.ts" compile-macos',
|
|
);
|
|
expect(script).toContain('--output "$APP_ROOT/Contents/Resources"');
|
|
});
|
|
|
|
it("preserves locked Swift resolution and verifies source around each native build", () => {
|
|
const worker = readFileSync(swiftScriptPath, "utf8");
|
|
const build = worker.indexOf('swift build -c "$BUILD_CONFIG" --jobs');
|
|
expect(worker).toContain('chmod 0400 "$SWIFT_PACKAGE_LOCK_BASELINE"');
|
|
expect(worker).toContain('cmp -s "$resolved_snapshot" "$resolved_file"');
|
|
expect(worker).toContain('cp "$resolved_snapshot" "$resolved_file"');
|
|
expect(worker).toContain("identity in result");
|
|
expect(worker.lastIndexOf("verify_snapshot_swift_lock", build)).toBeGreaterThan(
|
|
worker.indexOf("build_swift_architecture()"),
|
|
);
|
|
expect(worker.indexOf("verify_snapshot_swift_lock", build)).toBeGreaterThan(build);
|
|
expect(worker).toContain(
|
|
'cp "$ROOT_DIR/apps/macos-mlx-tts/Package.resolved" "$MLX_TTS_HELPER_ROOT/Package.resolved"',
|
|
);
|
|
});
|
|
|
|
it("runs no SwiftPM operation before the locked lock-file resolve", () => {
|
|
// Any earlier resolve on a reused scratch path can float pins before the lock guard snapshots.
|
|
const root = tempDirs.make("openclaw-swift-first-resolve-");
|
|
for (const app of ["macos", "macos-mlx-tts"]) {
|
|
mkdirSync(path.join(root, "apps", app), { recursive: true });
|
|
writeFileSync(path.join(root, "apps", app, "Package.swift"), "// fixture\n");
|
|
writeFileSync(path.join(root, "apps", app, "Package.resolved"), "locked\n");
|
|
}
|
|
const invocations = path.join(root, "swift-invocations");
|
|
const result = runHelper(`
|
|
set -euo pipefail
|
|
source ${JSON.stringify(swiftScriptPath)}
|
|
ROOT_DIR=${JSON.stringify(root)}
|
|
BUILD_ROOT="$ROOT_DIR/apps/macos/.build"
|
|
SWIFT_WORK_ROOT="$ROOT_DIR/work"
|
|
PEEKABOO_LOCKED_SOURCE_COMMIT=${JSON.stringify("b".repeat(40))}
|
|
swift() { printf '%s\\n' "$*" >> ${JSON.stringify(invocations)}; }
|
|
create_verified_peekaboo_snapshot() { exit 0; }
|
|
build_swift_architecture arm64
|
|
`);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(readFileSync(invocations, "utf8")).toBe(
|
|
`package --scratch-path ${root}/apps/macos/.build/arm64 resolve --force-resolved-versions\n`,
|
|
);
|
|
});
|
|
|
|
it("names every pin the edited Peekaboo resolution moved away from the committed lock", () => {
|
|
const root = tempDirs.make("openclaw-snapshot-swift-lock-");
|
|
const packageRoot = path.join(root, "package");
|
|
const baseline = path.join(root, "Package.resolved.committed");
|
|
mkdirSync(packageRoot);
|
|
const pin = (identity: string, version: string, revision: string) => ({
|
|
identity,
|
|
kind: "remoteSourceControl",
|
|
location: `https://github.com/example/${identity}.git`,
|
|
state: { revision, version },
|
|
});
|
|
const cmark = pin("swift-cmark", "0.8.0", "c".repeat(40));
|
|
const markdown = pin("swift-markdown", "0.8.0", "d".repeat(40));
|
|
writeFileSync(
|
|
baseline,
|
|
JSON.stringify({
|
|
version: 3,
|
|
pins: [pin("peekaboo", "4.6.0", "b".repeat(40)), cmark, markdown],
|
|
}),
|
|
);
|
|
const verify = (pins: unknown[]) => {
|
|
writeFileSync(
|
|
path.join(packageRoot, "Package.resolved"),
|
|
JSON.stringify({ version: 3, pins }),
|
|
);
|
|
return runHelper(`
|
|
set -euo pipefail
|
|
SWIFT_PACKAGE_LOCK_BASELINE=${JSON.stringify(baseline)}
|
|
SWIFT_PACKAGE_ROOT=${JSON.stringify(packageRoot)}
|
|
${scriptBlock("verify_snapshot_swift_lock() {", "create_verified_peekaboo_snapshot() {", swiftScriptPath)}
|
|
verify_snapshot_swift_lock
|
|
`);
|
|
};
|
|
|
|
const unchanged = verify([cmark, markdown]);
|
|
expect(unchanged.status, unchanged.stderr).toBe(0);
|
|
|
|
const drifted = verify([pin("swift-cmark", "0.9.0", "e".repeat(40)), markdown]);
|
|
expect(drifted.status).toBe(1);
|
|
expect(drifted.stderr).toBe(
|
|
`ERROR: Peekaboo snapshot resolution does not match the committed Package.resolved: swift-cmark: 0.8.0 ${"c".repeat(40)} from https://github.com/example/swift-cmark.git -> 0.9.0 ${"e".repeat(40)} from https://github.com/example/swift-cmark.git\n`,
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
{ operation: "create", exitCode: 1, reason: "No such file or directory", mounts: "empty" },
|
|
{ operation: "attach", exitCode: 73, reason: "Permission denied", mounts: "mounted" },
|
|
{ operation: "attach", exitCode: 73, reason: "Permission denied", mounts: "failed" },
|
|
])(
|
|
"preserves Peekaboo snapshot diagnostics and cleanup: $operation / $mounts",
|
|
({ operation, exitCode, reason, mounts }) => {
|
|
const root = tempDirs.make("openclaw-peekaboo-snapshot-fixture-");
|
|
const buildPath = path.join(root, "build with spaces");
|
|
const checkout = path.join(buildPath, "checkouts", "Peekaboo");
|
|
const scratch = path.join(root, "temporary snapshots");
|
|
const mount = path.join(scratch, "mounted source");
|
|
const unrelated = path.join(scratch, "unrelated-snapshot", "marker");
|
|
const operationsPath = path.join(root, "operations");
|
|
const expectedCommit = "b".repeat(40);
|
|
mkdirSync(checkout, { recursive: true });
|
|
mkdirSync(path.dirname(unrelated), { recursive: true });
|
|
writeFileSync(path.join(checkout, "source"), "source preserved\n");
|
|
writeFileSync(unrelated, "unrelated snapshot preserved\n");
|
|
const hdiutil = path.join(root, "hdiutil");
|
|
writeFileSync(
|
|
hdiutil,
|
|
`#!/bin/bash
|
|
set -euo pipefail
|
|
printf '%s\\n' "$1" >> "$operations"
|
|
printf '%s\\0' "$@" > "$fixture_root/$1.args"
|
|
if [[ "$1" == create ]]; then
|
|
image="\${@: -1}"
|
|
printf '%s' "\${image%/*}" > "$fixture_root/snapshot-root"
|
|
: > "$image"
|
|
fi
|
|
for arg in "$@"; do
|
|
if [[ "$arg" == -quiet ]]; then
|
|
exec 1>&- 2>&-
|
|
fi
|
|
done
|
|
if [[ "$1" == ${JSON.stringify(operation)} ]]; then
|
|
printf 'hdiutil: %s failed - %s\\n' "$1" ${JSON.stringify(reason)} >&2 || true
|
|
exit ${exitCode}
|
|
fi
|
|
if [[ "$1" == detach ]]; then
|
|
exit 1
|
|
fi
|
|
printf 'hdiutil: %s completed\\n' "$1" || true
|
|
exit 0
|
|
`,
|
|
);
|
|
chmodSync(hdiutil, 0o755);
|
|
const mountCommand = path.join(root, "mount");
|
|
writeFileSync(
|
|
mountCommand,
|
|
`#!/bin/bash
|
|
printf 'mount\\n' >> "$operations"
|
|
${mounts === "failed" ? "exit 1" : mounts === "mounted" ? `printf '/dev/disk9 on %s (apfs, read-only)\\n' "$fixture_mount"` : "exit 0"}
|
|
`,
|
|
);
|
|
chmodSync(mountCommand, 0o755);
|
|
|
|
const result = runHelper(
|
|
`
|
|
set -euo pipefail
|
|
export fixture_root=${JSON.stringify(root)}
|
|
export operations=${JSON.stringify(operationsPath)}
|
|
export fixture_mount=${JSON.stringify(mount)}
|
|
export PATH=${JSON.stringify(`${root}:/usr/bin:/bin`)}
|
|
TMPDIR=${JSON.stringify(scratch)}
|
|
ROOT_DIR=${JSON.stringify(root)}
|
|
${getSwiftPackageResolutionBlock()}
|
|
PEEKABOO_SNAPSHOT_MOUNT="$fixture_mount"
|
|
trap cleanup_swift_architecture EXIT
|
|
BUILD_PATH=${JSON.stringify(buildPath)}
|
|
compiled_peekaboo_commit() {
|
|
printf 'verify:%s:%s\\n' "$1" "$2" >> "$operations"
|
|
printf '%s' "$2"
|
|
}
|
|
rm() {
|
|
printf 'remove:%s\\n' "$*" >> "$operations"
|
|
command rm "$@"
|
|
}
|
|
create_verified_peekaboo_snapshot ${JSON.stringify(buildPath)} ${JSON.stringify(expectedCommit)}
|
|
printf 'snapshot-ready\\n' >> "$operations"
|
|
`,
|
|
"/bin/bash",
|
|
);
|
|
|
|
const snapshotRoot = readFileSync(path.join(root, "snapshot-root"), "utf8");
|
|
const image = path.join(snapshotRoot, "Peekaboo.dmg");
|
|
const expectedOperations = [`verify:${checkout}:${expectedCommit}`, "create"];
|
|
if (operation !== "create") {
|
|
expectedOperations.push("attach");
|
|
}
|
|
expectedOperations.push("detach", "mount");
|
|
const retained = mounts !== "empty";
|
|
if (!retained) {
|
|
expectedOperations.push(
|
|
`remove:-rf ${snapshotRoot} ${mount} ${path.join(root, "work/resource-backups")}`,
|
|
);
|
|
}
|
|
expect(result.status).toBe(retained ? 1 : exitCode);
|
|
expect(readFileSync(operationsPath, "utf8").trim().split("\n")).toEqual(expectedOperations);
|
|
expect(existsSync(snapshotRoot)).toBe(retained);
|
|
expect(existsSync(mount)).toBe(retained);
|
|
expect(readFileSync(path.join(checkout, "source"), "utf8")).toBe("source preserved\n");
|
|
expect(readFileSync(unrelated, "utf8")).toBe("unrelated snapshot preserved\n");
|
|
const readArgs = (command: string) =>
|
|
readFileSync(path.join(root, `${command}.args`), "utf8")
|
|
.split("\0")
|
|
.slice(0, -1)
|
|
.filter((arg) => arg !== "-quiet");
|
|
expect(readArgs("create")).toEqual([
|
|
"create",
|
|
"-fs",
|
|
"APFS",
|
|
"-format",
|
|
"UDRO",
|
|
"-srcfolder",
|
|
checkout,
|
|
"-volname",
|
|
"OpenClawPeekabooSnapshot",
|
|
image,
|
|
]);
|
|
if (operation !== "create") {
|
|
expect(readArgs("attach")).toEqual([
|
|
"attach",
|
|
"-readonly",
|
|
"-nobrowse",
|
|
"-mountpoint",
|
|
mount,
|
|
image,
|
|
]);
|
|
}
|
|
expect(readArgs("detach")).toEqual(["detach", mount]);
|
|
expect(result.stdout).toBe("");
|
|
expect(result.stderr).toBe(`hdiutil: ${operation} failed - ${reason}\n`);
|
|
},
|
|
);
|
|
|
|
it("stamps only the clean Peekaboo source that SwiftPM actually compiled", () => {
|
|
const verifier = getCompiledPeekabooHelperBlock();
|
|
expect(verifier).toContain('"core.commitGraph=false"');
|
|
expect(verifier).toContain('"--no-replace-objects"');
|
|
expect(verifier).toContain('"fsck", "--full", "--strict"');
|
|
expect(verifier).toContain('"cat-file", object_type');
|
|
expect(readFileSync(swiftScriptPath, "utf8")).toContain(
|
|
'swift package --scratch-path "$build_path" edit Peekaboo --path "$PEEKABOO_SNAPSHOT_MOUNT"',
|
|
);
|
|
const mismatched = runRealCompiledPeekabooHarness("none", "e".repeat(40));
|
|
expect(mismatched.status).toBe(1);
|
|
expect(mismatched.stderr).toContain("does not match locked source");
|
|
});
|
|
|
|
// Each real Git fixture owns a separate checkout and deadline; do not aggregate
|
|
// independent verification scenarios into one long synchronous test.
|
|
it.each(["nested-gitlink"] as const)("accepts committed Peekaboo source (%s)", (mutation) => {
|
|
const result = runRealCompiledPeekabooHarness(mutation);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
});
|
|
|
|
it.each([
|
|
"assume-unchanged",
|
|
"corrupt-object",
|
|
"dirty-gitlink",
|
|
"export-subst",
|
|
"gitlink-sibling",
|
|
"ignored",
|
|
"replacement-ref",
|
|
] as const)("rejects uncommitted Peekaboo source (%s)", (mutation) => {
|
|
const result = runRealCompiledPeekabooHarness(mutation);
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
"Compiled Peekaboo checkout does not exactly match its committed source",
|
|
);
|
|
});
|
|
|
|
it("restores and rejects a Swift package resolution that changes the lockfile", () => {
|
|
const { result, resolvedFile } = runSwiftPackageResolutionHarness();
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("ERROR: Swift package resolution changed Package.resolved");
|
|
expect(readFileSync(resolvedFile, "utf8")).toBe("locked\n");
|
|
});
|
|
|
|
it("stages the pinned CUA driver and thins single-architecture packages before signing", () => {
|
|
const packageScript = readFileSync(scriptPath, "utf8");
|
|
const stageScript = readFileSync("scripts/stage-cua-driver-macos.sh", "utf8");
|
|
const codesignScript = readFileSync("scripts/codesign-mac-app.sh", "utf8");
|
|
expect(stageScript).toContain('TAG="cua-driver-rs-v${VERSION}"');
|
|
expect(stageScript).toContain(
|
|
'ARTIFACT_MANIFEST="$ROOT_DIR/extensions/cua-computer/package.json"',
|
|
);
|
|
expect(stageScript).toContain('manifest.dependencies["@trycua/cua-driver"]');
|
|
expect(stageScript).toContain('manifest.cuaDriverArtifacts["darwin-universal-binary"]');
|
|
expect(packageScript).toContain('"$ROOT_DIR/scripts/stage-cua-driver-macos.sh" "$CUA_DRIVER"');
|
|
expect(packageScript).toContain('if [[ "${#BUILD_ARCHS[@]}" -eq 1 ]]');
|
|
expect(packageScript).toContain('lipo "$CUA_DRIVER" -thin "$CUA_ARCH"');
|
|
expect(packageScript).toContain('[[ "$(lipo -archs "$CUA_DRIVER")" == "$CUA_ARCH" ]]');
|
|
expect(packageScript.indexOf("Staging embedded CUA driver")).toBeLessThan(
|
|
packageScript.indexOf('echo "🔏 Signing bundle'),
|
|
);
|
|
expect(codesignScript).toContain(
|
|
'echo "Signing embedded CUA driver"; sign_plain_item "$CUA_DRIVER"',
|
|
);
|
|
});
|
|
|
|
it("omits the CUA driver only from elevation-host packages", () => {
|
|
const packageScript = readFileSync(scriptPath, "utf8");
|
|
const variantBlock = packageScript.slice(
|
|
packageScript.indexOf('SIGNING_VARIANT="${OPENCLAW_MAC_SIGNING_VARIANT:-standard}"'),
|
|
packageScript.indexOf("# OPENCLAW_SKIP_MLX_TTS"),
|
|
);
|
|
const cuaBlock = packageScript.slice(
|
|
packageScript.indexOf('if [[ "$SIGNING_VARIANT" == "elevation-host" ]]'),
|
|
packageScript.indexOf('echo "📦 Copying CLI installer"'),
|
|
);
|
|
|
|
expect(variantBlock).toContain("standard | elevation-host");
|
|
expect(variantBlock).toContain("Unknown OPENCLAW_MAC_SIGNING_VARIANT value");
|
|
expect(cuaBlock).toContain("Omitting embedded CUA driver from elevation-host package");
|
|
expect(cuaBlock).toContain("else");
|
|
expect(cuaBlock).toContain("Staging embedded CUA driver");
|
|
expect(cuaBlock).toContain('"$ROOT_DIR/scripts/stage-cua-driver-macos.sh" "$CUA_DRIVER"');
|
|
});
|
|
|
|
it("does not mask required Info.plist stamp failures", () => {
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
const stampBlock = script.slice(
|
|
script.indexOf("plist_set_string_required"),
|
|
script.indexOf('echo "🚚 Copying binary"'),
|
|
);
|
|
|
|
expect(stampBlock).toContain("plist_set_string_required");
|
|
expect(stampBlock).not.toContain("|| true");
|
|
});
|
|
|
|
it.runIf(process.platform === "darwin")(
|
|
"sets required strings and fails when the plist cannot be stamped",
|
|
() => {
|
|
const plist = makePlist();
|
|
const result = runHelper(`
|
|
set -euo pipefail
|
|
source scripts/lib/plistbuddy.sh
|
|
plist_set_string_required ${JSON.stringify(plist)} CFBundleIdentifier 'ai.openclaw.test'
|
|
/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' ${JSON.stringify(plist)}
|
|
broken="$(mktemp -d)"
|
|
plist_set_string_required "$broken" CFBundleIdentifier broken
|
|
`);
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stdout).toContain("ai.openclaw.test");
|
|
expect(result.stderr).toContain("Error Reading File");
|
|
},
|
|
);
|
|
|
|
it.runIf(process.platform === "darwin")("adds optional strings and booleans", () => {
|
|
const plist = makePlist();
|
|
const result = runHelper(`
|
|
set -euo pipefail
|
|
source scripts/lib/plistbuddy.sh
|
|
plist_set_or_add_string ${JSON.stringify(plist)} SUFeedURL ''
|
|
plist_set_or_add_string ${JSON.stringify(plist)} SUPublicEDKey 'key"with\\\\slashes'
|
|
plist_set_or_add_bool ${JSON.stringify(plist)} SUEnableAutomaticChecks false
|
|
/usr/libexec/PlistBuddy -c 'Print :SUFeedURL' ${JSON.stringify(plist)}
|
|
/usr/libexec/PlistBuddy -c 'Print :SUPublicEDKey' ${JSON.stringify(plist)}
|
|
/usr/libexec/PlistBuddy -c 'Print :SUEnableAutomaticChecks' ${JSON.stringify(plist)}
|
|
`);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stdout).toContain('key"with\\\\slashes');
|
|
expect(result.stdout).toContain("false");
|
|
});
|
|
});
|