File size: 6,741 Bytes
34810d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
// Argv invocation tests cover CLI argv normalization before command dispatch.
import { Command } from "commander";
import { describe, expect, it } from "vitest";
import { resolveCliArgvInvocation } from "./argv-invocation.js";
import { getCommanderCommandPath } from "./program/commander-parse-facts.js";

describe("argv-invocation", () => {
  it("resolves root help and empty command path", () => {
    expect(resolveCliArgvInvocation(["node", "openclaw", "--help"])).toEqual({
      argv: ["node", "openclaw", "--help"],
      commandPath: [],
      primary: null,
      hasHelpOrVersion: true,
      isRootHelpInvocation: true,
    });
  });

  it("resolves command path and primary with root options", () => {
    expect(
      resolveCliArgvInvocation(["node", "openclaw", "--profile", "work", "gateway", "status"]),
    ).toEqual({
      argv: ["node", "openclaw", "--profile", "work", "gateway", "status"],
      commandPath: ["gateway", "status"],
      primary: "gateway",
      hasHelpOrVersion: false,
      isRootHelpInvocation: false,
    });
  });

  it.each([
    {
      name: "version-pinned install",
      argv: ["node", "openclaw", "skills", "install", "@owner/weather", "--version", "1.2.3"],
      commandPath: ["skills", "install"],
    },
    {
      name: "version-pinned verification",
      argv: ["node", "openclaw", "skills", "verify", "@owner/weather", "--version", "1.2.3"],
      commandPath: ["skills", "verify"],
    },
    {
      name: "equals-form version-pinned install",
      argv: ["node", "openclaw", "skills", "install", "@owner/weather", "--version=1.2.3"],
      commandPath: ["skills", "install"],
    },
    {
      name: "profiled version-pinned verification",
      argv: [
        "node",
        "openclaw",
        "--profile",
        "work",
        "skills",
        "verify",
        "@owner/weather",
        "--version",
        "1.2.3",
      ],
      commandPath: ["skills", "verify"],
    },
  ])("keeps $name in command execution mode", ({ argv, commandPath }) => {
    expect(resolveCliArgvInvocation(argv)).toEqual({
      argv,
      commandPath,
      primary: "skills",
      hasHelpOrVersion: false,
      isRootHelpInvocation: false,
    });
  });

  it("consumes agent parent option values before the exec subcommand", () => {
    expect(
      resolveCliArgvInvocation([
        "node",
        "openclaw",
        "agent",
        "--model",
        "openai/gpt-5.6-sol",
        "exec",
        "fix it",
      ]).commandPath,
    ).toEqual(["agent", "exec"]);
  });

  it("does not treat an exec-valued parent option as the subcommand", () => {
    expect(
      resolveCliArgvInvocation(["node", "openclaw", "agent", "--message", "exec"]).commandPath,
    ).toEqual(["agent"]);
  });

  it("consumes root options between the agent parent and exec", () => {
    expect(
      resolveCliArgvInvocation([
        "node",
        "openclaw",
        "agent",
        "--no-color",
        "--model",
        "openai/gpt-5.6-sol",
        "exec",
        "fix it",
      ]).commandPath,
    ).toEqual(["agent", "exec"]);
  });

  it.each([
    ["separate agent value", ["models", "--agent", "main", "--status-json"]],
    ["inline agent value", ["models", "--agent=main", "--status-json"]],
    ["status alias before agent", ["models", "--status-json", "--agent", "main"]],
  ])("keeps models parent status options on the parent path: %s", (_name, args) => {
    expect(resolveCliArgvInvocation(["node", "openclaw", ...args]).commandPath).toEqual(["models"]);
  });

  it("still resolves a models child after parent options", () => {
    expect(
      resolveCliArgvInvocation(["node", "openclaw", "models", "--agent", "main", "status"])
        .commandPath,
    ).toEqual(["models", "status"]);
  });

  it.each([
    ["config", ["--section", "model"], ["config"]],
    ["config", ["--section", "get"], ["config"]],
    ["config", ["--section=model", "get"], ["config", "get"]],
    ["config", ["--", "get"], ["config", "get"]],
    ["skills", ["--agent", "main", "verify"], ["skills", "verify"]],
    ["skills", ["--agent=main", "verify"], ["skills", "verify"]],
    ["skills", ["--agent", "verify"], ["skills"]],
    ["skills", ["--json", "--agent", "main", "verify"], ["skills", "verify"]],
    ["skills", ["--", "verify"], ["skills", "verify"]],
  ])("matches Commander for %s %j", async (rootName, args, expectedPath) => {
    const program = new Command().name("openclaw").enablePositionalOptions();
    const root = program.command(rootName);
    if (rootName === "config") {
      root.option("--section <section>");
    } else {
      root.option("--agent <id>").option("--json");
    }
    const child = root.command(rootName === "config" ? "get" : "verify");
    let parsedPath: string[] = [];
    for (const command of [root, child]) {
      command.action(() => {
        parsedPath = getCommanderCommandPath(command);
      });
    }
    const argv = ["node", "openclaw", rootName, ...args];

    await program.parseAsync(argv);

    expect(parsedPath).toEqual(expectedPath);
    expect(resolveCliArgvInvocation(argv).commandPath).toEqual(parsedPath);
  });

  it.each(["cleanup", "status", "repair", "finalize", "wizard"])(
    "resolves update %s after parent options and interleaved root options",
    (child) => {
      for (const args of [
        ["--channel", "beta", "--tag", "latest", "--timeout", "5", child],
        ["--channel=beta", "--no-color", "--timeout=5", "--yes", child],
        ["--", child],
      ]) {
        expect(
          resolveCliArgvInvocation(["node", "openclaw", "--profile", "work", "update", ...args])
            .commandPath,
        ).toEqual(["update", child]);
      }
    },
  );

  it.each(["--channel", "--tag", "--timeout"])(
    "does not mistake a cleanup-valued %s for a child command",
    (flag) => {
      for (const args of [[flag, "cleanup"], [`${flag}=cleanup`], [flag]]) {
        expect(
          resolveCliArgvInvocation(["node", "openclaw", "update", ...args]).commandPath,
        ).toEqual(["update"]);
      }
    },
  );

  it.each([
    ["update", "--channel=beta", "cleanup", "--help"],
    ["update", "--help", "cleanup"],
    ["help", "update", "cleanup"],
  ])("recognizes update help without promoting scoped version flags: %j", (...args) => {
    expect(resolveCliArgvInvocation(["node", "openclaw", ...args]).hasHelpOrVersion).toBe(true);
  });

  it.each([
    ["update", "cleanup", "--version"],
    ["update", "cleanup", "--", "--help"],
    ["update", "--channel=--help", "cleanup"],
  ])("leaves scoped or literal help/version tokens to Commander: %j", (...args) => {
    expect(resolveCliArgvInvocation(["node", "openclaw", ...args]).hasHelpOrVersion).toBe(false);
  });
});