mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-21 18:08:05 -06:00
9924e2d7a7
* feat(cli): prepare session-host onboarding * fix(cli): gate session-host installer capability
274 lines
8.8 KiB
TypeScript
274 lines
8.8 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const SCRIPT_PATH = "scripts/connect.sh";
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
|
|
function createFixture() {
|
|
const root = tempDirs.make("openclaw-connect-installer-");
|
|
const installer = join(root, "install-cli.sh");
|
|
const installArgs = join(root, "install-args");
|
|
const helpArgs = join(root, "help-args");
|
|
const connectArgs = join(root, "connect-args");
|
|
const targetContent = join(root, "target-content");
|
|
const targetCreatedBeforeHelp = join(root, "target-created-before-help");
|
|
const targetMode = join(root, "target-mode");
|
|
const targetPath = join(root, "target-path");
|
|
writeFileSync(
|
|
installer,
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
printf '%s\n' "$@" >"$FAKE_INSTALL_ARGS"
|
|
prefix=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--prefix) prefix="$2"; shift 2 ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
[[ -n "$prefix" ]]
|
|
mkdir -p "$prefix/bin"
|
|
cat >"$prefix/bin/openclaw" <<'OPENCLAW'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ "$#" -eq 2 && "$1" == "connect" && "$2" == "--help" ]]; then
|
|
printf '%s\n' "$@" >"$FAKE_HELP_ARGS"
|
|
target_files=("\${TMPDIR:-/tmp}"/openclaw-connect.*/join-target)
|
|
if [[ -e "\${target_files[0]}" ]]; then
|
|
: >"$FAKE_TARGET_CREATED_BEFORE_HELP"
|
|
fi
|
|
if [[ -n "\${FAKE_CONNECT_HELP:-}" ]]; then
|
|
printf '%s\n' "$FAKE_CONNECT_HELP"
|
|
else
|
|
printf '%s\n' ' --target-file <path>' ' --service' ' --session-host'
|
|
fi
|
|
exit "\${FAKE_HELP_EXIT:-0}"
|
|
fi
|
|
printf '%s\n' "$@" >"$FAKE_CONNECT_ARGS"
|
|
target_file=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--target-file) target_file="$2"; shift 2 ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
[[ -n "$target_file" ]]
|
|
printf '%s\n' "$target_file" >"$FAKE_TARGET_PATH"
|
|
if mode="$(stat -f '%Lp' "$target_file" 2>/dev/null)"; then
|
|
printf '%s\n' "$mode" >"$FAKE_TARGET_MODE"
|
|
else
|
|
stat -c '%a' "$target_file" >"$FAKE_TARGET_MODE"
|
|
fi
|
|
cat "$target_file" >"$FAKE_TARGET_CONTENT"
|
|
exit "\${FAKE_CLI_EXIT:-0}"
|
|
OPENCLAW
|
|
chmod 0755 "$prefix/bin/openclaw"
|
|
`,
|
|
);
|
|
chmodSync(installer, 0o755);
|
|
return {
|
|
root,
|
|
installer,
|
|
installArgs,
|
|
helpArgs,
|
|
connectArgs,
|
|
targetContent,
|
|
targetCreatedBeforeHelp,
|
|
targetMode,
|
|
targetPath,
|
|
};
|
|
}
|
|
|
|
function runWrapper(
|
|
fixture: ReturnType<typeof createFixture>,
|
|
args: string[],
|
|
env: NodeJS.ProcessEnv = {},
|
|
) {
|
|
return spawnSync("/bin/bash", [SCRIPT_PATH, ...args], {
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
OPENCLAW_INSTALL_CLI_URL: fixture.installer,
|
|
FAKE_INSTALL_ARGS: fixture.installArgs,
|
|
FAKE_HELP_ARGS: fixture.helpArgs,
|
|
FAKE_CONNECT_ARGS: fixture.connectArgs,
|
|
FAKE_TARGET_CONTENT: fixture.targetContent,
|
|
FAKE_TARGET_CREATED_BEFORE_HELP: fixture.targetCreatedBeforeHelp,
|
|
FAKE_TARGET_MODE: fixture.targetMode,
|
|
FAKE_TARGET_PATH: fixture.targetPath,
|
|
TMPDIR: fixture.root,
|
|
...env,
|
|
},
|
|
});
|
|
}
|
|
|
|
function readArgs(path: string): string[] {
|
|
return readFileSync(path, "utf8").trim().split("\n");
|
|
}
|
|
|
|
describe("scripts/connect.sh", () => {
|
|
it("installs an exact version and hands the private target to a session-host service", () => {
|
|
const fixture = createFixture();
|
|
const prefix = join(fixture.root, "prefix");
|
|
const target = "oc-pair://private-join-target";
|
|
|
|
const result = runWrapper(
|
|
fixture,
|
|
["--version", "2026.8.1", "--prefix", prefix, "--display-name", "Runner Node", target],
|
|
{ HOME: undefined },
|
|
);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(readArgs(fixture.installArgs)).toEqual([
|
|
"--version",
|
|
"2026.8.1",
|
|
"--prefix",
|
|
prefix,
|
|
"--no-onboard",
|
|
]);
|
|
expect(readArgs(fixture.helpArgs)).toEqual(["connect", "--help"]);
|
|
expect(existsSync(fixture.targetCreatedBeforeHelp)).toBe(false);
|
|
const connectArgs = readArgs(fixture.connectArgs);
|
|
const privateTargetPath = readFileSync(fixture.targetPath, "utf8").trim();
|
|
expect(connectArgs).toEqual([
|
|
"connect",
|
|
"--target-file",
|
|
privateTargetPath,
|
|
"--service",
|
|
"--session-host",
|
|
"--display-name",
|
|
"Runner Node",
|
|
]);
|
|
expect(readFileSync(fixture.targetContent, "utf8")).toBe(`${target}\n`);
|
|
expect(readFileSync(fixture.targetMode, "utf8").trim()).toBe("600");
|
|
expect(readFileSync(fixture.installArgs, "utf8")).not.toContain(target);
|
|
expect(readFileSync(fixture.connectArgs, "utf8")).not.toContain(target);
|
|
expect(`${result.stdout}\n${result.stderr}`).not.toContain(target);
|
|
expect(existsSync(privateTargetPath)).toBe(false);
|
|
expect(existsSync(dirname(privateTargetPath))).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
{ name: "the help probe fails", env: { FAKE_HELP_EXIT: "2" } },
|
|
{
|
|
name: "help omits --target-file",
|
|
env: { FAKE_CONNECT_HELP: " --service\n --session-host" },
|
|
},
|
|
{
|
|
name: "help omits --service",
|
|
env: { FAKE_CONNECT_HELP: " --target-file <path>\n --session-host" },
|
|
},
|
|
{
|
|
name: "help omits --session-host",
|
|
env: { FAKE_CONNECT_HELP: " --target-file <path>\n --service" },
|
|
},
|
|
])("rejects an installed CLI when $name", ({ env }) => {
|
|
const fixture = createFixture();
|
|
const prefix = join(fixture.root, "prefix");
|
|
const target = "oc-pair://private-join-target";
|
|
|
|
const result = runWrapper(
|
|
fixture,
|
|
["--version", "2026.7.1-2", "--prefix", prefix, target],
|
|
env,
|
|
);
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(readArgs(fixture.installArgs)).toContain("2026.7.1-2");
|
|
expect(readArgs(fixture.helpArgs)).toEqual(["connect", "--help"]);
|
|
expect(result.stderr).toContain(
|
|
"selected exact version 2026.7.1-2 does not support session-host onboarding",
|
|
);
|
|
expect(result.stderr).toContain("Choose a newer supporting exact version");
|
|
expect(result.stderr.match(/ERROR:/gu)).toHaveLength(1);
|
|
expect(existsSync(fixture.targetCreatedBeforeHelp)).toBe(false);
|
|
expect(existsSync(fixture.connectArgs)).toBe(false);
|
|
expect(existsSync(fixture.targetPath)).toBe(false);
|
|
expect(existsSync(fixture.targetContent)).toBe(false);
|
|
expect(`${result.stdout}\n${result.stderr}`).not.toContain(target);
|
|
});
|
|
|
|
it("respects OPENCLAW_PREFIX when --prefix is omitted", () => {
|
|
const fixture = createFixture();
|
|
const prefix = join(fixture.root, "env-prefix");
|
|
|
|
const result = runWrapper(fixture, ["--version", "2026.8.1", "setup-code"], {
|
|
OPENCLAW_PREFIX: prefix,
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(readArgs(fixture.installArgs)).toContain(prefix);
|
|
});
|
|
|
|
it("cleans the private target after an installed CLI failure", () => {
|
|
const fixture = createFixture();
|
|
const prefix = join(fixture.root, "prefix");
|
|
|
|
const result = runWrapper(
|
|
fixture,
|
|
["--version", "2026.8.1", "--prefix", prefix, "setup-code"],
|
|
{ FAKE_CLI_EXIT: "23" },
|
|
);
|
|
|
|
const privateTargetPath = readFileSync(fixture.targetPath, "utf8").trim();
|
|
expect(result.status).toBe(23);
|
|
expect(result.stderr).toContain(
|
|
"OpenClaw could not connect or install the session-host service.",
|
|
);
|
|
expect(existsSync(privateTargetPath)).toBe(false);
|
|
expect(existsSync(dirname(privateTargetPath))).toBe(false);
|
|
});
|
|
|
|
it("requires an explicit version", () => {
|
|
const fixture = createFixture();
|
|
|
|
const result = runWrapper(fixture, ["setup-code"]);
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("--version is required");
|
|
expect(existsSync(fixture.installArgs)).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "default prefix",
|
|
args: ["--version", "2026.8.1", "setup-code"],
|
|
message: "Cannot resolve the default install prefix",
|
|
},
|
|
{
|
|
name: "tilde prefix",
|
|
args: ["--version", "2026.8.1", "--prefix", "~/.openclaw", "setup-code"],
|
|
message: "Cannot expand prefix '~/.openclaw'",
|
|
},
|
|
])("fails cleanly without HOME for the $name", ({ args, message }) => {
|
|
const fixture = createFixture();
|
|
|
|
const result = runWrapper(fixture, args, {
|
|
HOME: undefined,
|
|
OPENCLAW_PREFIX: undefined,
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(message);
|
|
expect(result.stderr.match(/ERROR:/gu)).toHaveLength(1);
|
|
expect(existsSync(fixture.installArgs)).toBe(false);
|
|
});
|
|
|
|
it.each(["latest", "next", "beta", "v2026.8.1", "2026.8", "2026.8.x", "^2026.8.1", "2026.8.*"])(
|
|
"rejects non-exact version %s",
|
|
(version) => {
|
|
const fixture = createFixture();
|
|
|
|
const result = runWrapper(fixture, ["--version", version, "setup-code"]);
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("leading v, moving tags, ranges, and wildcards");
|
|
expect(existsSync(fixture.installArgs)).toBe(false);
|
|
},
|
|
);
|
|
});
|