fix(cli): stop pairing list crashing with empty channel enum (#98142)

When no chat DM pairing channels are configured, `openclaw pairing list`
(no channel argument) threw `Channel required ... (expected one of: )`
with an empty enum that reads like a bug. Users who hit this are usually
trying to approve a TUI/device or scope-upgrade request, which lives under
`openclaw devices`, not `openclaw pairing` (which only handles chat DM
pairing).

- Guard the channel hint in help text and errors so an empty channel list
  no longer renders a bare `()` / `(expected one of: )`.
- When no pairing channels exist, redirect to `openclaw devices list` /
  `openclaw devices approve` instead of failing opaquely.

AI-assisted (Claude Code).
This commit is contained in:
Dallin Romney
2026-06-30 14:15:19 -07:00
committed by GitHub
parent b885c81479
commit 44ec7580e2
2 changed files with 37 additions and 6 deletions

View File

@@ -224,6 +224,30 @@ describe("pairing cli", () => {
expect(listChannelPairingRequests).toHaveBeenCalledWith("slack");
});
it("redirects to openclaw devices when no pairing channels are configured", async () => {
listPairingChannels.mockReturnValueOnce([]);
const error = await runPairing(["pairing", "list"]).then(
() => null,
(err: unknown) => err,
);
expect(error).toBeInstanceOf(Error);
const message = (error as Error).message;
expect(message).toContain("openclaw devices");
// Must not leak the empty enum that originally read like a bug.
expect(message).not.toContain("expected one of: )");
expect(message).not.toContain("()");
expect(listChannelPairingRequests).not.toHaveBeenCalled();
});
it("lists supported channels when one is required but omitted", async () => {
// Multiple channels configured (default mock) + no channel argument.
await expect(runPairing(["pairing", "list"])).rejects.toThrow(
"expected one of: telegram, discord, imessage",
);
});
it("accepts channel as positional for approve (npm-run compatible)", async () => {
mockApprovedPairing();

View File

@@ -88,6 +88,8 @@ async function maybeBootstrapCommandOwnerFromPairing(params: {
export function registerPairingCli(program: Command) {
const channels = listPairingChannels();
// Avoid rendering a bare "()" enum when no channels are configured.
const channelHint = channels.length > 0 ? channels.join(", ") : "none configured";
const pairing = program
.command("pairing")
.description("Secure DM pairing (approve inbound requests)")
@@ -100,16 +102,21 @@ export function registerPairingCli(program: Command) {
pairing
.command("list")
.description("List pending pairing requests")
.option("--channel <channel>", `Channel (${channels.join(", ")})`)
.option("--channel <channel>", `Channel (${channelHint})`)
.option("--account <accountId>", "Account id (for multi-account channels)")
.argument("[channel]", `Channel (${channels.join(", ")})`)
.argument("[channel]", `Channel (${channelHint})`)
.option("--json", "Print JSON", false)
.action(async (channelArg, opts) => {
const channelRaw = opts.channel ?? channelArg ?? (channels.length === 1 ? channels[0] : "");
if (!channelRaw) {
throw new Error(
`Channel required. Use --channel <channel> or pass it as the first argument (expected one of: ${channels.join(", ")})`,
);
if (channels.length === 0) {
// `pairing` is chat DM only; TUI/device approvals live under `openclaw devices`.
throw new Error(
`No chat DM pairing channels are configured. To approve a TUI or device request, ` +
`use ${formatCliCommand("openclaw devices approve")} instead.`,
);
}
throw new Error(`Channel required (expected one of: ${channelHint}).`);
}
const channel = parseChannel(channelRaw, channels);
const accountId = normalizeStringifiedOptionalString(opts.account) ?? "";
@@ -151,7 +158,7 @@ export function registerPairingCli(program: Command) {
pairing
.command("approve")
.description("Approve a pairing code and allow that sender")
.option("--channel <channel>", `Channel (${channels.join(", ")})`)
.option("--channel <channel>", `Channel (${channelHint})`)
.option("--account <accountId>", "Account id (for multi-account channels)")
.argument("<codeOrChannel>", "Pairing code (or channel when using 2 args)")
.argument("[code]", "Pairing code (when channel is passed as the 1st arg)")