fix(cli): reject invalid node run port (#84307)

Co-authored-by: Gio Della-Libera <giodl@microsoft.com>
This commit is contained in:
Gio Della-Libera
2026-05-21 13:47:28 -07:00
committed by GitHub
parent 7f4462e5c0
commit 8961eae3f0
3 changed files with 80 additions and 7 deletions

View File

@@ -1,8 +1,16 @@
import { Command } from "commander";
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { registerNodeCli } from "./register.js";
type LoadNodeHostConfig = typeof import("../../node-host/config.js").loadNodeHostConfig;
const daemonMocks = vi.hoisted(() => ({
defaultRuntime: {
error: vi.fn(),
exit: vi.fn(),
},
loadNodeHostConfig: vi.fn<LoadNodeHostConfig>(async () => null),
runNodeHost: vi.fn(),
runNodeDaemonInstall: vi.fn(),
runNodeDaemonRestart: vi.fn(),
runNodeDaemonStart: vi.fn(),
@@ -14,11 +22,15 @@ const daemonMocks = vi.hoisted(() => ({
vi.mock("./daemon.js", () => daemonMocks);
vi.mock("../../node-host/config.js", () => ({
loadNodeHostConfig: vi.fn(async () => null),
loadNodeHostConfig: daemonMocks.loadNodeHostConfig,
}));
vi.mock("../../node-host/runner.js", () => ({
runNodeHost: vi.fn(),
runNodeHost: daemonMocks.runNodeHost,
}));
vi.mock("../../runtime.js", () => ({
defaultRuntime: daemonMocks.defaultRuntime,
}));
function createProgram(): Command {
@@ -33,6 +45,20 @@ function createProgram(): Command {
}
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("registers node start for the macOS app node service manager", async () => {
const program = createProgram();
@@ -40,4 +66,41 @@ describe("registerNodeCli", () => {
expect(daemonMocks.runNodeDaemonStart.mock.calls[0]?.[0]?.json).toBe(true);
});
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("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 }),
);
});
});

View File

@@ -1,10 +1,12 @@
import type { Command } from "commander";
import { loadNodeHostConfig } from "../../node-host/config.js";
import { runNodeHost } from "../../node-host/runner.js";
import { defaultRuntime } from "../../runtime.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
import { parsePort } from "../daemon-cli/shared.js";
import { formatInvalidPortOption } from "../error-format.js";
import { formatHelpExamples } from "../help-format.js";
import {
runNodeDaemonInstall,
@@ -15,9 +17,11 @@ import {
runNodeDaemonUninstall,
} from "./daemon.js";
function parsePortWithFallback(value: unknown, fallback: number): number {
const parsed = parsePort(value);
return parsed ?? fallback;
function parsePortOption(value: unknown, fallback: number): number | null {
if (value === undefined) {
return fallback;
}
return parsePort(value);
}
export function registerNodeCli(program: Command) {
@@ -54,7 +58,12 @@ export function registerNodeCli(program: Command) {
normalizeOptionalString(opts.host as string | undefined) ||
existing?.gateway?.host ||
"127.0.0.1";
const port = parsePortWithFallback(opts.port, existing?.gateway?.port ?? 18789);
const port = parsePortOption(opts.port, existing?.gateway?.port ?? 18789);
if (port === null) {
defaultRuntime.error(formatInvalidPortOption("--port"));
defaultRuntime.exit(1);
return;
}
await runNodeHost({
gatewayHost: host,
gatewayPort: port,