1import { describe, expect, it } from "vite-plus/test";
2
3import { parseCliArgs, tokenizeCliArgs } from "./cliArgs.ts";
4
5describe("tokenizeCliArgs", () => {
6 it("preserves escaped double quotes outside quoted values", () => {
7 expect(tokenizeCliArgs(String.raw`--config model=\"gpt-6.1-sol\" --strict-config`)).toEqual([
8 "--config",
9 'model="gpt-6.1-sol"',
10 "--strict-config",
11 ]);
12 });
13
14 it("preserves escaped single quotes outside quoted values", () => {
15 expect(tokenizeCliArgs(String.raw`--append-system-prompt don\'t --verbose`)).toEqual([
16 "--append-system-prompt",
17 "don't",
18 "--verbose",
19 ]);
20 });
21
22 it("preserves quoted values and escaped spaces", () => {
23 expect(
24 tokenizeCliArgs(
25 String.raw`--config model="gpt 5" --enable foo\ bar --config=profile='work profile'`,
26 ),
27 ).toEqual(["--config", "model=gpt 5", "--enable", "foo bar", "--config=profile=work profile"]);
28
29 for (const whitespace of [" ", "\t"]) {
30 const args = `--add-dir /tmp/project\\${whitespace}`;
31 expect(tokenizeCliArgs(args)).toEqual(["--add-dir", `/tmp/project${whitespace}`]);
32 expect(tokenizeCliArgs(`${args} --verbose`)).toEqual([
33 "--add-dir",
34 `/tmp/project${whitespace}`,
35 "--verbose",
36 ]);
37 }
38 });
39
40 it("ignores unescaped surrounding whitespace", () => {
41 expect(tokenizeCliArgs(" \t\n")).toEqual([]);
42 expect(tokenizeCliArgs(" \t--add-dir /tmp/project \t\n")).toEqual([
43 "--add-dir",
44 "/tmp/project",
45 ]);
46 });
47
48 it("preserves literal backslashes in path values", () => {
49 expect(
50 tokenizeCliArgs(String.raw`--config cacheDir=C:\Users\me --config "quoted=C:\Users\me"`),
51 ).toEqual([
52 "--config",
53 String.raw`cacheDir=C:\Users\me`,
54 "--config",
55 String.raw`quoted=C:\Users\me`,
56 ]);
57 });
58});
59
60describe("parseCliArgs", () => {
61 it("parses --key=value mixed with boolean flags", () => {
62 expect(parseCliArgs("--chrome --model=claude-sonnet-4-6 --debug")).toEqual({
63 flags: { chrome: null, model: "claude-sonnet-4-6", debug: null },
64 positionals: [],
65 });
66 });
67
68 it("collects positionals mixed with flags (argv array)", () => {
69 expect(parseCliArgs(["1.2.3", "--root", "/path", "--github-output"])).toEqual({
70 flags: { root: "/path", "github-output": null },
71 positionals: ["1.2.3"],
72 });
73 });
74
75 it("accepts a trailing -- without operands", () => {
76 expect(parseCliArgs("--")).toEqual({ flags: {}, positionals: [] });
77 });
78
79 it.each([
80 { args: "--effort low -- --effort high --chrome" },
81 { args: ["--effort", "low", "--", "--effort", "high", "--chrome"] },
82 ])("ends option parsing at -- for $args", ({ args }) => {
83 expect(parseCliArgs(args)).toEqual({
84 flags: { effort: "low" },
85 positionals: ["--effort", "high", "--chrome"],
86 });
87 });
88
89 it("preserves all operands after --, including empty strings and another --", () => {
90 expect(parseCliArgs(["--", "", "--", "--model=example", "-x"])).toEqual({
91 flags: {},
92 positionals: ["", "--", "--model=example", "-x"],
93 });
94 });
95
96 it("ends option parsing with explicit boolean flags too", () => {
97 expect(parseCliArgs(["--chrome", "--", "--verbose"], { booleanFlags: ["chrome"] })).toEqual({
98 flags: { chrome: null },
99 positionals: ["--verbose"],
100 });
101 });
102
103 it("keeps -- inside an equals-form option value", () => {
104 expect(parseCliArgs("--append-system-prompt=-- --chrome")).toEqual({
105 flags: { "append-system-prompt": "--", chrome: null },
106 positionals: [],
107 });
108 });
109
110 it("boolean flag does not consume next token as value", () => {
111 expect(parseCliArgs(["--github-output", "1.2.3"], { booleanFlags: ["github-output"] })).toEqual(
112 {
113 flags: { "github-output": null },
114 positionals: ["1.2.3"],
115 },
116 );
117 });
118});