openclaw / src /cli /node-cli /register.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
f778c12 verified
Raw
History Blame Contribute Delete
14.2 kB
// Node CLI register tests cover node command registration and option wiring.
import { Command } from "commander";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { encodePairingSetupCode } from "../../pairing/setup-code.js";
import { registerNodeCli } from "./register.js";
const PAIR_TLS_FINGERPRINT = "ab".repeat(32);
const EXPLICIT_TLS_FINGERPRINT = "cd".repeat(32);
const SAVED_TLS_FINGERPRINT = "ef".repeat(32);
type LoadNodeHostConfig = typeof import("../../node-host/config.js").loadNodeHostConfig;
const daemonMocks = vi.hoisted(() => ({
defaultRuntime: {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
},
loadNodeHostConfig: vi.fn<LoadNodeHostConfig>(async () => null),
runNodeHost: vi.fn(),
runNodeDaemonInstall: vi.fn(),
runNodeDaemonRestart: vi.fn(),
runNodeDaemonStart: vi.fn(),
runNodeDaemonStatus: vi.fn(),
runNodeDaemonStop: vi.fn(),
runNodeDaemonUninstall: vi.fn(),
}));
vi.mock("./daemon.js", () => daemonMocks);
vi.mock("../../node-host/config.js", () => ({
loadNodeHostConfig: daemonMocks.loadNodeHostConfig,
}));
vi.mock("../../node-host/runner.js", () => ({
runNodeHost: daemonMocks.runNodeHost,
}));
vi.mock("../../runtime.js", () => ({
defaultRuntime: daemonMocks.defaultRuntime,
}));
function createProgram(): Command {
const program = new Command();
program.exitOverride();
program.configureOutput({
writeErr: () => undefined,
writeOut: () => undefined,
});
registerNodeCli(program);
return program;
}
describe("registerNodeCli", () => {
beforeEach(() => {
daemonMocks.defaultRuntime.error.mockClear();
daemonMocks.defaultRuntime.exit.mockClear();
daemonMocks.loadNodeHostConfig.mockClear();
daemonMocks.loadNodeHostConfig.mockResolvedValue(null);
daemonMocks.runNodeHost.mockClear();
daemonMocks.runNodeDaemonInstall.mockClear();
daemonMocks.runNodeDaemonRestart.mockClear();
daemonMocks.runNodeDaemonStart.mockClear();
daemonMocks.runNodeDaemonStatus.mockClear();
daemonMocks.runNodeDaemonStop.mockClear();
daemonMocks.runNodeDaemonUninstall.mockClear();
});
it.each([
["status", daemonMocks.runNodeDaemonStatus],
["uninstall", daemonMocks.runNodeDaemonUninstall],
["stop", daemonMocks.runNodeDaemonStop],
["start", daemonMocks.runNodeDaemonStart],
["restart", daemonMocks.runNodeDaemonRestart],
])("registers node %s and forwards --json", async (command, action) => {
const program = createProgram();
await program.parseAsync(["node", command, "--json"], { from: "user" });
expect(action.mock.calls[0]?.[0]?.json).toBe(true);
});
it("forwards node install options to the daemon adapter", async () => {
const program = createProgram();
await program.parseAsync(
[
"node",
"install",
"--port",
"19000",
"--host",
"gateway.example",
"--runtime",
"bun",
"--force",
"--json",
],
{ from: "user" },
);
expect(daemonMocks.runNodeDaemonInstall).toHaveBeenCalledWith(
expect.objectContaining({
port: "19000",
host: "gateway.example",
runtime: "bun",
force: true,
json: true,
}),
);
});
it.each(["run", "install"] as const)(
"accepts exact command allowlists before or after node %s",
async (leaf) => {
const action = leaf === "run" ? daemonMocks.runNodeHost : daemonMocks.runNodeDaemonInstall;
for (const args of [
["node", "--commands", "fixture.read,fixture.list", leaf],
["node", leaf, "--commands", "fixture.read", "--commands", "fixture.list,fixture.read"],
]) {
await createProgram().parseAsync(args, { from: "user" });
expect(action).toHaveBeenLastCalledWith(
expect.objectContaining({
commands: ["fixture.list", "fixture.read"],
}),
);
}
},
);
it("rejects empty command ids instead of silently widening the surface", async () => {
await expect(
createProgram().parseAsync(["node", "run", "--commands", "fixture.list,"], { from: "user" }),
).rejects.toThrow("non-empty command ids");
expect(daemonMocks.runNodeHost).not.toHaveBeenCalled();
});
it.each(["run", "install"] as const)(
"accepts --all-commands before or after node %s",
async (leaf) => {
const action = leaf === "run" ? daemonMocks.runNodeHost : daemonMocks.runNodeDaemonInstall;
for (const args of [
["node", "--all-commands", leaf],
["node", leaf, "--all-commands"],
]) {
await createProgram().parseAsync(args, { from: "user" });
expect(action).toHaveBeenLastCalledWith(expect.objectContaining({ allCommands: true }));
}
},
);
it.each(["run", "install"] as const)(
"rejects conflicting command selections for node %s",
async (leaf) => {
for (const args of [
["node", leaf, "--all-commands", "--commands", "fixture.read"],
["node", "--all-commands", leaf, "--commands", "fixture.read"],
["node", "--commands", "fixture.read", leaf, "--all-commands"],
["node", "--commands", "fixture.read", "--all-commands", leaf],
]) {
await expect(createProgram().parseAsync(args, { from: "user" })).rejects.toThrow(
/--all-commands.*--commands/,
);
}
expect(daemonMocks.runNodeHost).not.toHaveBeenCalled();
expect(daemonMocks.runNodeDaemonInstall).not.toHaveBeenCalled();
expect(daemonMocks.loadNodeHostConfig).not.toHaveBeenCalled();
},
);
it("rejects an explicit invalid node run port", async () => {
const program = createProgram();
await program.parseAsync(["node", "run", "--port", "abc"], { from: "user" });
expect(daemonMocks.runNodeHost).not.toHaveBeenCalled();
expect(daemonMocks.defaultRuntime.error).toHaveBeenCalledWith(
expect.stringContaining("Invalid --port"),
);
expect(daemonMocks.defaultRuntime.exit).toHaveBeenCalledWith(1);
});
it("uses an explicit valid node run port", async () => {
const program = createProgram();
await program.parseAsync(["node", "run", "--port", "19000"], { from: "user" });
expect(daemonMocks.runNodeHost).toHaveBeenCalledWith(
expect.objectContaining({ gatewayPort: 19000 }),
);
});
it("hosts worker turns process-locally for an ephemeral node run", async () => {
const program = createProgram();
await program.parseAsync(["node", "run", "--ephemeral"], { from: "user" });
expect(daemonMocks.runNodeHost).toHaveBeenCalledWith(
expect.objectContaining({ forceWorkerRuns: true, ephemeral: true }),
);
const nodeCommand = program.commands.find((command) => command.name() === "node");
const runCommand = nodeCommand?.commands.find((command) => command.name() === "run");
expect(runCommand?.helpInformation()).not.toContain("--ephemeral");
daemonMocks.runNodeHost.mockClear();
await createProgram().parseAsync(["node", "run"], { from: "user" });
expect(daemonMocks.runNodeHost.mock.calls[0]?.[0]).not.toHaveProperty("forceWorkerRuns");
});
it("falls back to configured node run port when --port is omitted", async () => {
daemonMocks.loadNodeHostConfig.mockResolvedValue({
version: 1,
nodeId: "node-existing",
gateway: { host: "10.0.0.2", port: 19001 },
});
const program = createProgram();
await program.parseAsync(["node", "run"], { from: "user" });
expect(daemonMocks.runNodeHost).toHaveBeenCalledWith(
expect.objectContaining({ gatewayHost: "10.0.0.2", gatewayPort: 19001 }),
);
});
it("derives the node endpoint, TLS pin, and bootstrap credential from --pair", async () => {
const setupCode = encodePairingSetupCode({
url: "wss://gateway.example:8443/openclaw-gw",
bootstrapToken: "bootstrap-123",
tlsFingerprint: `sha256:${PAIR_TLS_FINGERPRINT.toUpperCase()}`,
});
await createProgram().parseAsync(["node", "run", "--pair", `oc-pair://${setupCode}`], {
from: "user",
});
expect(daemonMocks.runNodeHost).toHaveBeenCalledWith(
expect.objectContaining({
gatewayHost: "gateway.example",
gatewayPort: 8443,
gatewayContextPath: "/openclaw-gw",
gatewayTls: true,
gatewayTlsFingerprint: PAIR_TLS_FINGERPRINT,
gatewayCandidates: [
{
host: "gateway.example",
port: 8443,
contextPath: "/openclaw-gw",
tls: true,
tlsFingerprint: PAIR_TLS_FINGERPRINT,
},
],
gatewayBootstrapToken: "bootstrap-123",
preferGatewayBootstrapToken: true,
}),
);
});
it("lets explicit gateway flags override --pair values", async () => {
const setupCode = encodePairingSetupCode({
url: "wss://paired.example:8443",
bootstrapToken: "bootstrap-123",
tlsFingerprint: `sha256:${PAIR_TLS_FINGERPRINT}`,
});
await createProgram().parseAsync(
[
"node",
"run",
"--pair",
setupCode,
"--host",
"explicit.example",
"--port",
"19000",
"--tls-fingerprint",
`sha256:${EXPLICIT_TLS_FINGERPRINT}`,
],
{ from: "user" },
);
expect(daemonMocks.runNodeHost).toHaveBeenCalledWith(
expect.objectContaining({
gatewayHost: "explicit.example",
gatewayPort: 19000,
gatewayTls: true,
gatewayTlsFingerprint: EXPLICIT_TLS_FINGERPRINT,
gatewayCandidates: undefined,
gatewayBootstrapToken: "bootstrap-123",
}),
);
});
it("rejects an invalid --pair value before loading node state", async () => {
await createProgram().parseAsync(["node", "run", "--pair", "not-a-setup-code"], {
from: "user",
});
expect(daemonMocks.runNodeHost).not.toHaveBeenCalled();
expect(daemonMocks.loadNodeHostConfig).not.toHaveBeenCalled();
expect(daemonMocks.defaultRuntime.error).toHaveBeenCalledWith(
expect.stringContaining("Invalid pairing setup"),
);
});
it.each([
["host", ["--host", "10.0.0.2"]],
["port", ["--port", "19001"]],
])("preserves saved gateway settings when the explicit %s is unchanged", async (_name, args) => {
daemonMocks.loadNodeHostConfig.mockResolvedValue({
version: 1,
nodeId: "node-existing",
gateway: {
host: "10.0.0.2",
port: 19001,
tls: true,
tlsFingerprint: SAVED_TLS_FINGERPRINT,
contextPath: "/saved",
},
});
await createProgram().parseAsync(["node", "run", ...args], { from: "user" });
expect(daemonMocks.runNodeHost).toHaveBeenCalledWith(
expect.objectContaining({
gatewayHost: "10.0.0.2",
gatewayPort: 19001,
gatewayTls: true,
gatewayTlsFingerprint: SAVED_TLS_FINGERPRINT,
gatewayContextPath: "/saved",
}),
);
});
it.each([
["host", ["--host", "10.0.0.3"]],
["port", ["--port", "19002"]],
])("clears saved gateway settings when the explicit %s changes", async (_name, args) => {
daemonMocks.loadNodeHostConfig.mockResolvedValue({
version: 1,
nodeId: "node-existing",
gateway: {
host: "10.0.0.2",
port: 19001,
tls: true,
tlsFingerprint: SAVED_TLS_FINGERPRINT,
contextPath: "/saved",
},
});
await createProgram().parseAsync(["node", "run", ...args], { from: "user" });
expect(daemonMocks.runNodeHost).toHaveBeenCalledWith(
expect.objectContaining({
gatewayTls: undefined,
gatewayTlsFingerprint: undefined,
gatewayContextPath: undefined,
}),
);
});
it("inherits saved TLS settings only when using the saved gateway endpoint", async () => {
daemonMocks.loadNodeHostConfig.mockResolvedValue({
version: 1,
nodeId: "node-existing",
gateway: {
host: "10.0.0.2",
port: 19001,
tls: true,
tlsFingerprint: SAVED_TLS_FINGERPRINT,
},
});
await createProgram().parseAsync(["node", "run"], { from: "user" });
expect(daemonMocks.runNodeHost).toHaveBeenLastCalledWith(
expect.objectContaining({
gatewayTls: true,
gatewayTlsFingerprint: SAVED_TLS_FINGERPRINT,
}),
);
await createProgram().parseAsync(["node", "run", "--host", "10.0.0.3"], { from: "user" });
expect(daemonMocks.runNodeHost).toHaveBeenLastCalledWith(
expect.objectContaining({
gatewayHost: "10.0.0.3",
gatewayTls: undefined,
gatewayTlsFingerprint: undefined,
}),
);
});
it("passes an explicit plaintext selection to the node host", async () => {
daemonMocks.loadNodeHostConfig.mockResolvedValue({
version: 1,
nodeId: "node-existing",
gateway: {
host: "10.0.0.2",
port: 19001,
tls: true,
tlsFingerprint: SAVED_TLS_FINGERPRINT,
},
});
await createProgram().parseAsync(["node", "run", "--no-tls"], { from: "user" });
expect(daemonMocks.runNodeHost).toHaveBeenCalledWith(
expect.objectContaining({
gatewayTls: false,
gatewayTlsFingerprint: undefined,
}),
);
});
it("rejects a TLS fingerprint with an explicit plaintext selection", async () => {
await createProgram().parseAsync(
["node", "run", "--no-tls", "--tls-fingerprint", PAIR_TLS_FINGERPRINT],
{ from: "user" },
);
expect(daemonMocks.runNodeHost).not.toHaveBeenCalled();
expect(daemonMocks.defaultRuntime.error).toHaveBeenCalledWith(
"--no-tls cannot be combined with --tls-fingerprint",
);
expect(daemonMocks.defaultRuntime.exit).toHaveBeenCalledWith(1);
});
it("rejects an invalid --tls-fingerprint before starting the node host", async () => {
await createProgram().parseAsync(["node", "run", "--tls-fingerprint", "sha256:abc123"], {
from: "user",
});
expect(daemonMocks.runNodeHost).not.toHaveBeenCalled();
expect(daemonMocks.defaultRuntime.error).toHaveBeenCalledWith(
expect.stringContaining("Invalid TLS fingerprint"),
);
expect(daemonMocks.defaultRuntime.exit).toHaveBeenCalledWith(1);
});
});