// Gateway register option collision tests cover gateway command option registration. import { Command } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { registerGatewayCli } from "./register.js"; const mocks = vi.hoisted(() => ({ callGatewayCli: vi.fn(async (method: string, _opts: unknown, _params?: unknown) => { if (method === "gateway.suspend.prepare") { return { status: "ready", suspensionId: "suspension-1", expiresAtMs: 1_800_000_000_000, activeCount: 0, blockers: [], }; } if (method === "gateway.suspend.resume") { return { ok: true, status: "running", resumed: true }; } return { ok: true }; }), emitReachableGatewayAuthDiagnostic: vi.fn(async (_params: unknown) => false), formatHealthChannelLines: vi.fn(() => []), gatewayStatusCommand: vi.fn(async (_opts: unknown, _runtime: unknown) => {}), gatewayAuthTokenCommand: vi.fn(async (_runtime: unknown) => {}), defaultRuntime: { log: vi.fn(), error: vi.fn(), writeStdout: vi.fn(), writeJson: vi.fn(), exit: vi.fn(), }, })); const { callGatewayCli, emitReachableGatewayAuthDiagnostic, gatewayStatusCommand, defaultRuntime } = mocks; vi.mock("../cli-utils.js", () => ({ runCommandWithRuntime: async ( _runtime: unknown, action: () => Promise, onError: (err: unknown) => void, ) => { try { await action(); } catch (err) { onError(err); } }, })); vi.mock("../../runtime.js", async () => ({ ...(await vi.importActual("../../runtime.js")), defaultRuntime: mocks.defaultRuntime, })); vi.mock("../../commands/gateway-status.js", () => ({ gatewayStatusCommand: (opts: unknown, runtime: unknown) => mocks.gatewayStatusCommand(opts, runtime), })); vi.mock("../../commands/gateway-auth-token.js", () => ({ gatewayAuthTokenCommand: (runtime: unknown) => mocks.gatewayAuthTokenCommand(runtime), })); vi.mock("../gateway-rpc.js", async () => ({ ...(await vi.importActual("../gateway-rpc.js")), callGatewayFromCliWithTransport: (method: string, opts: unknown, params?: unknown) => mocks.callGatewayCli(method, opts, params), })); vi.mock("./run-command.js", () => ({ addGatewayRunCommand: (cmd: Command) => cmd .option("--port ", "Port for the gateway WebSocket") .option("--token ", "Gateway token") .option("--password ", "Gateway password"), })); vi.mock("../daemon-cli/register-service-commands.js", () => ({ addGatewayServiceCommands: () => undefined, })); vi.mock("../../commands/health.js", () => ({ emitReachableGatewayAuthDiagnostic: (params: unknown) => mocks.emitReachableGatewayAuthDiagnostic(params), formatHealthChannelLines: () => mocks.formatHealthChannelLines(), readNonObservingHealthConfig: async () => ({}), })); vi.mock("../../config/read-best-effort-config.runtime.js", () => ({ readBestEffortConfig: async () => ({}), readSourceConfigBestEffort: async () => ({}), })); vi.mock("../../infra/bonjour-discovery.js", () => ({ discoverGatewayBeacons: async () => [], })); vi.mock("../../infra/widearea-dns.js", () => ({ resolveWideAreaDiscoveryDomain: () => undefined, })); vi.mock("../../../packages/terminal-core/src/health-style.js", () => ({ styleHealthChannelLine: (line: string) => line, })); vi.mock("../../../packages/terminal-core/src/links.js", () => ({ formatDocsLink: () => "docs.openclaw.ai/cli/gateway", })); vi.mock("../../../packages/terminal-core/src/theme.js", () => ({ colorize: (_rich: boolean, _fn: (value: string) => string, value: string) => value, isRich: () => false, theme: { heading: (value: string) => value, muted: (value: string) => value, success: (value: string) => value, }, })); vi.mock("../../utils/usage-format.js", () => ({ formatTokenCount: () => "0", formatUsd: () => "$0.00", })); vi.mock("../help-format.js", () => ({ formatHelpExamples: () => "", })); vi.mock("../progress.js", () => ({ withProgress: async (_opts: unknown, fn: () => Promise) => await fn(), })); vi.mock("./discover.js", () => ({ dedupeBeacons: (beacons: unknown[]) => beacons, parseDiscoverTimeoutMs: () => 2000, renderBeaconLines: () => [], })); function firstGatewayCall() { return callGatewayCli.mock.calls[0] ?? []; } function firstGatewayStatusCall() { return gatewayStatusCommand.mock.calls[0] ?? []; } function expectLocalGatewayCall(method: string, port: number, params?: unknown) { expect(defaultRuntime.error.mock.calls).toEqual([]); expect(callGatewayCli).toHaveBeenCalledTimes(1); const [actualMethod, opts, actualParams] = firstGatewayCall(); expect(actualMethod).toBe(method); if (params !== undefined) { expect(actualParams).toEqual(params); } const gatewayOpts = opts as { localPortOverride?: number } | undefined; expect(gatewayOpts?.localPortOverride).toBe(port); } describe("gateway register option collisions", () => { const sharedProgram: Command = new Command(); if (sharedProgram.commands.length === 0) { sharedProgram.exitOverride(); registerGatewayCli(sharedProgram); } beforeEach(() => { callGatewayCli.mockClear(); emitReachableGatewayAuthDiagnostic.mockClear(); mocks.formatHealthChannelLines.mockClear(); gatewayStatusCommand.mockClear(); mocks.gatewayAuthTokenCommand.mockClear(); defaultRuntime.log.mockClear(); defaultRuntime.error.mockClear(); defaultRuntime.writeStdout.mockClear(); defaultRuntime.writeJson.mockClear(); defaultRuntime.exit.mockClear(); }); it("requires explicit confirmation before revealing the Gateway token", async () => { await sharedProgram.parseAsync(["gateway", "auth-token"], { from: "user" }); expect(mocks.gatewayAuthTokenCommand).not.toHaveBeenCalled(); expect(defaultRuntime.error).toHaveBeenCalledWith( expect.stringContaining("Pass --show to confirm"), ); expect(defaultRuntime.exit).toHaveBeenCalledWith(1); }); it("routes an explicitly confirmed token reveal through the output runtime", async () => { await sharedProgram.parseAsync(["gateway", "auth-token", "--show"], { from: "user" }); expect(mocks.gatewayAuthTokenCommand).toHaveBeenCalledWith(defaultRuntime); expect(defaultRuntime.error).not.toHaveBeenCalled(); }); it.each([ { name: "forwards the expected endpoint without overriding configured routing", argv: ["gateway", "call", "chat.send", "--expect-url", "wss://gateway.example/ws", "--json"], assert: () => { expect(callGatewayCli).toHaveBeenCalledTimes(1); const [method, opts] = firstGatewayCall(); expect(method).toBe("chat.send"); expect(opts).toMatchObject({ expectUrl: "wss://gateway.example/ws" }); expect(opts).not.toHaveProperty("url"); }, }, { name: "forwards --token to gateway call when parent and child option names collide", argv: ["gateway", "call", "health", "--token", "tok_call", "--json"], assert: () => { expect(callGatewayCli).toHaveBeenCalledTimes(1); const [method, opts, params] = firstGatewayCall(); expect(method).toBe("health"); expect((opts as { token?: string } | undefined)?.token).toBe("tok_call"); expect(params).toEqual({}); }, }, { name: "gives setup detection enough transport grace", argv: ["gateway", "call", "openclaw.setup.detect", "--json"], assert: () => { const [method, opts] = firstGatewayCall(); expect(method).toBe("openclaw.setup.detect"); expect((opts as { timeout?: string } | undefined)?.timeout).toBe("40000"); }, }, { name: "projects gateway call --port into the local override", argv: ["gateway", "call", "health", "--port", "19084", "--json"], assert: () => { expectLocalGatewayCall("health", 19084, {}); }, }, { name: "inherits parent --port for gateway call", argv: ["gateway", "--port", "19085", "call", "health", "--json"], assert: () => { expectLocalGatewayCall("health", 19085); }, }, { name: "projects gateway suspend --port and request id", argv: ["gateway", "suspend", "--request-id", "host-operation", "--port", "19086", "--json"], assert: () => { expectLocalGatewayCall("gateway.suspend.prepare", 19086, { requestId: "host-operation", }); expect(defaultRuntime.writeJson).toHaveBeenCalledWith( expect.objectContaining({ status: "ready", requestId: "host-operation" }), ); }, }, { name: "preserves the custom suspend port in its human-readable resume hint", argv: ["gateway", "suspend", "--port", "19086"], assert: () => { expectLocalGatewayCall("gateway.suspend.prepare", 19086); expect(defaultRuntime.log).toHaveBeenCalledWith( "Resume with: openclaw gateway resume suspension-1 --port 19086", ); }, }, { name: "preserves an inherited suspend port in its human-readable resume hint", argv: ["gateway", "--port", "19087", "suspend"], assert: () => { expectLocalGatewayCall("gateway.suspend.prepare", 19087); expect(defaultRuntime.log).toHaveBeenCalledWith( "Resume with: openclaw gateway resume suspension-1 --port 19087", ); }, }, { name: "inherits parent --port for gateway resume", argv: ["gateway", "--port", "19087", "resume", "suspension-1", "--json"], assert: () => { expectLocalGatewayCall("gateway.suspend.resume", 19087, { suspensionId: "suspension-1", }); expect(defaultRuntime.writeJson).toHaveBeenCalledWith({ ok: true, status: "running", resumed: true, }); }, }, { name: "forwards --token to gateway probe when parent and child option names collide", argv: ["gateway", "probe", "--token", "tok_probe", "--json"], assert: () => { expect(gatewayStatusCommand).toHaveBeenCalledTimes(1); const [opts, runtime] = firstGatewayStatusCall(); expect((opts as { token?: string } | undefined)?.token).toBe("tok_probe"); expect(runtime).toBe(defaultRuntime); }, }, { name: "forwards --port to gateway probe", argv: ["gateway", "probe", "--port", "19080", "--json"], assert: () => { expect(gatewayStatusCommand).toHaveBeenCalledTimes(1); const [opts] = firstGatewayStatusCall(); expect((opts as { port?: string } | undefined)?.port).toBe("19080"); }, }, { name: "inherits parent --port for gateway probe", argv: ["gateway", "--port", "19082", "probe", "--json"], assert: () => { expect(gatewayStatusCommand).toHaveBeenCalledTimes(1); const [opts] = firstGatewayStatusCall(); expect((opts as { port?: string } | undefined)?.port).toBe("19082"); }, }, { name: "projects gateway health --port into the local override", argv: ["gateway", "health", "--port", "19081", "--json"], assert: () => { expectLocalGatewayCall("health", 19081); }, }, { name: "inherits parent --port for gateway health", argv: ["gateway", "--port", "19083", "health", "--json"], assert: () => { expectLocalGatewayCall("health", 19083); }, }, { name: "passes decimal usage-cost --days values", argv: ["gateway", "usage-cost", "--days", "7", "--json"], assert: () => { expect(callGatewayCli).toHaveBeenCalledTimes(1); const [method, _opts, params] = firstGatewayCall(); expect(method).toBe("usage.cost"); expect(params).toEqual({ days: 7 }); }, }, { name: "projects gateway usage-cost --port into the local override", argv: ["gateway", "usage-cost", "--port", "19088", "--json"], assert: () => { expectLocalGatewayCall("usage.cost", 19088, { days: 30 }); }, }, { name: "inherits parent --port for gateway usage-cost", argv: ["gateway", "--port", "19089", "usage-cost", "--json"], assert: () => { expectLocalGatewayCall("usage.cost", 19089, { days: 30 }); }, }, { name: "prefers the explicit usage-cost --port over the parent --port", argv: ["gateway", "--port", "19090", "usage-cost", "--port", "19091", "--json"], assert: () => { expectLocalGatewayCall("usage.cost", 19091, { days: 30 }); }, }, { name: "projects gateway stability --port into the local override", argv: ["gateway", "stability", "--port", "19092", "--json"], assert: () => { expectLocalGatewayCall("diagnostics.stability", 19092, { limit: 25 }); }, }, { name: "inherits parent --port for live gateway stability", argv: ["gateway", "--port", "19093", "stability", "--json"], assert: () => { expectLocalGatewayCall("diagnostics.stability", 19093, { limit: 25 }); }, }, { name: "prefers the explicit live stability --port over the parent --port", argv: ["gateway", "--port", "19094", "stability", "--port", "19095", "--json"], assert: () => { expectLocalGatewayCall("diagnostics.stability", 19095, { limit: 25 }); }, }, ])("$name", async ({ argv, assert }) => { await sharedProgram.parseAsync(argv, { from: "user" }); assert(); }); it("rejects non-decimal usage-cost --days values instead of silently defaulting", async () => { await sharedProgram.parseAsync(["gateway", "usage-cost", "--days", "1e3", "--json"], { from: "user", }); expect(callGatewayCli).not.toHaveBeenCalled(); expect(defaultRuntime.writeJson).toHaveBeenCalledWith({ ok: false, error: { type: "cli_error", message: expect.stringContaining("Invalid --days") }, }); expect(defaultRuntime.error).not.toHaveBeenCalled(); expect(defaultRuntime.exit).toHaveBeenCalledWith(1); }); it.each([ { name: "call", args: ["call", "health"], }, { name: "usage-cost", args: ["usage-cost"], }, { name: "live stability", args: ["stability"], }, ])("rejects combining --url and --port for gateway $name", async ({ args }) => { await sharedProgram.parseAsync( ["gateway", ...args, "--url", "ws://127.0.0.1:19084", "--port", "19084", "--json"], { from: "user" }, ); expect(callGatewayCli).not.toHaveBeenCalled(); expect(defaultRuntime.writeJson).toHaveBeenCalledWith({ ok: false, error: { type: "cli_error", message: "Use either --url or --port, not both." }, }); expect(defaultRuntime.error).not.toHaveBeenCalled(); expect(defaultRuntime.exit).toHaveBeenCalledWith(1); }); it("uses the effective local port and timeout for gateway health auth diagnostics", async () => { const authError = new Error("gateway auth required"); callGatewayCli.mockRejectedValueOnce(authError); emitReachableGatewayAuthDiagnostic.mockResolvedValueOnce(true); await sharedProgram.parseAsync( ["gateway", "health", "--port", "19081", "--timeout", "1234", "--json"], { from: "user" }, ); expect(emitReachableGatewayAuthDiagnostic).toHaveBeenCalledTimes(1); expect(emitReachableGatewayAuthDiagnostic).toHaveBeenCalledWith({ error: authError, config: {}, runtime: defaultRuntime, timeoutMs: 1234, token: undefined, password: undefined, localPortOverride: 19081, json: true, }); }); it("defers health presentation imports for successful JSON output", async () => { const program = new Command(); program.exitOverride(); const loadGatewayHealthModule = vi.fn(async () => ({ emitReachableGatewayAuthDiagnostic: mocks.emitReachableGatewayAuthDiagnostic, formatHealthChannelLines: mocks.formatHealthChannelLines, })); const loadHealthStyleModule = vi.fn(async () => ({ styleHealthChannelLine: (line: string) => line, })); registerGatewayCli(program, { loadGatewayHealthModule: loadGatewayHealthModule as never, loadHealthStyleModule: loadHealthStyleModule as never, }); await program.parseAsync(["node", "openclaw", "gateway", "health", "--json"]); expect(callGatewayCli).toHaveBeenCalledWith( "health", expect.objectContaining({ json: true }), undefined, ); expect(loadGatewayHealthModule).not.toHaveBeenCalled(); expect(loadHealthStyleModule).not.toHaveBeenCalled(); expect(defaultRuntime.writeJson).toHaveBeenCalledWith({ ok: true }); }); });