import { describe, expect, it } from "vitest"; import { buildParseArgv, getFlagValue, getCommandPath, getCommandPositionalsWithRootOptions, getCommandPathWithRootOptions, getPrimaryCommand, getPositiveIntFlagValue, getVerboseFlag, hasHelpOrVersion, hasFlag, isRootHelpInvocation, isRootVersionInvocation, shouldMigrateState, shouldMigrateStateFromPath, } from "./argv.js"; describe("argv helpers", () => { it.each([ { name: "help flag", argv: ["node", "openclaw", "--help"], expected: true, }, { name: "version flag", argv: ["node", "openclaw", "-V"], expected: true, }, { name: "normal command", argv: ["node", "openclaw", "status"], expected: false, }, { name: "root -v alias", argv: ["node", "openclaw", "-v"], expected: true, }, { name: "root -v alias with profile", argv: ["node", "openclaw", "--profile", "work", "-v"], expected: true, }, { name: "root -v alias with log-level", argv: ["node", "openclaw", "--log-level", "debug", "-v"], expected: true, }, { name: "subcommand -v should not be treated as version", argv: ["node", "openclaw", "acp", "-v"], expected: false, }, { name: "root -v alias with equals profile", argv: ["node", "openclaw", "--profile=work", "-v"], expected: true, }, { name: "subcommand path after global root flags should not be treated as version", argv: ["node", "openclaw", "--dev", "skills", "list", "-v"], expected: false, }, ])("detects help/version flags: $name", ({ argv, expected }) => { expect(hasHelpOrVersion(argv)).toBe(expected); }); it.each([ { name: "root --version", argv: ["node", "openclaw", "--version"], expected: true, }, { name: "root -V", argv: ["node", "openclaw", "-V"], expected: true, }, { name: "root -v alias with profile", argv: ["node", "openclaw", "--profile", "work", "-v"], expected: true, }, { name: "subcommand version flag", argv: ["node", "openclaw", "status", "--version"], expected: false, }, { name: "unknown root flag with version", argv: ["node", "openclaw", "--unknown", "--version"], expected: false, }, ])("detects root-only version invocations: $name", ({ argv, expected }) => { expect(isRootVersionInvocation(argv)).toBe(expected); }); it.each([ { name: "root --help", argv: ["node", "openclaw", "--help"], expected: true, }, { name: "root -h", argv: ["node", "openclaw", "-h"], expected: true, }, { name: "root --help with profile", argv: ["node", "openclaw", "--profile", "work", "--help"], expected: true, }, { name: "subcommand --help", argv: ["node", "openclaw", "status", "--help"], expected: false, }, { name: "help before subcommand token", argv: ["node", "openclaw", "--help", "status"], expected: false, }, { name: "help after -- terminator", argv: ["node", "openclaw", "nodes", "run", "--", "git", "--help"], expected: false, }, { name: "unknown root flag before help", argv: ["node", "openclaw", "--unknown", "--help"], expected: false, }, { name: "unknown root flag after help", argv: ["node", "openclaw", "--help", "--unknown"], expected: false, }, ])("detects root-only help invocations: $name", ({ argv, expected }) => { expect(isRootHelpInvocation(argv)).toBe(expected); }); it.each([ { name: "single command with trailing flag", argv: ["node", "openclaw", "status", "--json"], expected: ["status"], }, { name: "two-part command", argv: ["node", "openclaw", "agents", "list"], expected: ["agents", "list"], }, { name: "terminator cuts parsing", argv: ["node", "openclaw", "status", "--", "ignored"], expected: ["status"], }, ])("extracts command path: $name", ({ argv, expected }) => { expect(getCommandPath(argv, 2)).toEqual(expected); }); it("extracts command path while skipping known root option values", () => { expect( getCommandPathWithRootOptions( ["node", "openclaw", "--profile", "work", "--no-color", "config", "validate"], 2, ), ).toEqual(["config", "validate"]); }); it("extracts routed config get positionals with interleaved root options", () => { expect( getCommandPositionalsWithRootOptions( ["node", "openclaw", "config", "get", "--log-level", "debug", "update.channel", "--json"], { commandPath: ["config", "get"], booleanFlags: ["--json"], }, ), ).toEqual(["update.channel"]); }); it("extracts routed config unset positionals with interleaved root options", () => { expect( getCommandPositionalsWithRootOptions( ["node", "openclaw", "config", "unset", "--profile", "work", "update.channel"], { commandPath: ["config", "unset"], }, ), ).toEqual(["update.channel"]); }); it("returns null when routed command sees unknown options", () => { expect( getCommandPositionalsWithRootOptions( ["node", "openclaw", "config", "get", "--mystery", "value", "update.channel"], { commandPath: ["config", "get"], booleanFlags: ["--json"], }, ), ).toBeNull(); }); it.each([ { name: "returns first command token", argv: ["node", "openclaw", "agents", "list"], expected: "agents", }, { name: "returns null when no command exists", argv: ["node", "openclaw"], expected: null, }, { name: "skips known root option values", argv: ["node", "openclaw", "--log-level", "debug", "status"], expected: "status", }, ])("returns primary command: $name", ({ argv, expected }) => { expect(getPrimaryCommand(argv)).toBe(expected); }); it.each([ { name: "detects flag before terminator", argv: ["node", "openclaw", "status", "--json"], flag: "--json", expected: true, }, { name: "ignores flag after terminator", argv: ["node", "openclaw", "--", "--json"], flag: "--json", expected: false, }, ])("parses boolean flags: $name", ({ argv, flag, expected }) => { expect(hasFlag(argv, flag)).toBe(expected); }); it.each([ { name: "value in next token", argv: ["node", "openclaw", "status", "--timeout", "5000"], expected: "5000", }, { name: "value in equals form", argv: ["node", "openclaw", "status", "--timeout=2500"], expected: "2500", }, { name: "missing value", argv: ["node", "openclaw", "status", "--timeout"], expected: null, }, { name: "next token is another flag", argv: ["node", "openclaw", "status", "--timeout", "--json"], expected: null, }, { name: "flag appears after terminator", argv: ["node", "openclaw", "--", "--timeout=99"], expected: undefined, }, ])("extracts flag values: $name", ({ argv, expected }) => { expect(getFlagValue(argv, "--timeout")).toBe(expected); }); it("parses verbose flags", () => { expect(getVerboseFlag(["node", "openclaw", "status", "--verbose"])).toBe(true); expect(getVerboseFlag(["node", "openclaw", "status", "--debug"])).toBe(false); expect(getVerboseFlag(["node", "openclaw", "status", "--debug"], { includeDebug: true })).toBe( true, ); }); it.each([ { name: "missing flag", argv: ["node", "openclaw", "status"], expected: undefined, }, { name: "missing value", argv: ["node", "openclaw", "status", "--timeout"], expected: null, }, { name: "valid positive integer", argv: ["node", "openclaw", "status", "--timeout", "5000"], expected: 5000, }, { name: "invalid integer", argv: ["node", "openclaw", "status", "--timeout", "nope"], expected: undefined, }, ])("parses positive integer flag values: $name", ({ argv, expected }) => { expect(getPositiveIntFlagValue(argv, "--timeout")).toBe(expected); }); it("builds parse argv from raw args", () => { const cases = [ { rawArgs: ["node", "openclaw", "status"], expected: ["node", "openclaw", "status"], }, { rawArgs: ["node-22", "openclaw", "status"], expected: ["node-22", "openclaw", "status"], }, { rawArgs: ["node-22.2.0.exe", "openclaw", "status"], expected: ["node-22.2.0.exe", "openclaw", "status"], }, { rawArgs: ["node-22.2", "openclaw", "status"], expected: ["node-22.2", "openclaw", "status"], }, { rawArgs: ["node-22.2.exe", "openclaw", "status"], expected: ["node-22.2.exe", "openclaw", "status"], }, { rawArgs: ["/usr/bin/node-22.2.0", "openclaw", "status"], expected: ["/usr/bin/node-22.2.0", "openclaw", "status"], }, { rawArgs: ["node24", "openclaw", "status"], expected: ["node24", "openclaw", "status"], }, { rawArgs: ["/usr/bin/node24", "openclaw", "status"], expected: ["/usr/bin/node24", "openclaw", "status"], }, { rawArgs: ["node24.exe", "openclaw", "status"], expected: ["node24.exe", "openclaw", "status"], }, { rawArgs: ["nodejs", "openclaw", "status"], expected: ["nodejs", "openclaw", "status"], }, { rawArgs: ["node-dev", "openclaw", "status"], expected: ["node", "openclaw", "node-dev", "openclaw", "status"], }, { rawArgs: ["openclaw", "status"], expected: ["node", "openclaw", "status"], }, { rawArgs: ["bun", "src/entry.ts", "status"], expected: ["bun", "src/entry.ts", "status"], }, ] as const; for (const testCase of cases) { const parsed = buildParseArgv({ programName: "openclaw", rawArgs: [...testCase.rawArgs], }); expect(parsed).toEqual([...testCase.expected]); } }); it("builds parse argv from fallback args", () => { const fallbackArgv = buildParseArgv({ programName: "openclaw", fallbackArgv: ["status"], }); expect(fallbackArgv).toEqual(["node", "openclaw", "status"]); }); it("decides when to migrate state", () => { const nonMutatingArgv = [ ["node", "openclaw", "status"], ["node", "openclaw", "health"], ["node", "openclaw", "sessions"], ["node", "openclaw", "config", "get", "update"], ["node", "openclaw", "config", "unset", "update"], ["node", "openclaw", "models", "list"], ["node", "openclaw", "models", "status"], ["node", "openclaw", "memory", "status"], ["node", "openclaw", "agent", "--message", "hi"], ] as const; const mutatingArgv = [ ["node", "openclaw", "agents", "list"], ["node", "openclaw", "message", "send"], ] as const; for (const argv of nonMutatingArgv) { expect(shouldMigrateState([...argv])).toBe(false); } for (const argv of mutatingArgv) { expect(shouldMigrateState([...argv])).toBe(true); } }); it.each([ { path: ["status"], expected: false }, { path: ["config", "get"], expected: false }, { path: ["models", "status"], expected: false }, { path: ["agents", "list"], expected: true }, ])("reuses command path for migrate state decisions: $path", ({ path, expected }) => { expect(shouldMigrateStateFromPath(path)).toBe(expected); }); });