fix(cli): show configured chat channels in list

This commit is contained in:
Peter Steinberger
2026-04-28 08:12:23 +01:00
parent bdba90a20b
commit e4139c3cb6
3 changed files with 108 additions and 5 deletions

View File

@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
- Agents/models: keep per-agent primary models strict when `fallbacks` is omitted, so probe-only custom providers are not tried as hidden fallback candidates unless the agent explicitly opts in. Fixes #73332. Thanks @haumanto.
- Cron/Telegram: preserve explicit `:topic:` delivery targets over stale session-derived thread IDs when isolated cron announces to Telegram forum topics. Carries forward #59069; refs #49704 and #43808. Thanks @roytong9.
- Build/runtime: write the runtime-postbuild stamp after `pnpm build` writes the build stamp, so the next CLI invocation does not re-sync runtime artifacts after a successful build. Fixes #73151. Thanks @bittoby.
- CLI/channels: list configured chat channel accounts from read-only setup metadata even when the standalone CLI has not loaded the runtime channel registry, so `openclaw channels list` shows Telegram accounts before auth providers. Fixes #73319 and #73322. Thanks @mlaihk.
- CLI/model probes: reject empty or whitespace-only `infer model run --prompt` values before calling local providers or the Gateway, so smoke checks do not spend provider calls on invalid turns. Fixes #73185. Thanks @iot2edge.
- Gateway/media: route text-only `chat.send` image offloads through media-understanding fields so `agents.defaults.imageModel` can describe WebChat attachments instead of leaving only an opaque `media://inbound` marker. Fixes #72968. Thanks @vorajeeah.
- Gateway/Windows: route no-listener restart handoffs through the Windows supervisor without leaving restart tokens in flight, so failed task scheduling can be retried and successful handoffs do not coalesce later restart requests. (#69056) Thanks @Thatgfsj.

View File

@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { baseConfigSnapshot, createTestRuntime } from "./test-runtime-config-helpers.js";
const mocks = vi.hoisted(() => ({
@@ -9,7 +9,8 @@ const mocks = vi.hoisted(() => ({
diagnostics: [],
})),
loadAuthProfileStoreWithoutExternalProfiles: vi.fn(),
listChannelPlugins: vi.fn(() => []),
listReadOnlyChannelPluginsForConfig: vi.fn(() => []),
buildChannelAccountSnapshot: vi.fn(),
}));
vi.mock("../config/config.js", () => ({
@@ -28,13 +29,26 @@ vi.mock("../agents/auth-profiles.js", () => ({
loadAuthProfileStoreWithoutExternalProfiles: mocks.loadAuthProfileStoreWithoutExternalProfiles,
}));
vi.mock("../channels/plugins/index.js", () => ({
listChannelPlugins: mocks.listChannelPlugins,
vi.mock("../channels/plugins/read-only.js", () => ({
listReadOnlyChannelPluginsForConfig: mocks.listReadOnlyChannelPluginsForConfig,
}));
vi.mock("../channels/plugins/status.js", () => ({
buildChannelAccountSnapshot: mocks.buildChannelAccountSnapshot,
}));
import { channelsListCommand } from "./channels/list.js";
describe("channels list auth profiles", () => {
beforeEach(() => {
mocks.readConfigFileSnapshot.mockReset();
mocks.resolveCommandConfigWithSecrets.mockClear();
mocks.loadAuthProfileStoreWithoutExternalProfiles.mockReset();
mocks.listReadOnlyChannelPluginsForConfig.mockReset();
mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([]);
mocks.buildChannelAccountSnapshot.mockReset();
});
it("includes local auth profiles in JSON output without loading external profiles", async () => {
const runtime = createTestRuntime();
mocks.readConfigFileSnapshot.mockResolvedValue({
@@ -73,4 +87,92 @@ describe("channels list auth profiles", () => {
expect(ids).toContain("anthropic:default");
expect(ids).toContain("openai-codex:default");
});
it("includes configured chat channel accounts in JSON output", async () => {
const runtime = createTestRuntime();
mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([
{
id: "telegram",
meta: { id: "telegram", label: "Telegram" },
config: {
listAccountIds: () => ["alerts", "default"],
},
},
]);
mocks.readConfigFileSnapshot.mockResolvedValue({
...baseConfigSnapshot,
config: {
channels: {
telegram: {
accounts: {
default: { botToken: "123:abc" },
alerts: { botToken: "456:def" },
},
},
},
},
});
mocks.loadAuthProfileStoreWithoutExternalProfiles.mockReturnValue({
version: 1,
profiles: {},
});
await channelsListCommand({ json: true, usage: false }, runtime);
expect(mocks.listReadOnlyChannelPluginsForConfig).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({ includeSetupRuntimeFallback: true }),
);
const payload = JSON.parse(runtime.log.mock.calls[0]?.[0] as string) as {
chat?: Record<string, string[]>;
};
expect(payload.chat?.telegram).toEqual(["alerts", "default"]);
});
it("prints configured chat channel accounts before auth providers", async () => {
const runtime = createTestRuntime();
mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([
{
id: "telegram",
meta: { id: "telegram", label: "Telegram" },
config: {
listAccountIds: () => ["default"],
},
},
]);
mocks.buildChannelAccountSnapshot.mockResolvedValue({
accountId: "default",
configured: true,
tokenSource: "config",
enabled: true,
});
mocks.readConfigFileSnapshot.mockResolvedValue({
...baseConfigSnapshot,
config: {
channels: {
telegram: {
accounts: {
default: { botToken: "123:abc" },
},
},
},
},
});
mocks.loadAuthProfileStoreWithoutExternalProfiles.mockReturnValue({
version: 1,
profiles: {},
});
await channelsListCommand({ usage: false }, runtime);
expect(mocks.listReadOnlyChannelPluginsForConfig).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({ includeSetupRuntimeFallback: true }),
);
const output = runtime.log.mock.calls[0]?.[0] as string;
expect(output).toContain("Chat channels:");
expect(output).toContain("Telegram default:");
expect(output).toContain("configured");
expect(output.indexOf("Telegram default:")).toBeLessThan(output.indexOf("Auth providers"));
});
});

View File

@@ -114,7 +114,7 @@ export async function channelsListCommand(
const includeUsage = opts.usage !== false;
const plugins = listReadOnlyChannelPluginsForConfig(cfg, {
includeSetupRuntimeFallback: false,
includeSetupRuntimeFallback: true,
});
const authStore = loadAuthProfileStoreWithoutExternalProfiles();