From 244a6f2ceb7f58c16b3cb2186584c39524e37874 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Sat, 27 Jun 2026 03:56:31 +0530 Subject: [PATCH] fix(desktop): broken "Open setup guide" button for plugin platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the desktop Channels / Messaging page, the "Open setup guide" button was rendered as a bare with no guard. Plugin-provided platforms (Microsoft Teams, Google Chat, Line, Raft, Yuanbao, …) ship an empty docs_url, so the anchor's href was "". In a packaged build, Electron resolves an empty href against the current document — the app's own index.html inside the asar bundle — and shell.openPath then fails with an OS "file not found" dialog. This is exactly the Windows error reported for Messaging → Teams → Open guide. Fix (3 changes): 1. fix(desktop) — Only render the "Open setup guide" button when docs_url is non-empty, and route clicks through openExternalLink so a relative/empty value can never be treated as a local bundle path. Fixes the whole class (every plugin platform), not just Teams. 2. fix(messaging) — Give the Teams platform plugin a real docs_url (Microsoft Teams setup guide) so its card shows a working button instead of nothing. 3. fix(messaging) — Give the Google Chat platform plugin a real docs_url (Google Chat setup guide) so its card shows a working button instead of nothing. Originally from #48940; folded in here because that PR's test was broken (it queried the HTTP endpoint, but google_chat is a dynamic enum member that only appears after the adapter module is imported). Test plan: - apps/desktop — new src/app/messaging/index.test.tsx: button is hidden when docs_url is empty; a real URL opens via the validated external opener (does not navigate). - apps/desktop typecheck (tsc --noEmit) clean. - backend — test_teams_messaging_metadata_links_setup_guide: the Teams catalog entry exposes the setup-guide docs_url. - backend — test_google_chat_messaging_metadata_links_setup_guide: the Google Chat catalog entry exposes the setup-guide docs_url. Co-authored-by: xxxigm Co-authored-by: p-andhika --- apps/desktop/src/app/messaging/index.test.tsx | 89 +++++++++++++++++++ apps/desktop/src/app/messaging/index.tsx | 34 +++++-- hermes_cli/web_server.py | 12 +++ tests/hermes_cli/test_web_server.py | 25 ++++++ 4 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 apps/desktop/src/app/messaging/index.test.tsx diff --git a/apps/desktop/src/app/messaging/index.test.tsx b/apps/desktop/src/app/messaging/index.test.tsx new file mode 100644 index 00000000000..a7d9273c0c9 --- /dev/null +++ b/apps/desktop/src/app/messaging/index.test.tsx @@ -0,0 +1,89 @@ +// @vitest-environment jsdom +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { MemoryRouter } from 'react-router-dom' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import type { MessagingPlatformInfo } from '@/types/hermes' + +const getMessagingPlatforms = vi.fn() +const updateMessagingPlatform = vi.fn() +const openExternalLink = vi.fn() + +vi.mock('@/hermes', () => ({ + getMessagingPlatforms: () => getMessagingPlatforms(), + updateMessagingPlatform: (id: string, body: unknown) => updateMessagingPlatform(id, body) +})) + +vi.mock('@/lib/external-link', () => ({ + openExternalLink: (href: string) => openExternalLink(href) +})) + +vi.mock('@/store/notifications', () => ({ + notify: vi.fn(), + notifyError: vi.fn() +})) + +vi.mock('@/store/system-actions', () => ({ + runGatewayRestart: vi.fn() +})) + +function platform(patch: Partial = {}): MessagingPlatformInfo { + return { + configured: false, + description: 'A platform.', + docs_url: '', + enabled: false, + env_vars: [], + gateway_running: true, + id: 'teams', + name: 'Microsoft Teams', + state: 'disabled', + ...patch + } +} + +beforeEach(() => { + updateMessagingPlatform.mockResolvedValue({ ok: true, platform: 'teams' }) +}) + +afterEach(() => { + cleanup() + vi.clearAllMocks() +}) + +async function renderMessaging() { + const { MessagingView } = await import('./index') + + return render( + + + + ) +} + +describe('MessagingView setup-guide link', () => { + it('hides the setup-guide button for a plugin platform with no docs URL', async () => { + // Teams (and other plugin platforms) ship an empty docs_url. Rendering an + // anchor with href="" let Electron resolve it to the app's own packaged + // index.html and fail with an OS "file not found" dialog. The button must + // simply not appear when there is no guide to open. + getMessagingPlatforms.mockResolvedValue({ platforms: [platform({ docs_url: '' })] }) + + await renderMessaging() + + expect((await screen.findAllByText('Microsoft Teams')).length).toBeGreaterThan(0) + expect(screen.queryByText('Open setup guide')).toBeNull() + }) + + it('opens a real docs URL through the validated external opener', async () => { + const docsUrl = 'https://hermes-agent.nousresearch.com/docs/user-guide/messaging/teams' + getMessagingPlatforms.mockResolvedValue({ platforms: [platform({ docs_url: docsUrl })] }) + + await renderMessaging() + + const link = await screen.findByText('Open setup guide') + fireEvent.click(link) + + await waitFor(() => expect(openExternalLink).toHaveBeenCalledWith(docsUrl)) + }) +}) diff --git a/apps/desktop/src/app/messaging/index.tsx b/apps/desktop/src/app/messaging/index.tsx index 659c655dccf..b2d5837fef6 100644 --- a/apps/desktop/src/app/messaging/index.tsx +++ b/apps/desktop/src/app/messaging/index.tsx @@ -14,6 +14,7 @@ import { updateMessagingPlatform } from '@/hermes' import { type Translations, useI18n } from '@/i18n' +import { openExternalLink } from '@/lib/external-link' import { AlertTriangle, ExternalLink, Save, Trash2 } from '@/lib/icons' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' @@ -404,14 +405,31 @@ function PlatformDetail({

{introCopy(platform, m)}

-
- -
+ {platform.docs_url && ( +
+ +
+ )}
diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 4647bc87b93..3d4bf45b56c 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -4749,6 +4749,11 @@ _PLATFORM_OVERRIDES: dict[str, dict[str, Any]] = { ), "required_env": ("FEISHU_APP_ID", "FEISHU_APP_SECRET"), }, + "google_chat": { + "name": "Google Chat", + "description": "Connect Hermes to Google Chat via Cloud Pub/Sub.", + "docs_url": "https://hermes-agent.nousresearch.com/docs/user-guide/messaging/google_chat", + }, "wecom": { "name": "WeCom (group bot)", "description": "Send-only WeCom group bot via webhook.", @@ -4798,6 +4803,12 @@ _PLATFORM_OVERRIDES: dict[str, dict[str, Any]] = { "env_vars": ("QQ_APP_ID", "QQ_CLIENT_SECRET", "QQ_ALLOWED_USERS"), "required_env": ("QQ_APP_ID", "QQ_CLIENT_SECRET"), }, + # Teams ships as a platform plugin, so its name/env vars come from the + # plugin registry. Only the docs link needs an override here so the + # Channels page can point at the Microsoft Teams setup guide. + "teams": { + "docs_url": "https://hermes-agent.nousresearch.com/docs/user-guide/messaging/teams", + }, "yuanbao": { "name": "Yuanbao (元宝)", "description": "Connect Hermes to Tencent Yuanbao.", @@ -4842,6 +4853,7 @@ _PLATFORM_ORDER: tuple[str, ...] = ( "sms", "dingtalk", "feishu", + "google_chat", "wecom", "wecom_callback", "weixin", diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index 1afe572832f..c92599bd74a 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -1746,6 +1746,31 @@ class TestWebServerEndpoints: assert "QR login" in fields[key]["description"] assert "Official Account" not in fields[key]["description"] + def test_teams_messaging_metadata_links_setup_guide(self): + # Teams is a platform plugin, so the catalog entry is built from the + # plugin registry. The override must still supply a docs link so the + # Channels page renders a working "Open setup guide" button instead of + # an empty href (which resolves to the packaged app's own index.html). + from hermes_cli.web_server import _build_catalog_entry + + teams = _build_catalog_entry("teams") + assert teams["docs_url"] == ( + "https://hermes-agent.nousresearch.com/docs/user-guide/messaging/teams" + ) + + def test_google_chat_messaging_metadata_links_setup_guide(self): + # Google Chat is a platform plugin, so the catalog entry is built from + # the plugin registry. The override must supply a docs link so the + # Channels page renders a working "Open setup guide" button instead of + # an empty href (which resolves to the packaged app's own index.html). + from hermes_cli.web_server import _build_catalog_entry + + google_chat = _build_catalog_entry("google_chat") + assert google_chat["name"] == "Google Chat" + assert google_chat["docs_url"] == ( + "https://hermes-agent.nousresearch.com/docs/user-guide/messaging/google_chat" + ) + def test_messaging_catalog_covers_gateway_platforms(self): """Catalog is derived from the Platform enum, so every built-in shows up.""" from gateway.config import Platform