mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
269 lines
10 KiB
TypeScript
269 lines
10 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
readlinkSync,
|
|
realpathSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { requireNodeTool } from "../helpers/node-toolchain.js";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
const scriptPath = "scripts/install-cli.sh";
|
|
const nodeExecutable = requireNodeTool("node");
|
|
|
|
function fixture() {
|
|
const root = tempDirs.make("openclaw-freebsd-installer-");
|
|
const bin = join(root, "system bin");
|
|
const prefix = join(root, "prefix");
|
|
mkdirSync(bin);
|
|
symlinkSync(nodeExecutable, join(bin, "node"));
|
|
writeFileSync(join(bin, "npm"), "#!/usr/bin/env node\nconsole.log('11.19.1');\n", {
|
|
mode: 0o755,
|
|
});
|
|
return { root, bin, prefix };
|
|
}
|
|
|
|
function run(body: string, env: NodeJS.ProcessEnv = {}) {
|
|
// Sourcing cannot use the installer's system-Bash re-exec guard on macOS.
|
|
return spawnSync(
|
|
process.platform === "darwin" ? "/bin/bash" : "bash",
|
|
["--noprofile", "--norc", "-c", `source ${scriptPath}\n${body}`],
|
|
{
|
|
encoding: "utf8",
|
|
env: { ...process.env, OPENCLAW_INSTALL_CLI_SH_NO_RUN: "1", ...env, BASH_ENV: "", ENV: "" },
|
|
},
|
|
);
|
|
}
|
|
|
|
function install(bin: string, prefix: string, body = "", env: NodeJS.ProcessEnv = {}) {
|
|
return run(
|
|
`
|
|
PREFIX="$FIXTURE_PREFIX"
|
|
PATH="$FIXTURE_BIN:$PATH"
|
|
export PATH
|
|
uname() { printf 'FreeBSD\\n'; }
|
|
download_file() { printf 'unexpected download\\n'; exit 91; }
|
|
pkg() { printf 'unexpected package change\\n'; exit 92; }
|
|
${body}
|
|
install_node "$(os_detect)" x64
|
|
`,
|
|
{ FIXTURE_BIN: bin, FIXTURE_PREFIX: prefix, ...env },
|
|
);
|
|
}
|
|
|
|
describe("FreeBSD CLI runtime installation", () => {
|
|
it("links a usable system runtime and can reuse the resulting runtime links", () => {
|
|
const { bin, prefix } = fixture();
|
|
const result = install(bin, prefix, "JSON=1");
|
|
expect(result.status, result.stdout + result.stderr).toBe(0);
|
|
expect(result.stdout).toContain('"method":"system"');
|
|
const active = join(prefix, "tools", "node", "bin");
|
|
expect(realpathSync(join(active, "node"))).toBe(realpathSync(nodeExecutable));
|
|
expect(realpathSync(join(active, "npm"))).toBe(join(bin, "npm"));
|
|
const rerun = install(active, prefix);
|
|
expect(rerun.status, rerun.stdout + rerun.stderr).toBe(0);
|
|
expect(realpathSync(join(active, "node"))).toBe(realpathSync(nodeExecutable));
|
|
expect(realpathSync(join(active, "npm"))).toBe(join(bin, "npm"));
|
|
});
|
|
|
|
it.each(["missing", "unsupported", "npm", "sqlite", "requested"])(
|
|
"rejects an unusable %s prerequisite without replacing an existing runtime",
|
|
(failure) => {
|
|
const { root, bin, prefix } = fixture();
|
|
const oldRuntime = join(root, "old-runtime");
|
|
mkdirSync(oldRuntime);
|
|
mkdirSync(join(prefix, "tools"), { recursive: true });
|
|
symlinkSync(oldRuntime, join(prefix, "tools", "node"));
|
|
const node = join(bin, "candidate");
|
|
const npm = join(bin, "candidate-npm");
|
|
writeFileSync(
|
|
node,
|
|
failure === "unsupported"
|
|
? "#!/usr/bin/env bash\nprintf 'v22.18.0\\n'\n"
|
|
: `#!/usr/bin/env bash\nif [[ "$1" == -e ]]; then exit 1; fi\nexec "$FIXTURE_NODE" "$@"\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
writeFileSync(npm, "#!/usr/bin/env bash\nexit 42\n", { mode: 0o755 });
|
|
const result = install(
|
|
bin,
|
|
prefix,
|
|
`
|
|
JSON=1
|
|
command_path_without_node_prefix() {
|
|
case "$1" in
|
|
node) printf '%s\\n' "$FIXTURE_CANDIDATE_NODE" ;;
|
|
npm) printf '%s\\n' "$FIXTURE_CANDIDATE_NPM" ;;
|
|
esac
|
|
}
|
|
${failure === "requested" ? "NODE_VERSION=99.0.0; NODE_VERSION_REQUESTED=1" : ""}
|
|
`,
|
|
{
|
|
FIXTURE_NODE: nodeExecutable,
|
|
FIXTURE_CANDIDATE_NODE:
|
|
failure === "missing"
|
|
? ""
|
|
: ["unsupported", "sqlite"].includes(failure)
|
|
? node
|
|
: join(bin, "node"),
|
|
FIXTURE_CANDIDATE_NPM: failure === "npm" ? npm : join(bin, "npm"),
|
|
},
|
|
);
|
|
expect(result.status, result.stdout + result.stderr).toBe(1);
|
|
expect(result.stdout).toContain("npm-node24");
|
|
expect(result.stdout).not.toContain("unexpected");
|
|
expect(readlinkSync(join(prefix, "tools", "node"))).toBe(oldRuntime);
|
|
expect(existsSync(join(prefix, "tools", "node-v24.19.0"))).toBe(false);
|
|
},
|
|
);
|
|
|
|
it("refuses private Node recovery before linking or changing packages", () => {
|
|
const { bin, prefix } = fixture();
|
|
const result = install(bin, prefix, "NODE_ONLY=1");
|
|
expect(result.status).toBe(1);
|
|
expect(result.stdout).toContain("Private Node.js recovery is unavailable on FreeBSD");
|
|
expect(result.stdout).not.toContain("unexpected");
|
|
expect(existsSync(prefix)).toBe(false);
|
|
});
|
|
|
|
it("explains how to install missing Git without invoking pkg", () => {
|
|
const result = run(`
|
|
uname() { printf 'FreeBSD\\n'; }
|
|
command() { if [[ "$*" == '-v git' ]]; then return 1; fi; builtin command "$@"; }
|
|
pkg() { printf 'unexpected package change\\n'; exit 92; }
|
|
ensure_git
|
|
`);
|
|
expect(result.status).toBe(1);
|
|
expect(result.stdout).toContain("pkg install git");
|
|
expect(result.stdout).not.toContain("unexpected");
|
|
});
|
|
});
|
|
|
|
describe("FreeBSD source-install admission", () => {
|
|
it.each(
|
|
["--install-method git", "--method git", "--git", "--github", "--npm --git", ""].flatMap(
|
|
(args) => [false, true].map((json) => ({ args, json })),
|
|
),
|
|
)("refuses $args before installation side effects (JSON: $json)", ({ args, json }) => {
|
|
const { root, prefix } = fixture();
|
|
const oldRuntime = join(root, "old-runtime");
|
|
const checkout = join(root, "checkout");
|
|
const temporary = join(root, "temporary");
|
|
mkdirSync(oldRuntime);
|
|
mkdirSync(checkout);
|
|
mkdirSync(temporary);
|
|
mkdirSync(join(prefix, "bin"), { recursive: true });
|
|
mkdirSync(join(prefix, "tools"));
|
|
symlinkSync(oldRuntime, join(prefix, "tools", "node"));
|
|
writeFileSync(join(prefix, "bin", "openclaw"), "preserve installed CLI");
|
|
writeFileSync(join(checkout, "pnpm-workspace.yaml"), "preserve source allowlist");
|
|
const result = run(
|
|
`
|
|
os_detect() { printf 'freebsd\\n'; }
|
|
prepare_tmpdir() { printf 'unexpected temporary setup\\n'; exit 91; }
|
|
preflight_fresh_git_disk_space() { printf 'unexpected disk preflight\\n'; exit 92; }
|
|
install_node() { printf 'unexpected runtime change\\n'; exit 93; }
|
|
install_openclaw_from_git() { printf 'unexpected checkout change\\n'; exit 94; }
|
|
refresh_gateway_service_if_loaded() { printf 'unexpected service change\\n'; exit 95; }
|
|
main ${json ? "--json" : ""} ${args}
|
|
`,
|
|
{
|
|
HOME: root,
|
|
TMPDIR: temporary,
|
|
OPENCLAW_PREFIX: prefix,
|
|
OPENCLAW_GIT_DIR: checkout,
|
|
OPENCLAW_INSTALL_METHOD: args ? "npm" : "git",
|
|
},
|
|
);
|
|
expect(result.status, result.stdout + result.stderr).toBe(1);
|
|
expect(result.stderr).toBe("");
|
|
const message = json
|
|
? (JSON.parse(result.stdout) as { event: string; message: string }).message
|
|
: result.stdout;
|
|
if (json) {
|
|
expect(JSON.parse(result.stdout)).toMatchObject({ event: "error" });
|
|
} else {
|
|
expect(result.stdout).toMatch(/^ERROR: /);
|
|
}
|
|
expect(message).toContain("Source/git installation is unsupported on FreeBSD");
|
|
expect(message).toContain("--install-method npm");
|
|
expect(message).toContain("published version or compatible built .tgz");
|
|
expect(message).toContain("same --prefix");
|
|
expect(message).toContain("pkg/Ports-managed");
|
|
expect(result.stdout).not.toContain("unexpected");
|
|
expect(readFileSync(join(prefix, "bin", "openclaw"), "utf8")).toBe("preserve installed CLI");
|
|
expect(readlinkSync(join(prefix, "tools", "node"))).toBe(oldRuntime);
|
|
expect(readdirSync(join(prefix, "tools"))).toEqual(["node"]);
|
|
expect(readFileSync(join(checkout, "pnpm-workspace.yaml"), "utf8")).toBe(
|
|
"preserve source allowlist",
|
|
);
|
|
expect(readdirSync(checkout)).toEqual(["pnpm-workspace.yaml"]);
|
|
expect(readdirSync(temporary)).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
["freebsd", "--git --npm"],
|
|
["freebsd", "--github --install-method npm"],
|
|
["freebsd", "--method npm"],
|
|
["linux", "--git"],
|
|
["darwin", "--git"],
|
|
["linux", "--npm"],
|
|
["darwin", "--npm"],
|
|
])("keeps %s %s on its selected install route", (os, args) => {
|
|
const result = run(
|
|
`
|
|
os_detect() { printf '${os}\\n'; }
|
|
arch_detect() { printf 'x64\\n'; }
|
|
prepare_tmpdir() { :; }
|
|
preflight_fresh_git_disk_space() { :; }
|
|
install_node() { printf 'selected:%s:%s\\n' "$1" "$INSTALL_METHOD"; exit 73; }
|
|
main ${args}
|
|
`,
|
|
{ OPENCLAW_INSTALL_METHOD: "git" },
|
|
);
|
|
expect(result.status, result.stdout + result.stderr).toBe(73);
|
|
expect(result.stdout.trim()).toBe(`selected:${os}:${args === "--git" ? "git" : "npm"}`);
|
|
});
|
|
|
|
it("keeps the FreeBSD Node-only refusal ahead of the ignored git method", () => {
|
|
const { root, prefix } = fixture();
|
|
const result = run(
|
|
`
|
|
os_detect() { printf 'freebsd\\n'; }
|
|
arch_detect() { printf 'x64\\n'; }
|
|
main --node-only --git
|
|
`,
|
|
{ HOME: root, TMPDIR: root, OPENCLAW_PREFIX: prefix },
|
|
);
|
|
expect(result.status).toBe(1);
|
|
expect(result.stdout).toContain("Private Node.js recovery is unavailable on FreeBSD");
|
|
expect(result.stdout).not.toContain("Source/git");
|
|
expect(existsSync(prefix)).toBe(false);
|
|
});
|
|
|
|
it.each(["main", "github:openclaw/openclaw#main"])(
|
|
"does not recommend git for the rejected npm source target %s",
|
|
(version) => {
|
|
const result = run(
|
|
`
|
|
os_detect() { printf 'freebsd\\n'; }
|
|
JSON=1
|
|
install_openclaw
|
|
`,
|
|
{ OPENCLAW_VERSION: version },
|
|
);
|
|
expect(result.status).toBe(1);
|
|
expect(JSON.parse(result.stdout)).toMatchObject({
|
|
event: "error",
|
|
message: expect.stringContaining("--install-method npm"),
|
|
});
|
|
expect(result.stdout).not.toContain("--install-method git");
|
|
},
|
|
);
|
|
});
|