import { describe, expect, it } from "vitest"; import { parseArgs, ParseError, resolvePorts } from "./args"; describe("parseArgs", () => { it("defaults to the run command with no args", () => { const { options, showHelp } = parseArgs([]); expect(options.command).toBe("run"); expect(showHelp).toBe(false); }); it("parses --port and --port= as the backend port", () => { expect(parseArgs(["--port", "3000"]).options.backendPort).toBe(3000); expect(parseArgs(["--port=3000"]).options.backendPort).toBe(3000); }); it("parses --backend-port the same as --port (no deprecation note)", () => { expect(parseArgs(["--backend-port=3447"]).options.backendPort).toBe(3447); }); it("parses --web-internal-port for dev mode", () => { const r = parseArgs(["dev", "--web-internal-port", "12345"]); expect(r.options.webPort).toBe(12345); }); it("rejects --web-internal-port outside dev mode", () => { expect(() => parseArgs(["--web-internal-port", "12345"])).toThrow( /--web-internal-port only applies to dev mode/, ); }); it("rejects removed --web-port", () => { expect(() => parseArgs(["--web-port=8080"])).toThrow(/--web-port has been removed/); }); it("reports --help via showHelp without exiting", () => { expect(parseArgs(["--help"]).showHelp).toBe(true); }); it("throws ParseError when a value-taking flag has no value", () => { expect(() => parseArgs(["--port"])).toThrow(ParseError); expect(() => parseArgs(["--port"])).toThrow(/--port requires a value/); }); it("throws ParseError when the next token is another flag", () => { expect(() => parseArgs(["--port", "--debug"])).toThrow(/--port requires a value/); }); it("throws ParseError on a non-numeric port value", () => { expect(() => parseArgs(["--port=abc"])).toThrow(/--port value must be an integer/); }); it("throws ParseError on a non-integer port value", () => { expect(() => parseArgs(["--port=3000.5"])).toThrow(/--port value must be an integer/); }); it.each(["0", "-1", "65536"])("throws ParseError on out-of-range port %s", (val) => { expect(() => parseArgs([`--port=${val}`])).toThrow(/--port value must be an integer between/); }); it("throws ParseError on empty --port=", () => { expect(() => parseArgs(["--port="])).toThrow(/--port value must be an integer/); }); it("sets showVersion for --version and -V flags", () => { expect(parseArgs(["--version"]).options.showVersion).toBe(true); expect(parseArgs(["-V"]).options.showVersion).toBe(true); }); it("parses --headless and --no-browser as headless", () => { expect(parseArgs(["--headless"]).options.headless).toBe(true); expect(parseArgs(["--no-browser"]).options.headless).toBe(true); expect(parseArgs([]).options.headless).toBeUndefined(); }); it("parses --runtime-version as runtimeVersion", () => { expect(parseArgs(["--runtime-version", "v0.16.0"]).options.runtimeVersion).toBe("v0.16.0"); expect(parseArgs(["--runtime-version=v0.16.0"]).options.runtimeVersion).toBe("v0.16.0"); }); it("throws ParseError on empty --runtime-version=", () => { expect(() => parseArgs(["--runtime-version="])).toThrow(/--runtime-version requires a value/); }); it("throws ParseError when --runtime-version has no value", () => { expect(() => parseArgs(["--runtime-version"])).toThrow(ParseError); expect(() => parseArgs(["--runtime-version"])).toThrow(/--runtime-version requires a value/); }); it("throws ParseError when the next token after --runtime-version is another flag", () => { expect(() => parseArgs(["--runtime-version", "--debug"])).toThrow( /--runtime-version requires a value/, ); }); }); describe("resolvePorts", () => { it("returns undefined for both ports when nothing is set", () => { const r = resolvePorts({ command: "run" }, {} as NodeJS.ProcessEnv); expect(r).toEqual({ backendPort: undefined, webPort: undefined }); }); it("CLI backendPort wins over env vars", () => { const r = resolvePorts({ command: "start", backendPort: 3000 }, { KANDEV_BACKEND_PORT: "4000", KANDEV_PORT: "5000", } as NodeJS.ProcessEnv); expect(r.backendPort).toBe(3000); }); it("KANDEV_BACKEND_PORT wins over KANDEV_PORT (more specific env wins)", () => { const r = resolvePorts({ command: "run" }, { KANDEV_PORT: "5555", KANDEV_BACKEND_PORT: "6666", } as NodeJS.ProcessEnv); expect(r.backendPort).toBe(6666); }); it("falls back to KANDEV_PORT when KANDEV_BACKEND_PORT is not set", () => { const r = resolvePorts({ command: "run" }, { KANDEV_PORT: "5555" } as NodeJS.ProcessEnv); expect(r.backendPort).toBe(5555); }); it("KANDEV_WEB_PORT sets the internal web port in dev", () => { const r = resolvePorts({ command: "dev" }, { KANDEV_WEB_PORT: "8080" } as NodeJS.ProcessEnv); expect(r.webPort).toBe(8080); }); it("KANDEV_WEB_PORT is ignored outside dev", () => { const r = resolvePorts({ command: "run" }, { KANDEV_WEB_PORT: "8080" } as NodeJS.ProcessEnv); expect(r).toEqual({ backendPort: undefined, webPort: undefined }); }); it("--port maps to backend in every command (including dev)", () => { const r = resolvePorts({ command: "dev", backendPort: 3447 }, {} as NodeJS.ProcessEnv); expect(r).toEqual({ backendPort: 3447, webPort: undefined }); }); it("throws ParseError when KANDEV_PORT is not a number", () => { expect(() => resolvePorts({ command: "run" }, { KANDEV_PORT: "abc" } as NodeJS.ProcessEnv), ).toThrow(ParseError); }); it("throws ParseError when KANDEV_PORT is a float", () => { expect(() => resolvePorts({ command: "run" }, { KANDEV_PORT: "3000.5" } as NodeJS.ProcessEnv), ).toThrow(/KANDEV_PORT must be an integer/); }); it.each(["0", "-1", "65536"])("throws ParseError when KANDEV_PORT is out-of-range %s", (val) => { expect(() => resolvePorts({ command: "run" }, { KANDEV_PORT: val } as NodeJS.ProcessEnv), ).toThrow(/KANDEV_PORT must be an integer between/); }); it.each(["KANDEV_PORT", "KANDEV_BACKEND_PORT"])( "throws ParseError when %s is set to empty string", (name) => { expect(() => resolvePorts({ command: "run" }, { [name]: "" } as NodeJS.ProcessEnv)).toThrow( new RegExp(`${name} must be an integer`), ); }, ); it("throws ParseError when KANDEV_WEB_PORT is empty in dev", () => { expect(() => resolvePorts({ command: "dev" }, { KANDEV_WEB_PORT: "" } as NodeJS.ProcessEnv), ).toThrow(/KANDEV_WEB_PORT must be an integer/); }); });