






















@@ -17,6 +17,7 @@ const mocks = vi.hoisted(() => ({
1717isCatalogChannelInstalled: vi.fn<(params: { entry: ChannelPluginCatalogEntry }) => boolean>(
1818() => true,
1919),
20+resolveMissingOfficialExternalChannelPluginRepairHint: vi.fn(),
2021callGateway: vi.fn(),
2122resolveAgentWorkspaceDir: vi.fn(() => "/tmp/workspace"),
2223resolveDefaultAgentId: vi.fn(() => "main"),
@@ -54,6 +55,11 @@ vi.mock("./channel-setup/discovery.js", () => ({
5455isCatalogChannelInstalled: mocks.isCatalogChannelInstalled,
5556}));
565758+vi.mock("../plugins/official-external-plugin-repair-hints.js", () => ({
59+resolveMissingOfficialExternalChannelPluginRepairHint:
60+mocks.resolveMissingOfficialExternalChannelPluginRepairHint,
61+}));
62+5763vi.mock("../agents/agent-scope.js", () => ({
5864resolveAgentWorkspaceDir: mocks.resolveAgentWorkspaceDir,
5965resolveDefaultAgentId: mocks.resolveDefaultAgentId,
@@ -120,6 +126,8 @@ describe("channels list", () => {
120126mocks.listTrustedChannelPluginCatalogEntries.mockReturnValue([]);
121127mocks.isCatalogChannelInstalled.mockReset();
122128mocks.isCatalogChannelInstalled.mockReturnValue(true);
129+mocks.resolveMissingOfficialExternalChannelPluginRepairHint.mockReset();
130+mocks.resolveMissingOfficialExternalChannelPluginRepairHint.mockReturnValue(null);
123131mocks.callGateway.mockReset();
124132mocks.callGateway.mockRejectedValue(new Error("gateway unavailable"));
125133});
@@ -355,6 +363,92 @@ describe("channels list", () => {
355363expect(output).toContain("--all");
356364});
357365366+it("default output shows configured official external channels when the plugin is missing", async () => {
367+const runtime = createTestRuntime();
368+mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([]);
369+mocks.listTrustedChannelPluginCatalogEntries.mockReturnValue([
370+createCatalogEntry("discord", "Discord"),
371+]);
372+mocks.isCatalogChannelInstalled.mockReturnValue(false);
373+mocks.resolveMissingOfficialExternalChannelPluginRepairHint.mockReturnValue({
374+pluginId: "discord",
375+channelId: "discord",
376+label: "Discord",
377+installSpec: "@openclaw/discord",
378+installCommand: "openclaw plugins install @openclaw/discord",
379+doctorFixCommand: "openclaw doctor --fix",
380+repairHint:
381+"Install the official external plugin with: openclaw plugins install @openclaw/discord, or run: openclaw doctor --fix.",
382+});
383+mocks.readConfigFileSnapshot.mockResolvedValue({
384+ ...baseConfigSnapshot,
385+config: {
386+channels: {
387+discord: { enabled: true, token: "secret" },
388+},
389+},
390+});
391+392+await channelsListCommand({}, runtime);
393+394+expect(mocks.resolveMissingOfficialExternalChannelPluginRepairHint).toHaveBeenCalledWith({
395+config: {
396+channels: {
397+discord: { enabled: true, token: "secret" },
398+},
399+},
400+channelId: "discord",
401+workspaceDir: "/tmp/workspace",
402+});
403+const output = stripAnsi(loggedText(runtime));
404+expect(output).toContain("Discord");
405+expect(output).toContain("not installed");
406+expect(output).toContain("configured");
407+expect(output).toContain("disabled");
408+expect(output).toContain(
409+"run openclaw plugins install @openclaw/discord or openclaw doctor --fix",
410+);
411+expect(output).not.toContain("no configured chat channels");
412+});
413+414+it("JSON output includes configured official external channels when the plugin is missing", async () => {
415+const runtime = createTestRuntime();
416+mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([]);
417+mocks.listTrustedChannelPluginCatalogEntries.mockReturnValue([
418+createCatalogEntry("discord", "Discord"),
419+]);
420+mocks.isCatalogChannelInstalled.mockReturnValue(false);
421+mocks.resolveMissingOfficialExternalChannelPluginRepairHint.mockReturnValue({
422+pluginId: "discord",
423+channelId: "discord",
424+label: "Discord",
425+installSpec: "@openclaw/discord",
426+installCommand: "openclaw plugins install @openclaw/discord",
427+doctorFixCommand: "openclaw doctor --fix",
428+repairHint:
429+"Install the official external plugin with: openclaw plugins install @openclaw/discord, or run: openclaw doctor --fix.",
430+});
431+mocks.readConfigFileSnapshot.mockResolvedValue({
432+ ...baseConfigSnapshot,
433+config: {
434+channels: {
435+discord: { enabled: true, token: "secret" },
436+},
437+},
438+});
439+440+await channelsListCommand({ json: true }, runtime);
441+442+const payload = JSON.parse(loggedText(runtime)) as {
443+chat: Record<string, { accounts: string[]; origin: string; installed: boolean }>;
444+};
445+expect(payload.chat.discord).toEqual({
446+accounts: [],
447+installed: false,
448+origin: "configured",
449+});
450+});
451+358452it("--all surfaces uninstalled catalog channels with installed=false / not configured / not enabled", async () => {
359453const runtime = createTestRuntime();
360454mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([]);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。