diff --git a/frontend/src/components/ai/AIChatHeader.test.tsx b/frontend/src/components/ai/AIChatHeader.test.tsx index b3a4edc..baddbbf 100644 --- a/frontend/src/components/ai/AIChatHeader.test.tsx +++ b/frontend/src/components/ai/AIChatHeader.test.tsx @@ -2,13 +2,11 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; const headerSource = readFileSync(new URL('./AIChatHeader.tsx', import.meta.url), 'utf8'); -const v2ThemeCss = readFileSync(new URL('../../v2-theme.css', import.meta.url), 'utf8'); describe('AIChatHeader export affordance', () => { - it('does not expose chat export UI or markdown export implementation', () => { - expect(headerSource).not.toContain('exportToMarkdown'); - expect(headerSource).not.toContain('导出为 Markdown'); - expect(headerSource).not.toContain('gn-v2-ai-export-button'); - expect(v2ThemeCss).not.toContain('gn-v2-ai-export-button'); + it('keeps chat export UI and markdown export implementation wired', () => { + expect(headerSource).toContain('exportToMarkdown'); + expect(headerSource).toContain('gn-v2-ai-export-button'); + expect(headerSource).toContain("t('ai_chat.header.action.export')"); }); }); diff --git a/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx b/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx index 2abf4cf..00f4fb1 100644 --- a/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx +++ b/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx @@ -62,25 +62,25 @@ describe('AIMCPClientInstallPanel', () => { />, ); - expect(markup).toContain('这里是在把 GoNavi MCP 接入 Claude Code / Codex / OpenClaw / Hermans'); - expect(markup).toContain('给外部工具调用'); - expect(markup).toContain('OpenClaw、Hermans 这类云端 Agent 会提供远程接入说明'); - expect(markup).toContain('接入外部客户端'); - expect(markup).toContain('选择外部客户端'); - expect(markup).toContain('选择目标客户端'); - expect(markup).toContain('写入或复制配置'); - expect(markup).toContain('重启或配置目标端'); - expect(markup).toContain('未接入'); - expect(markup).toContain('需更新'); - expect(markup).toContain('外部工具接入状态:已存在旧配置,需更新'); - expect(markup).toContain('外部工具接入状态:未接入'); - expect(markup).toContain('复制配置路径'); - expect(markup).toContain('复制启动命令'); - expect(markup).toContain('更新 Codex 接入配置'); - expect(markup).toContain('已选客户端状态'); - expect(markup).toContain('CLI 检测:已检测到 codex'); - expect(markup).toContain('当前已选中,将只对这个客户端执行写入或更新'); - expect(markup).toContain('当前目标客户端:Codex'); + expect(markup).toContain('This connects GoNavi MCP to Claude Code / Codex / OpenClaw / Hermans'); + expect(markup).toContain('external tool calls'); + expect(markup).toContain('Cloud Agents such as OpenClaw and Hermans use remote connection guidance'); + expect(markup).toContain('Connect external client'); + expect(markup).toContain('Select external client'); + expect(markup).toContain('Choose target client'); + expect(markup).toContain('Write or copy config'); + expect(markup).toContain('Restart or configure target'); + expect(markup).toContain('Not connected'); + expect(markup).toContain('Update needed'); + expect(markup).toContain('External tool connection status: old config found, update needed'); + expect(markup).toContain('External tool connection status: not connected'); + expect(markup).toContain('Copy config path'); + expect(markup).toContain('Copy launch command'); + expect(markup).toContain('Update Codex connection config'); + expect(markup).toContain('Selected client status'); + expect(markup).toContain('CLI detection: Detected codex'); + expect(markup).toContain('Selected. Only this client will be written or updated'); + expect(markup).toContain('Current target client: Codex'); }); it('shows an already-connected label and supports prewriting config when the client command is not detected locally', () => { @@ -131,10 +131,10 @@ describe('AIMCPClientInstallPanel', () => { />, ); - expect(markup).toContain('安装到 Claude Code(外部工具)'); - expect(markup).toContain('CLI 检测:未检测到 claude'); - expect(markup).toContain('未检测到本机 claude 命令'); - expect(markup).toContain('已接入'); + expect(markup).toContain('Install to Claude Code (external tool)'); + expect(markup).toContain('CLI detection: claude was not detected'); + expect(markup).toContain('Local claude command was not detected'); + expect(markup).toContain('Connected'); }); it('renders remote Agent clients as bridge guidance instead of local installs', () => { @@ -188,35 +188,35 @@ describe('AIMCPClientInstallPanel', () => { />, ); - expect(markup).toContain('远程桥接'); - expect(markup).toContain('当前已选中,将复制远程接入说明'); - expect(markup).toContain('远程接入边界'); - expect(markup).toContain('云端 Agent 默认通过 schema-only MCP 工具读取连接摘要、库表和 DDL'); - expect(markup).toContain('不注册 execute_sql'); - expect(markup).toContain('OpenClaw 远程 MCP 快速配置'); - expect(markup).toContain('公网/隧道 URL'); - expect(markup).toContain('云端 Agent 能访问到的 Streamable HTTP MCP 地址'); - expect(markup).toContain('不要填 Windows 本机的 127.0.0.1'); + expect(markup).toContain('Remote bridge'); + expect(markup).toContain('Selected. The remote connection guide will be copied'); + expect(markup).toContain('Remote connection boundary'); + expect(markup).toContain('Cloud Agents read connection summaries, tables, and DDL through schema-only MCP tools by default'); + expect(markup).toContain('execute_sql is not registered'); + expect(markup).toContain('OpenClaw Remote MCP quick setup'); + expect(markup).toContain('Public/tunnel URL'); + expect(markup).toContain('Enter the Streamable HTTP MCP address reachable by the cloud Agent'); + expect(markup).toContain('Do not use the Windows local 127.0.0.1 address'); expect(markup).toContain('Bearer Token'); - expect(markup).toContain('Windows 启动命令和云端 Agent 配置必须一致'); - expect(markup).toContain('不要把数据库密码当 token 填进去'); - expect(markup).toContain('本机监听地址'); - expect(markup).toContain('MCP 路径'); - expect(markup).toContain('配置到云端 Agent'); - expect(markup).toContain('无 GUI / CLI 生成配置'); + expect(markup).toContain('the Windows launch command and cloud Agent config must match'); + expect(markup).toContain('do not put a database password here'); + expect(markup).toContain('Local listen address'); + expect(markup).toContain('MCP path'); + expect(markup).toContain('Configure in cloud Agent'); + expect(markup).toContain('Generate config without GUI / CLI'); expect(markup).toContain('"type": "streamable-http"'); - expect(markup).toContain('"url": "https://<你的域名或隧道地址>/mcp"'); - expect(markup).toContain('"Authorization": "Bearer <随机token>"'); - expect(markup).toContain('GoNavi.exe mcp-server remote-config --client openclaw --url https://<你的域名或隧道地址>/mcp --token <随机token> --schema-only'); - expect(markup).toContain('Windows 启动 GoNavi MCP HTTP'); - expect(markup).toContain('GoNavi.exe mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token> --schema-only'); - expect(markup).toContain('独立二进制:gonavi-mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token> --schema-only'); - expect(markup).toContain('验证顺序'); - expect(markup).toContain('安全边界'); - expect(markup).toContain('数据库账号和密码仍保存在 Windows GoNavi'); - expect(markup).toContain('默认 --schema-only 不注册 execute_sql'); - expect(markup).toContain('CLI 检测:远程 Agent 不需要检测本机 openclaw 命令'); - expect(markup).toContain('复制 OpenClaw 远程接入说明'); + expect(markup).toContain('"url": "https://<your-domain-or-tunnel>/mcp"'); + expect(markup).toContain('"Authorization": "Bearer <random-token>"'); + expect(markup).toContain('GoNavi.exe mcp-server remote-config --client openclaw --url https://<your-domain-or-tunnel>/mcp --token <random-token> --schema-only'); + expect(markup).toContain('Start GoNavi MCP HTTP on Windows'); + expect(markup).toContain('GoNavi.exe mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <random-token> --schema-only'); + expect(markup).toContain('Standalone binary: gonavi-mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <random-token> --schema-only'); + expect(markup).toContain('Verification order'); + expect(markup).toContain('Security boundary'); + expect(markup).toContain('Database accounts and passwords stay in Windows GoNavi'); + expect(markup).toContain('--schema-only does not register execute_sql by default'); + expect(markup).toContain('CLI detection: Remote Agent does not need local openclaw command detection'); + expect(markup).toContain('Copy OpenClaw remote connection guide'); }); it('makes repeated install avoidance explicit when the selected client already matches current GoNavi', () => { @@ -267,9 +267,9 @@ describe('AIMCPClientInstallPanel', () => { />, ); - expect(markup).toContain('当前状态:已接入当前 GoNavi,无需重复操作'); - expect(markup).toContain('Claude Code 已接入,无需重复安装'); - expect(markup).toContain('下面的主按钮会自动禁用,避免重复写入'); + expect(markup).toContain('Current status: Connected to current GoNavi; no repeated action needed'); + expect(markup).toContain('Claude Code is connected; no reinstall needed'); + expect(markup).toContain('the main button is disabled to avoid repeated writes'); }); it('prefers the client that already matches current GoNavi over another stale installed record', () => { @@ -320,7 +320,7 @@ describe('AIMCPClientInstallPanel', () => { />, ); - expect(markup).toContain('已选客户端状态'); - expect(markup).toContain('当前状态:已接入当前 GoNavi,无需重复操作'); + expect(markup).toContain('Selected client status'); + expect(markup).toContain('Current status: Connected to current GoNavi; no repeated action needed'); }); }); diff --git a/frontend/src/components/ai/AISettingsMCPSection.test.tsx b/frontend/src/components/ai/AISettingsMCPSection.test.tsx index 1684642..0230fd6 100644 --- a/frontend/src/components/ai/AISettingsMCPSection.test.tsx +++ b/frontend/src/components/ai/AISettingsMCPSection.test.tsx @@ -4,27 +4,9 @@ import { describe, expect, it, vi } from 'vitest'; import AISettingsMCPSection from './AISettingsMCPSection'; import type { AISettingsMCPSectionProps } from './AISettingsMCPSection'; +import { I18nProvider } from '../../i18n/provider'; import { buildOverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme'; -const findElement = (node: any, predicate: (element: any) => boolean): any => { - if (node == null || typeof node === 'boolean' || typeof node === 'string' || typeof node === 'number') { - return null; - } - if (Array.isArray(node)) { - for (const item of node) { - const match = findElement(item, predicate); - if (match) { - return match; - } - } - return null; - } - if (predicate(node)) { - return node; - } - return findElement(node.props?.children, predicate); -}; - const buildMCPSectionProps = (patch: Partial = {}): AISettingsMCPSectionProps => ({ mcpClientStatuses: [ { @@ -98,41 +80,82 @@ const buildMCPSectionProps = (patch: Partial = {}): A ...patch, }); +const renderSectionWithMockedHTTPPanel = async (props: AISettingsMCPSectionProps) => { + const captured: { httpPanelProps?: any } = {}; + + vi.resetModules(); + vi.doMock('./AIMCPHTTPServerPanel', () => ({ + default: (panelProps: any) => { + captured.httpPanelProps = panelProps; + return null; + }, + })); + vi.doMock('./AIMCPClientInstallPanel', () => ({ default: () => null })); + vi.doMock('./AIMCPQuickAddServerPanel', () => ({ default: () => null })); + vi.doMock('./AIMCPFieldGuideCard', () => ({ default: () => null })); + vi.doMock('./AIMCPServerCard', () => ({ default: () => null })); + + const { default: MockedAISettingsMCPSection } = await import('./AISettingsMCPSection'); + renderToStaticMarkup(); + + vi.doUnmock('./AIMCPHTTPServerPanel'); + vi.doUnmock('./AIMCPClientInstallPanel'); + vi.doUnmock('./AIMCPQuickAddServerPanel'); + vi.doUnmock('./AIMCPFieldGuideCard'); + vi.doUnmock('./AIMCPServerCard'); + + return captured; +}; + describe('AISettingsMCPSection', () => { it('renders the extracted MCP client installer and server management entry point', () => { const markup = renderToStaticMarkup( , ); - expect(markup).toContain('GoNavi MCP HTTP 服务'); - expect(markup).toContain('可自定义本机监听端口和 Bearer Token'); + expect(markup).toContain('GoNavi MCP HTTP service'); + expect(markup).toContain('customize the local listen port and Bearer Token'); expect(markup).toContain('http://127.0.0.1:8765/mcp'); - expect(markup).toContain('复制 Authorization'); - expect(markup).toContain('接入外部客户端'); - expect(markup).toContain('尚未把当前 GoNavi MCP 接入到这里'); - expect(markup).toContain('一行命令快速新增'); - expect(markup).toContain('先选最接近的模板'); - expect(markup).toContain('解析并新增草稿'); - expect(markup).toContain('新增 MCP 参数速查'); + expect(markup).toContain('Copy Authorization'); + expect(markup).toContain('Connect external client'); + expect(markup).toContain('Current GoNavi MCP is not connected here yet'); + expect(markup).toContain('Quick add from one command'); + expect(markup).toContain('Choose the closest template'); + expect(markup).toContain('Parse and add draft'); + expect(markup).toContain('New MCP parameter quick reference'); expect(markup).toContain('command'); expect(markup).toContain('args'); expect(markup).toContain('env'); expect(markup).toContain('timeout'); - expect(markup).toContain('只填程序名或启动器本身'); - expect(markup).toContain('应填:'); - expect(markup).toContain('填 npx、node、uvx、python、docker,或某个 exe 的绝对路径'); - expect(markup).toContain('不要填整行命令,例如不要填 npx -y pkg --stdio'); - expect(markup).toContain('把脚本名、模块名、开关参数拆开逐项填写'); - expect(markup).toContain('不要再填 npx/node/uvx/python/docker'); - expect(markup).toContain('给 MCP Server 传入 KEY=VALUE 形式的配置'); - expect(markup).toContain('不要写 export、set 或 $env: 前缀'); - expect(markup).toContain('单次工具发现或调用最多等待多久'); - expect(markup).toContain('常见启动方式模板'); - expect(markup).toContain('npx 包'); + expect(markup).toContain('Enter only the program name or launcher itself'); + expect(markup).toContain('Fill:'); + expect(markup).toContain('Enter npx, node, uvx, python, docker, or an absolute path to an exe'); + expect(markup).toContain('Do not enter the whole command line, such as npx -y pkg --stdio'); + expect(markup).toContain('Split script names, module names, and flags into separate entries'); + expect(markup).toContain('Do not enter npx/node/uvx/python/docker again'); + expect(markup).toContain('Pass KEY=VALUE configuration to the MCP Server'); + expect(markup).toContain('Do not write export, set, or a $env: prefix'); + expect(markup).toContain('Maximum wait time for one tool discovery or call'); + expect(markup).toContain('Common startup templates'); + expect(markup).toContain('npx package'); expect(markup).toContain('npx -y @modelcontextprotocol/server-filesystem --stdio'); - expect(markup).toContain('Node 脚本'); - expect(markup).toContain('Docker 镜像'); + expect(markup).toContain('Node script'); + expect(markup).toContain('Docker image'); expect(markup).toContain('docker run -i --rm image'); + expect(markup).toContain('Add MCP service'); + expect(markup).toContain('No MCP service yet'); + expect(markup).toContain('npx -y package --stdio'); + }); + + it('renders the MCP quick reference in Chinese when an i18n provider is available', () => { + const markup = renderToStaticMarkup( + {}}> + + , + ); + + expect(markup).toContain('新增 MCP 参数速查'); + expect(markup).toContain('应填:'); expect(markup).toContain('新增 MCP 服务'); expect(markup).toContain('还没有 MCP 服务'); expect(markup).toContain('npx -y package --stdio'); @@ -181,52 +204,44 @@ describe('AISettingsMCPSection', () => { />, ); - expect(markup).toContain('常见填错现象'); - expect(markup).toContain('测试提示找不到命令'); - expect(markup).toContain('认证失败、401 或 403'); - expect(markup).toContain('当前只支持 stdio'); - expect(markup).toContain('不要把密钥写进聊天内容'); - expect(markup).toContain('已发现工具和参数提示'); + expect(markup).toContain('Common setup mistakes'); + expect(markup).toContain('Test says the command cannot be found'); + expect(markup).toContain('Authentication failed, 401, or 403'); + expect(markup).toContain('the current GoNavi add flow does not directly support it'); + expect(markup).toContain('do not put secrets into chat content'); + expect(markup).toContain('Discovered tools and parameter hints'); expect(markup).toContain('execute_sql'); - expect(markup).toContain('参数 4 个,必填 2 个'); - expect(markup).toContain('最小 arguments 示例'); + expect(markup).toContain('4 parameters, 2 required; an asterisk marks required fields.'); + expect(markup).toContain('Minimum arguments example:'); expect(markup).toContain('"connectionId":"<connectionId>"'); expect(markup).toContain('"sql":"<sql>"'); expect(markup).toContain('connectionId*: string'); expect(markup).toContain('sql*: string'); expect(markup).toContain('allowMutating: boolean'); expect(markup).toContain('legacy_tool'); - expect(markup).toContain('未声明 inputSchema'); + expect(markup).toContain('No inputSchema declared; check the service docs or use /mcptool before calling.'); }); - it('toggles the in-app MCP HTTP service from the switch panel', () => { + it('toggles the in-app MCP HTTP service from the switch panel', async () => { const onToggleHTTPServer = vi.fn(); - const tree = AISettingsMCPSection(buildMCPSectionProps({ + const captured = await renderSectionWithMockedHTTPPanel(buildMCPSectionProps({ onToggleHTTPServer, })); - const httpPanel = findElement( - tree, - (node) => node.props?.onToggle === onToggleHTTPServer, - ); - expect(httpPanel).toBeTruthy(); - httpPanel.props.onToggle(true); + expect(captured.httpPanelProps).toBeTruthy(); + captured.httpPanelProps.onToggle(true); expect(onToggleHTTPServer).toHaveBeenCalledWith(true); }); - it('passes MCP HTTP draft updates through the switch panel', () => { + it('passes MCP HTTP draft updates through the switch panel', async () => { const onUpdateHTTPServerDraft = vi.fn(); - const tree = AISettingsMCPSection(buildMCPSectionProps({ + const captured = await renderSectionWithMockedHTTPPanel(buildMCPSectionProps({ onUpdateHTTPServerDraft, })); - const httpPanel = findElement( - tree, - (node) => node.props?.onDraftChange === onUpdateHTTPServerDraft, - ); - expect(httpPanel).toBeTruthy(); - httpPanel.props.onDraftChange({ addr: '127.0.0.1:9123' }); + expect(captured.httpPanelProps).toBeTruthy(); + captured.httpPanelProps.onDraftChange({ addr: '127.0.0.1:9123' }); expect(onUpdateHTTPServerDraft).toHaveBeenCalledWith({ addr: '127.0.0.1:9123' }); }); diff --git a/frontend/src/components/ai/aiLocalToolExecutor.localAssetsInspection.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.localAssetsInspection.test.ts index 0664b28..203a43d 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.localAssetsInspection.test.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.localAssetsInspection.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; +import { t as translateCatalog } from '../../i18n/catalog'; import type { AIToolCall, SavedConnection } from '../../types'; import { executeLocalAIToolCall } from './aiLocalToolExecutor'; @@ -191,7 +192,7 @@ describe('aiLocalToolExecutor local asset inspection tools', () => { expect(result.content).toContain('"toolResultChars":21000'); expect(result.content).toContain('"tableName":"orders"'); expect(result.content).toContain('"mcpToolCount":1'); - expect(result.content).toContain('最近工具结果较长'); + expect(result.content).toContain('Recent tool results are long'); }); it('returns an ai support bundle with health, message flow, context budget, and remote MCP evidence', async () => { @@ -295,6 +296,7 @@ describe('aiLocalToolExecutor local asset inspection tools', () => { getDatabases: vi.fn(), getTables: vi.fn(), }, + translate: (key, params) => translateCatalog('zh-CN', key, params), }); expect(result.success).toBe(true);