Files
Moltbot/src/cli/gateway-cli/run.option-collisions.test.ts
2026-03-07 18:28:18 -08:00

319 lines
9.5 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { Command } from "commander";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createCliRuntimeCapture } from "../test-runtime-capture.js";
const startGatewayServer = vi.fn(async (_port: number, _opts?: unknown) => ({
close: vi.fn(async () => {}),
}));
const setGatewayWsLogStyle = vi.fn((_style: string) => undefined);
const setVerbose = vi.fn((_enabled: boolean) => undefined);
const forceFreePortAndWait = vi.fn(async (_port: number, _opts: unknown) => ({
killed: [],
waitedMs: 0,
escalatedToSigkill: false,
}));
const waitForPortBindable = vi.fn(async (_port: number, _opts?: unknown) => 0);
const ensureDevGatewayConfig = vi.fn(async (_opts?: unknown) => {});
const runGatewayLoop = vi.fn(async ({ start }: { start: () => Promise<unknown> }) => {
await start();
});
const configState = vi.hoisted(() => ({
cfg: {} as Record<string, unknown>,
snapshot: { exists: false } as Record<string, unknown>,
}));
const { runtimeErrors, defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
vi.mock("../../config/config.js", () => ({
getConfigPath: () => "/tmp/openclaw-test-missing-config.json",
loadConfig: () => configState.cfg,
readConfigFileSnapshot: async () => configState.snapshot,
resolveStateDir: () => "/tmp",
resolveGatewayPort: () => 18789,
}));
vi.mock("../../gateway/auth.js", () => ({
resolveGatewayAuth: (params: {
authConfig?: { mode?: string; token?: unknown; password?: unknown };
authOverride?: { mode?: string; token?: unknown; password?: unknown };
env?: NodeJS.ProcessEnv;
}) => {
const mode = params.authOverride?.mode ?? params.authConfig?.mode ?? "token";
const token =
(typeof params.authOverride?.token === "string" ? params.authOverride.token : undefined) ??
(typeof params.authConfig?.token === "string" ? params.authConfig.token : undefined) ??
params.env?.OPENCLAW_GATEWAY_TOKEN;
const password =
(typeof params.authOverride?.password === "string"
? params.authOverride.password
: undefined) ??
(typeof params.authConfig?.password === "string" ? params.authConfig.password : undefined) ??
params.env?.OPENCLAW_GATEWAY_PASSWORD;
return {
mode,
token,
password,
allowTailscale: false,
};
},
}));
vi.mock("../../gateway/server.js", () => ({
startGatewayServer: (port: number, opts?: unknown) => startGatewayServer(port, opts),
}));
vi.mock("../../gateway/ws-logging.js", () => ({
setGatewayWsLogStyle: (style: string) => setGatewayWsLogStyle(style),
}));
vi.mock("../../globals.js", () => ({
setVerbose: (enabled: boolean) => setVerbose(enabled),
}));
vi.mock("../../infra/gateway-lock.js", () => ({
GatewayLockError: class GatewayLockError extends Error {},
}));
vi.mock("../../infra/ports.js", () => ({
formatPortDiagnostics: () => [],
inspectPortUsage: async () => ({ status: "free" }),
}));
vi.mock("../../logging/console.js", () => ({
setConsoleSubsystemFilter: () => undefined,
setConsoleTimestampPrefix: () => undefined,
}));
vi.mock("../../logging/subsystem.js", () => ({
createSubsystemLogger: () => ({
info: () => undefined,
warn: () => undefined,
error: () => undefined,
}),
}));
vi.mock("../../runtime.js", () => ({
defaultRuntime,
}));
vi.mock("../command-format.js", () => ({
formatCliCommand: (cmd: string) => cmd,
}));
vi.mock("../ports.js", () => ({
forceFreePortAndWait: (port: number, opts: unknown) => forceFreePortAndWait(port, opts),
waitForPortBindable: (port: number, opts?: unknown) => waitForPortBindable(port, opts),
}));
vi.mock("./dev.js", () => ({
ensureDevGatewayConfig: (opts?: unknown) => ensureDevGatewayConfig(opts),
}));
vi.mock("./run-loop.js", () => ({
runGatewayLoop: (params: { start: () => Promise<unknown> }) => runGatewayLoop(params),
}));
describe("gateway run option collisions", () => {
let addGatewayRunCommand: typeof import("./run.js").addGatewayRunCommand;
let sharedProgram: Command;
beforeAll(async () => {
({ addGatewayRunCommand } = await import("./run.js"));
sharedProgram = new Command();
sharedProgram.exitOverride();
const gateway = addGatewayRunCommand(sharedProgram.command("gateway"));
addGatewayRunCommand(gateway.command("run"));
});
beforeEach(() => {
resetRuntimeCapture();
configState.cfg = {};
configState.snapshot = { exists: false };
startGatewayServer.mockClear();
setGatewayWsLogStyle.mockClear();
setVerbose.mockClear();
forceFreePortAndWait.mockClear();
waitForPortBindable.mockClear();
ensureDevGatewayConfig.mockClear();
runGatewayLoop.mockClear();
});
async function runGatewayCli(argv: string[]) {
await sharedProgram.parseAsync(argv, { from: "user" });
}
function expectAuthOverrideMode(mode: string) {
expect(startGatewayServer).toHaveBeenCalledWith(
18789,
expect.objectContaining({
auth: expect.objectContaining({
mode,
}),
}),
);
}
it("forwards parent-captured options to `gateway run` subcommand", async () => {
await runGatewayCli([
"gateway",
"run",
"--token",
"tok_run",
"--allow-unconfigured",
"--ws-log",
"full",
"--force",
]);
expect(forceFreePortAndWait).toHaveBeenCalledWith(18789, expect.anything());
expect(waitForPortBindable).toHaveBeenCalledWith(
18789,
expect.objectContaining({ host: "127.0.0.1" }),
);
expect(setGatewayWsLogStyle).toHaveBeenCalledWith("full");
expect(startGatewayServer).toHaveBeenCalledWith(
18789,
expect.objectContaining({
auth: expect.objectContaining({
token: "tok_run",
}),
}),
);
});
it("starts gateway when token mode has no configured token (startup bootstrap path)", async () => {
await runGatewayCli(["gateway", "run", "--allow-unconfigured"]);
expect(startGatewayServer).toHaveBeenCalledWith(
18789,
expect.objectContaining({
bind: "loopback",
}),
);
});
it("accepts --auth none override", async () => {
await runGatewayCli(["gateway", "run", "--auth", "none", "--allow-unconfigured"]);
expectAuthOverrideMode("none");
});
it("accepts --auth trusted-proxy override", async () => {
await runGatewayCli(["gateway", "run", "--auth", "trusted-proxy", "--allow-unconfigured"]);
expectAuthOverrideMode("trusted-proxy");
});
it("prints all supported modes on invalid --auth value", async () => {
await expect(
runGatewayCli(["gateway", "run", "--auth", "bad-mode", "--allow-unconfigured"]),
).rejects.toThrow("__exit__:1");
expect(runtimeErrors).toContain(
'Invalid --auth (use "none", "token", "password", or "trusted-proxy")',
);
});
it("allows password mode preflight when password is configured via SecretRef", async () => {
configState.cfg = {
gateway: {
auth: {
mode: "password",
password: { source: "env", provider: "default", id: "OPENCLAW_GATEWAY_PASSWORD" },
},
},
secrets: {
defaults: {
env: "default",
},
},
};
configState.snapshot = { exists: true, parsed: configState.cfg };
await runGatewayCli(["gateway", "run", "--allow-unconfigured"]);
expect(startGatewayServer).toHaveBeenCalledWith(
18789,
expect.objectContaining({
bind: "loopback",
}),
);
});
it("reads gateway password from --password-file", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-run-"));
try {
const passwordFile = path.join(tempDir, "gateway-password.txt");
await fs.writeFile(passwordFile, "pw_from_file\n", "utf8");
await runGatewayCli([
"gateway",
"run",
"--auth",
"password",
"--password-file",
passwordFile,
"--allow-unconfigured",
]);
expect(startGatewayServer).toHaveBeenCalledWith(
18789,
expect.objectContaining({
auth: expect.objectContaining({
mode: "password",
password: "pw_from_file", // pragma: allowlist secret
}),
}),
);
expect(runtimeErrors).not.toContain(
"Warning: --password can be exposed via process listings. Prefer --password-file or OPENCLAW_GATEWAY_PASSWORD.",
);
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
it("warns when gateway password is passed inline", async () => {
await runGatewayCli([
"gateway",
"run",
"--auth",
"password",
"--password",
"pw_inline",
"--allow-unconfigured",
]);
expect(runtimeErrors).toContain(
"Warning: --password can be exposed via process listings. Prefer --password-file or OPENCLAW_GATEWAY_PASSWORD.",
);
});
it("rejects using both --password and --password-file", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-run-"));
try {
const passwordFile = path.join(tempDir, "gateway-password.txt");
await fs.writeFile(passwordFile, "pw_from_file\n", "utf8");
await expect(
runGatewayCli([
"gateway",
"run",
"--password",
"pw_inline",
"--password-file",
passwordFile,
"--allow-unconfigured",
]),
).rejects.toThrow("__exit__:1");
expect(runtimeErrors).toContain("Use either --password or --password-file.");
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
});