diff --git a/CHANGELOG.md b/CHANGELOG.md index c3a8b866fe1..4c26c106e70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/commands/channels.list.auth-profiles.test.ts b/src/commands/channels.list.auth-profiles.test.ts index fe48a5c4d34..bdf0e8fd38d 100644 --- a/src/commands/channels.list.auth-profiles.test.ts +++ b/src/commands/channels.list.auth-profiles.test.ts @@ -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; + }; + 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")); + }); }); diff --git a/src/commands/channels/list.ts b/src/commands/channels/list.ts index fecb3754dd2..d555ab34aca 100644 --- a/src/commands/channels/list.ts +++ b/src/commands/channels/list.ts @@ -114,7 +114,7 @@ export async function channelsListCommand( const includeUsage = opts.usage !== false; const plugins = listReadOnlyChannelPluginsForConfig(cfg, { - includeSetupRuntimeFallback: false, + includeSetupRuntimeFallback: true, }); const authStore = loadAuthProfileStoreWithoutExternalProfiles();