Skip to content

Commit 44ec758

Browse files
authored
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).
1 parent b885c81 commit 44ec758

2 files changed

Lines changed: 37 additions & 6 deletions

File tree

src/cli/pairing-cli.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,30 @@ describe("pairing cli", () => {
224224
expect(listChannelPairingRequests).toHaveBeenCalledWith("slack");
225225
});
226226

227+
it("redirects to openclaw devices when no pairing channels are configured", async () => {
228+
listPairingChannels.mockReturnValueOnce([]);
229+
230+
const error = await runPairing(["pairing", "list"]).then(
231+
() => null,
232+
(err: unknown) => err,
233+
);
234+
235+
expect(error).toBeInstanceOf(Error);
236+
const message = (error as Error).message;
237+
expect(message).toContain("openclaw devices");
238+
// Must not leak the empty enum that originally read like a bug.
239+
expect(message).not.toContain("expected one of: )");
240+
expect(message).not.toContain("()");
241+
expect(listChannelPairingRequests).not.toHaveBeenCalled();
242+
});
243+
244+
it("lists supported channels when one is required but omitted", async () => {
245+
// Multiple channels configured (default mock) + no channel argument.
246+
await expect(runPairing(["pairing", "list"])).rejects.toThrow(
247+
"expected one of: telegram, discord, imessage",
248+
);
249+
});
250+
227251
it("accepts channel as positional for approve (npm-run compatible)", async () => {
228252
mockApprovedPairing();
229253

src/cli/pairing-cli.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ async function maybeBootstrapCommandOwnerFromPairing(params: {
8888

8989
export function registerPairingCli(program: Command) {
9090
const channels = listPairingChannels();
91+
// Avoid rendering a bare "()" enum when no channels are configured.
92+
const channelHint = channels.length > 0 ? channels.join(", ") : "none configured";
9193
const pairing = program
9294
.command("pairing")
9395
.description("Secure DM pairing (approve inbound requests)")
@@ -100,16 +102,21 @@ export function registerPairingCli(program: Command) {
100102
pairing
101103
.command("list")
102104
.description("List pending pairing requests")
103-
.option("--channel <channel>", `Channel (${channels.join(", ")})`)
105+
.option("--channel <channel>", `Channel (${channelHint})`)
104106
.option("--account <accountId>", "Account id (for multi-account channels)")
105-
.argument("[channel]", `Channel (${channels.join(", ")})`)
107+
.argument("[channel]", `Channel (${channelHint})`)
106108
.option("--json", "Print JSON", false)
107109
.action(async (channelArg, opts) => {
108110
const channelRaw = opts.channel ?? channelArg ?? (channels.length === 1 ? channels[0] : "");
109111
if (!channelRaw) {
110-
throw new Error(
111-
`Channel required. Use --channel <channel> or pass it as the first argument (expected one of: ${channels.join(", ")})`,
112-
);
112+
if (channels.length === 0) {
113+
// `pairing` is chat DM only; TUI/device approvals live under `openclaw devices`.
114+
throw new Error(
115+
`No chat DM pairing channels are configured. To approve a TUI or device request, ` +
116+
`use ${formatCliCommand("openclaw devices approve")} instead.`,
117+
);
118+
}
119+
throw new Error(`Channel required (expected one of: ${channelHint}).`);
113120
}
114121
const channel = parseChannel(channelRaw, channels);
115122
const accountId = normalizeStringifiedOptionalString(opts.account) ?? "";
@@ -151,7 +158,7 @@ export function registerPairingCli(program: Command) {
151158
pairing
152159
.command("approve")
153160
.description("Approve a pairing code and allow that sender")
154-
.option("--channel <channel>", `Channel (${channels.join(", ")})`)
161+
.option("--channel <channel>", `Channel (${channelHint})`)
155162
.option("--account <accountId>", "Account id (for multi-account channels)")
156163
.argument("<codeOrChannel>", "Pairing code (or channel when using 2 args)")
157164
.argument("[code]", "Pairing code (when channel is passed as the 1st arg)")

0 commit comments

Comments
 (0)