CLI: fix root-option parsing for routed config paths

This commit is contained in:
Gustavo Madeira Santana
2026-03-02 19:14:28 -05:00
parent b799c9e22b
commit feb7216b0f
3 changed files with 68 additions and 2 deletions

View File

@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Gateway/Control UI basePath webhook passthrough: let non-read methods under configured `controlUiBasePath` fall through to plugin routes (instead of returning Control UI 405), restoring webhook handlers behind basePath mounts. (#32311) Thanks @ademczuk.
- CLI/route-first config path parsing: ignore split root-option values when extracting routed `config get/unset` positional args so invocations like `openclaw --log-level debug config get <path>` and `--profile <name> config unset <path>` resolve the intended config key. (#32050) thanks @gumadeiras.
- Voice-call/Twilio signature verification: retry signature validation across deterministic URL port variants (with/without port) to handle mixed Twilio signing behavior behind reverse proxies and non-standard ports. (#25140) Thanks @drvoss.
- Hooks/webhook ACK compatibility: return `200` (instead of `202`) for successful `/hooks/agent` requests so providers that require `200` (for example Forward Email) accept dispatched agent hook deliveries. (#28204) Thanks @Glucksberg.
- Voice-call/Twilio external outbound: auto-register webhook-first `outbound-api` calls (initiated outside OpenClaw) so media streams are accepted and call direction metadata stays accurate. (#31181) Thanks @scoootscooob.

View File

@@ -1,7 +1,19 @@
import { describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { findRoutedCommand } from "./routes.js";
const runConfigGetMock = vi.hoisted(() => vi.fn(async () => {}));
const runConfigUnsetMock = vi.hoisted(() => vi.fn(async () => {}));
vi.mock("../config-cli.js", () => ({
runConfigGet: runConfigGetMock,
runConfigUnset: runConfigUnsetMock,
}));
describe("program routes", () => {
beforeEach(() => {
vi.clearAllMocks();
});
function expectRoute(path: string[]) {
const route = findRoutedCommand(path);
expect(route).not.toBeNull();
@@ -58,6 +70,31 @@ describe("program routes", () => {
await expectRunFalse(["config", "unset"], ["node", "openclaw", "config", "unset"]);
});
it("passes config get path correctly when root option values precede command", async () => {
const route = expectRoute(["config", "get"]);
await expect(
route?.run([
"node",
"openclaw",
"--log-level",
"debug",
"config",
"get",
"update.channel",
"--json",
]),
).resolves.toBe(true);
expect(runConfigGetMock).toHaveBeenCalledWith({ path: "update.channel", json: true });
});
it("passes config unset path correctly when root option values precede command", async () => {
const route = expectRoute(["config", "unset"]);
await expect(
route?.run(["node", "openclaw", "--profile", "work", "config", "unset", "update.channel"]),
).resolves.toBe(true);
expect(runConfigUnsetMock).toHaveBeenCalledWith({ path: "update.channel" });
});
it("returns false for memory status route when --agent value is missing", async () => {
await expectRunFalse(["memory", "status"], ["node", "openclaw", "memory", "status", "--agent"]);
});

View File

@@ -99,16 +99,44 @@ const routeMemoryStatus: RouteSpec = {
},
};
function isValueToken(arg: string | undefined): boolean {
if (!arg || arg === "--") {
return false;
}
if (!arg.startsWith("-")) {
return true;
}
return /^-\d+(?:\.\d+)?$/.test(arg);
}
function getCommandPositionals(argv: string[]): string[] {
const out: string[] = [];
const args = argv.slice(2);
for (const arg of args) {
let commandStarted = false;
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (!arg || arg === "--") {
break;
}
if (!commandStarted) {
if (arg.startsWith("--profile=") || arg.startsWith("--log-level=")) {
continue;
}
if (arg === "--dev" || arg === "--no-color") {
continue;
}
if (arg === "--profile" || arg === "--log-level") {
const next = args[i + 1];
if (isValueToken(next)) {
i += 1;
}
continue;
}
}
if (arg.startsWith("-")) {
continue;
}
commandStarted = true;
out.push(arg);
}
return out;