mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 18:08:10 +00:00
296 lines
10 KiB
TypeScript
296 lines
10 KiB
TypeScript
// Create Dmg tests cover create dmg 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/create-dmg.sh";
|
|
|
|
function makeApp(plistEntries: string[]): string {
|
|
const dir = mkdtempSync(path.join(tmpdir(), "openclaw-create-dmg-"));
|
|
tempDirs.push(dir);
|
|
const app = path.join(dir, "OpenClaw.app");
|
|
const contents = path.join(app, "Contents");
|
|
mkdirSync(contents, { recursive: true });
|
|
writeFileSync(
|
|
path.join(contents, "Info.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>",
|
|
...plistEntries,
|
|
"</dict>",
|
|
"</plist>",
|
|
"",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
return app;
|
|
}
|
|
|
|
function makeValidApp(): string {
|
|
return makeApp([
|
|
"<key>CFBundleName</key>",
|
|
"<string>OpenClaw</string>",
|
|
"<key>CFBundleShortVersionString</key>",
|
|
"<string>2026.6.16</string>",
|
|
]);
|
|
}
|
|
|
|
function makeFakeDmgTools() {
|
|
const dir = mkdtempSync(path.join(tmpdir(), "openclaw-create-dmg-tools-"));
|
|
tempDirs.push(dir);
|
|
const bin = path.join(dir, "bin");
|
|
const hdiutilLog = path.join(dir, "hdiutil.log");
|
|
const osascriptLog = path.join(dir, "osascript.applescript");
|
|
mkdirSync(bin, { recursive: true });
|
|
const hdiutil = path.join(bin, "hdiutil");
|
|
writeFileSync(
|
|
hdiutil,
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
printf '%s\\n' "$*" >> "$HDIUTIL_LOG"
|
|
command_name="\${1:-}"
|
|
shift || true
|
|
if [[ "\${HDIUTIL_FAIL_ON:-}" == "$command_name" ]]; then
|
|
exit 17
|
|
fi
|
|
case "$command_name" in
|
|
create)
|
|
: > "\${!#}"
|
|
;;
|
|
attach)
|
|
mountpoint=""
|
|
while (($#)); do
|
|
if [[ "$1" == "-mountpoint" ]]; then
|
|
mountpoint="$2"
|
|
break
|
|
fi
|
|
shift
|
|
done
|
|
if [[ "\${HDIUTIL_ATTACH_MARKER:-0}" == "1" && -n "$mountpoint" ]]; then
|
|
mkdir -p "$mountpoint"
|
|
printf mounted > "$mountpoint/live-volume-file"
|
|
fi
|
|
;;
|
|
detach)
|
|
if [[ "\${HDIUTIL_DETACH_FAIL:-0}" == "1" ]]; then
|
|
exit 9
|
|
fi
|
|
;;
|
|
resize)
|
|
if [[ "\${1:-}" == "-limits" ]]; then
|
|
printf '100 200 300\\n'
|
|
fi
|
|
;;
|
|
convert)
|
|
output=""
|
|
while (($#)); do
|
|
if [[ "$1" == "-o" ]]; then
|
|
output="$2"
|
|
break
|
|
fi
|
|
shift
|
|
done
|
|
[[ -n "$output" ]]
|
|
printf 'converted' > "$output"
|
|
;;
|
|
verify)
|
|
[[ -f "\${1:-}" ]]
|
|
;;
|
|
esac
|
|
`,
|
|
"utf8",
|
|
);
|
|
chmodSync(hdiutil, 0o755);
|
|
const osascript = path.join(bin, "osascript");
|
|
writeFileSync(osascript, '#!/usr/bin/env bash\ncat > "$OSASCRIPT_LOG"\nexit 0\n', "utf8");
|
|
chmodSync(osascript, 0o755);
|
|
const sleep = path.join(bin, "sleep");
|
|
writeFileSync(sleep, "#!/usr/bin/env bash\nexit 0\n", "utf8");
|
|
chmodSync(sleep, 0o755);
|
|
return {
|
|
env: {
|
|
HDIUTIL_LOG: hdiutilLog,
|
|
OSASCRIPT_LOG: osascriptLog,
|
|
PATH: `${bin}:${process.env.PATH ?? ""}`,
|
|
SKIP_DMG_STYLE: "1",
|
|
},
|
|
hdiutilLog,
|
|
osascriptLog,
|
|
};
|
|
}
|
|
|
|
function runScript(args: string[], env: NodeJS.ProcessEnv = {}) {
|
|
return spawnSync("bash", [scriptPath, ...args], {
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
env: { ...process.env, ...env },
|
|
});
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("create-dmg plist validation", () => {
|
|
it("fails closed for required Info.plist reads", () => {
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
const readBlock = script.slice(
|
|
script.indexOf("APP_NAME="),
|
|
script.indexOf('DMG_NAME="${APP_NAME}-${VERSION}.dmg"'),
|
|
);
|
|
|
|
expect(script).toContain('source "$ROOT_DIR/scripts/lib/plistbuddy.sh"');
|
|
expect(readBlock).toContain(
|
|
'APP_NAME="$(plist_print_required "$APP_PATH/Contents/Info.plist" CFBundleName)"',
|
|
);
|
|
expect(readBlock).toContain(
|
|
'VERSION="$(plist_print_required "$APP_PATH/Contents/Info.plist" CFBundleShortVersionString)"',
|
|
);
|
|
expect(readBlock).not.toContain("PlistBuddy");
|
|
expect(readBlock).not.toContain("|| echo");
|
|
});
|
|
|
|
it("keeps temporary DMG artifacts scoped to one run", () => {
|
|
const script = readFileSync(scriptPath, "utf8");
|
|
|
|
expect(script).toContain('DMG_TEMP="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-dmg.XXXXXX")"');
|
|
expect(script).toContain('DMG_SOURCE="$DMG_TEMP/source"');
|
|
expect(script).toContain('MOUNT_POINT="$DMG_TEMP/mount"');
|
|
expect(script).toContain('DMG_RW_PATH="$DMG_TEMP/image-rw.dmg"');
|
|
expect(script).toContain('DMG_OUTPUT_TEMP=""');
|
|
expect(script).toContain('DMG_FINAL_PATH=""');
|
|
expect(script).toContain(
|
|
'DMG_OUTPUT_TEMP="$(mktemp -d "$(dirname "$OUT_PATH")/.openclaw-dmg.XXXXXX")"',
|
|
);
|
|
expect(script).toContain('DMG_FINAL_PATH="$DMG_OUTPUT_TEMP/final.dmg"');
|
|
expect(script).toContain('DMG_LIMITS_PATH="$DMG_TEMP/resize-limits.txt"');
|
|
expect(script).toContain('hdiutil resize -limits "$DMG_RW_PATH" >"$DMG_LIMITS_PATH"');
|
|
expect(script).not.toContain("/tmp/openclaw-dmg-limits.txt");
|
|
expect(script).not.toContain('"/Volumes/$DMG_VOLUME_NAME"');
|
|
expect(script).not.toContain('tell application "Finder" to close every window');
|
|
});
|
|
|
|
it.runIf(process.platform === "darwin")(
|
|
"fails before hdiutil when required plist keys are missing",
|
|
() => {
|
|
const app = makeApp(["<key>CFBundleName</key>", "<string>OpenClaw</string>"]);
|
|
const result = runScript([app, path.join(path.dirname(app), "out.dmg")]);
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("Does Not Exist");
|
|
expect(result.stdout).not.toContain("Creating DMG:");
|
|
},
|
|
);
|
|
});
|
|
|
|
describe.runIf(process.platform === "darwin")("create-dmg ownership boundaries", () => {
|
|
it("uses private intermediate paths without deleting caller-owned siblings", () => {
|
|
const app = makeValidApp();
|
|
const outputDir = mkdtempSync(path.join(tmpdir(), "openclaw-create-dmg-output-"));
|
|
tempDirs.push(outputDir);
|
|
const output = path.join(outputDir, "OpenClaw.dmg");
|
|
const sibling = path.join(outputDir, "OpenClaw-rw.dmg");
|
|
writeFileSync(output, "previous output", "utf8");
|
|
writeFileSync(sibling, "caller owned", "utf8");
|
|
const tools = makeFakeDmgTools();
|
|
|
|
const result = runScript([app, output], tools.env);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(readFileSync(output, "utf8")).toBe("converted");
|
|
expect(readFileSync(sibling, "utf8")).toBe("caller owned");
|
|
const log = readFileSync(tools.hdiutilLog, "utf8");
|
|
expect(log).toContain("image-rw.dmg -mountpoint");
|
|
expect(log).toContain("convert ");
|
|
expect(log).toContain("final.dmg");
|
|
expect(log).toContain(`${outputDir}${path.sep}.openclaw-dmg.`);
|
|
expect(log).not.toContain("/Volumes/");
|
|
expect(log).not.toContain(sibling);
|
|
});
|
|
|
|
it("preserves an existing output when image creation fails", () => {
|
|
const app = makeValidApp();
|
|
const outputDir = mkdtempSync(path.join(tmpdir(), "openclaw-create-dmg-output-"));
|
|
tempDirs.push(outputDir);
|
|
const output = path.join(outputDir, "OpenClaw.dmg");
|
|
writeFileSync(output, "previous output", "utf8");
|
|
const tools = makeFakeDmgTools();
|
|
|
|
const result = runScript([app, output], { ...tools.env, HDIUTIL_FAIL_ON: "create" });
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(readFileSync(output, "utf8")).toBe("previous output");
|
|
expect(readFileSync(tools.hdiutilLog, "utf8")).not.toContain("detach");
|
|
});
|
|
|
|
it("preserves an existing output when verification fails", () => {
|
|
const app = makeValidApp();
|
|
const outputDir = mkdtempSync(path.join(tmpdir(), "openclaw-create-dmg-output-"));
|
|
tempDirs.push(outputDir);
|
|
const output = path.join(outputDir, "OpenClaw.dmg");
|
|
writeFileSync(output, "previous output", "utf8");
|
|
const tools = makeFakeDmgTools();
|
|
|
|
const result = runScript([app, output], { ...tools.env, HDIUTIL_FAIL_ON: "verify" });
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(readFileSync(output, "utf8")).toBe("previous output");
|
|
const log = readFileSync(tools.hdiutilLog, "utf8");
|
|
expect(log).toContain("convert ");
|
|
expect(log).toContain("final.dmg");
|
|
});
|
|
|
|
it("fails before resize and conversion when its private mount cannot detach", () => {
|
|
const app = makeValidApp();
|
|
const outputDir = mkdtempSync(path.join(tmpdir(), "openclaw-create-dmg-output-"));
|
|
tempDirs.push(outputDir);
|
|
const output = path.join(outputDir, "OpenClaw.dmg");
|
|
const tools = makeFakeDmgTools();
|
|
|
|
const result = runScript([app, output], {
|
|
...tools.env,
|
|
HDIUTIL_ATTACH_MARKER: "1",
|
|
HDIUTIL_DETACH_FAIL: "1",
|
|
});
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("Failed to detach DMG mount");
|
|
expect(result.stderr).toContain("Preserving DMG temp root");
|
|
const log = readFileSync(tools.hdiutilLog, "utf8");
|
|
expect(log).not.toContain("resize");
|
|
expect(log).not.toContain("convert");
|
|
expect(log).not.toContain("/Volumes/");
|
|
const mountPoint = log.match(/-mountpoint ([^ ]+)/)?.[1];
|
|
expect(mountPoint).toBeTruthy();
|
|
expect(readFileSync(path.join(mountPoint as string, "live-volume-file"), "utf8")).toBe(
|
|
"mounted",
|
|
);
|
|
rmSync(path.dirname(mountPoint as string), { recursive: true, force: true });
|
|
});
|
|
|
|
it("styles the private mount without closing unrelated Finder windows", () => {
|
|
const app = makeValidApp();
|
|
const outputDir = mkdtempSync(path.join(tmpdir(), "openclaw-create-dmg-output-"));
|
|
tempDirs.push(outputDir);
|
|
const output = path.join(outputDir, "OpenClaw.dmg");
|
|
const tools = makeFakeDmgTools();
|
|
|
|
const result = runScript([app, output], { ...tools.env, SKIP_DMG_STYLE: "0" });
|
|
|
|
expect(result.status).toBe(0);
|
|
const applescript = readFileSync(tools.osascriptLog, "utf8");
|
|
expect(applescript).toContain('set dmgRoot to POSIX file "');
|
|
expect(applescript).toContain('/mount" as alias');
|
|
expect(applescript).toContain("set dmgDisk to disk of dmgRoot");
|
|
expect(applescript).not.toContain('tell disk "OpenClaw"');
|
|
expect(applescript).not.toContain("close every window");
|
|
});
|
|
});
|