Files
MyGoNavi/frontend/src/components/ai/aiLocalToolExecutor.aiConfigInspection.test.ts
Syngnat b815e7b296 feat(mcp): 增强新增服务参数逐项提示
- 新增 MCP 参数逐项说明,覆盖未知参数、位置参数和常见运行时参数

- 对敏感参数值做脱敏展示,避免提示区泄露 token 或 api key

- 将逐项说明拆分到独立 util,并接入 inspect_mcp_draft 诊断输出
2026-06-12 09:09:47 +08:00

502 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it, vi } from 'vitest';
import type { AIToolCall, SavedConnection } from '../../types';
import { executeLocalAIToolCall } from './aiLocalToolExecutor';
const buildConnection = (): SavedConnection => ({
id: 'conn-1',
name: '主库',
config: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
user: 'root',
},
});
const buildToolCall = (name: string, args: Record<string, unknown>): AIToolCall => ({
id: `call-${name}`,
type: 'function',
function: {
name,
arguments: JSON.stringify(args),
},
});
describe('aiLocalToolExecutor AI config inspection tools', () => {
it('returns the current ai runtime snapshot so the model can inspect provider, safety, skills, and tools', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_ai_runtime', {}),
connections: [buildConnection()],
mcpTools: [{
alias: 'browser_open',
originalName: 'browser_open',
serverId: 'server-1',
serverName: 'browser',
title: '打开浏览器',
description: '打开页面',
}],
skills: [{
id: 'skill-1',
name: '结构审查',
systemPrompt: '先核对字段',
enabled: true,
scopes: ['database'],
requiredTools: ['get_columns'],
}],
dynamicModels: ['gpt-5.4', 'gpt-4.1'],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
getAIRuntimeState: vi.fn().mockResolvedValue({
activeProviderId: 'provider-1',
providers: [{
id: 'provider-1',
type: 'openai',
name: 'OpenAI 主账号',
apiKey: '',
hasSecret: true,
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-5.4',
models: ['gpt-5.4', 'gpt-4.1'],
maxTokens: 32000,
temperature: 0.2,
}],
safetyLevel: 'readonly',
contextLevel: 'with_samples',
}),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"hasActiveProvider":true');
expect(result.content).toContain('"name":"OpenAI 主账号"');
expect(result.content).toContain('"safetyLevel":"readonly"');
expect(result.content).toContain('"contextLevel":"with_samples"');
expect(result.content).toContain('"enabledSkillCount":1');
expect(result.content).toContain('"alias":"browser_open"');
expect(result.content).toContain('"builtinToolCount":');
});
it('returns the current ai safety snapshot so the model can inspect write boundaries and readonly guards', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_ai_safety', {}),
connections: [{
id: 'jvm-1',
name: 'JVM 诊断环境',
config: {
type: 'jvm',
host: '10.0.0.8',
port: 0,
user: '',
jvm: {
environment: 'uat',
readOnly: true,
diagnostic: {
transport: 'agent-bridge',
allowObserveCommands: true,
allowTraceCommands: true,
allowMutatingCommands: false,
},
},
},
}],
tabs: [{
id: 'diag-tab-1',
title: 'JVM 诊断',
type: 'jvm-diagnostic',
connectionId: 'jvm-1',
readOnly: true,
}],
activeTabId: 'diag-tab-1',
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
getAIRuntimeState: vi.fn().mockResolvedValue({
safetyLevel: 'readwrite',
}),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"safetyLevel":"readwrite"');
expect(result.content).toContain('"allowDML":true');
expect(result.content).toContain('"allowDDL":false');
expect(result.content).toContain('"readOnly":true');
expect(result.content).toContain('"allowMutatingCommands":false');
expect(result.content).toContain('allowMutating=true');
});
it('returns the current ai provider snapshot so the model can inspect provider readiness and model selection', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_ai_providers', {}),
connections: [buildConnection()],
mcpTools: [],
dynamicModels: ['gpt-5.4', 'gpt-4.1-mini'],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
getAIRuntimeState: vi.fn().mockResolvedValue({
activeProviderId: 'provider-1',
providers: [
{
id: 'provider-1',
type: 'openai',
name: 'OpenAI 主账号',
apiKey: '',
hasSecret: true,
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-5.4',
models: ['gpt-5.4', 'gpt-4.1'],
maxTokens: 32000,
temperature: 0.2,
},
{
id: 'provider-2',
type: 'custom',
name: '自建代理',
apiKey: '',
hasSecret: false,
baseUrl: '',
model: '',
models: [],
headers: {
Authorization: 'Bearer secret-token',
},
maxTokens: 16000,
temperature: 0.7,
},
],
}),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"providerCount":2');
expect(result.content).toContain('"missingSecretCount":1');
expect(result.content).toContain('"name":"OpenAI 主账号"');
expect(result.content).toContain('"name":"自建代理"');
expect(result.content).toContain('"issues":["missing_secret","missing_base_url","missing_selected_model","missing_declared_models"]');
expect(result.content).not.toContain('secret-token');
});
it('returns the current chat readiness snapshot so the model can inspect why ai input cannot send yet', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_ai_chat_readiness', {}),
connections: [buildConnection()],
mcpTools: [],
dynamicModels: ['gpt-5.5', 'gpt-4.1-mini'],
activeContext: {
connectionId: 'conn-1',
dbName: 'demo',
},
aiContexts: {
'conn-1:demo': [{
dbName: 'demo',
tableName: 'orders',
ddl: 'CREATE TABLE orders (...)',
}],
},
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
getAIRuntimeState: vi.fn().mockResolvedValue({
activeProviderId: 'provider-1',
providers: [{
id: 'provider-1',
type: 'openai',
name: 'OpenAI 主账号',
apiKey: '',
hasSecret: true,
baseUrl: 'https://api.openai.com/v1',
model: '',
models: ['gpt-5.5', 'gpt-4.1-mini'],
maxTokens: 32000,
temperature: 0.2,
}],
}),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"status":"missing_model"');
expect(result.content).toContain('"contextAttachedCount":1');
expect(result.content).toContain('"selectableModelCount":2');
expect(result.content).toContain('OpenAI 主账号');
});
it('returns the ai tool catalog so the model can choose probes and build arguments', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_ai_tool_catalog', {
keyword: 'mcp',
limit: 8,
}),
connections: [buildConnection()],
mcpTools: [{
alias: 'github_create_issue',
originalName: 'create_issue',
serverId: 'github-server',
serverName: 'GitHub',
title: '创建 Issue',
description: 'Create a GitHub issue',
inputSchema: {
type: 'object',
required: ['owner', 'repo', 'title'],
properties: {
owner: { type: 'string', description: '仓库 owner' },
repo: { type: 'string', description: '仓库名' },
title: { type: 'string', description: 'Issue 标题' },
},
},
}],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"keyword":"mcp"');
expect(result.content).toContain('新增 MCP 填写指引');
expect(result.content).toContain('"name":"inspect_mcp_draft"');
expect(result.content).toContain('"name":"fullCommand"');
expect(result.content).toContain('"alias":"github_create_issue"');
expect(result.content).toContain('"requiredParameters":["owner","repo","title"]');
expect(result.content).toContain('调用带参数工具前');
});
it('returns the current mcp setup snapshot so the model can inspect configured servers and client install state', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_mcp_setup', {}),
connections: [buildConnection()],
mcpTools: [{
alias: 'browser_open',
originalName: 'browser_open',
serverId: 'server-1',
serverName: 'Browser',
title: '打开页面',
description: '打开页面',
}],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
getMCPServers: vi.fn().mockResolvedValue([
{
id: 'server-1',
name: 'Browser',
transport: 'stdio',
command: 'uvx',
args: ['mcp-server-browser'],
env: {
OPENAI_API_KEY: '***',
},
enabled: true,
timeoutSeconds: 20,
},
{
id: 'server-2',
name: 'Broken',
transport: 'stdio',
command: '',
args: [],
env: {},
enabled: true,
timeoutSeconds: 1,
},
]),
getMCPClientInstallStatuses: vi.fn().mockResolvedValue([
{
client: 'codex',
displayName: 'Codex',
installed: true,
matchesCurrent: false,
clientDetected: true,
clientCommand: 'codex',
clientPath: 'C:/Tools/codex.exe',
configPath: 'C:/Users/demo/.codex/config.toml',
command: 'gonavi-mcp-server',
args: ['stdio'],
message: '检测到旧的 GoNavi 路径',
},
]),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"serverCount":2');
expect(result.content).toContain('"name":"Browser"');
expect(result.content).toContain('"launchCommandPreview":"uvx mcp-server-browser"');
expect(result.content).toContain('"serverConfigurationIssueCount":2');
expect(result.content).toContain('"serversWithConfigurationErrors":1');
expect(result.content).toContain('"key":"command-missing"');
expect(result.content).toContain('"displayName":"Codex"');
expect(result.content).toContain('"launchCommandPreview":"gonavi-mcp-server stdio"');
});
it('returns the builtin mcp authoring guide so the model can explain how to fill command, args, env, and templates', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_mcp_authoring_guide', {}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"supportsWholeCommandAutoSplit":true');
expect(result.content).toContain('"fullCommandPasteExample":"$env:GITHUB_TOKEN=...; uvx mcp-server-github --stdio"');
expect(result.content).toContain('"title":"启动命令"');
expect(result.content).toContain('"example":"npx / node / uvx / python / docker"');
expect(result.content).toContain('PowerShell $env:KEY=VALUE;');
expect(result.content).toContain('"title":"npx 包"');
expect(result.content).toContain('"exampleLaunchPreview":"npx -y @modelcontextprotocol/server-filesystem --stdio"');
expect(result.content).toContain('"title":"uvx 工具"');
expect(result.content).toContain('"exampleLaunchPreview":"uvx some-mcp-server"');
});
it('validates an mcp draft with the real command splitter and server validator', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_mcp_draft', {
fullCommand: '$env:GITHUB_TOKEN="ghp test"; uvx mcp-server-github --stdio',
timeoutSeconds: 45,
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"command":"uvx"');
expect(result.content).toContain('"args":["mcp-server-github","--stdio"]');
expect(result.content).toContain('"envKeys":["GITHUB_TOKEN"]');
expect(result.content).toContain('"envHints"');
expect(result.content).toContain('"label":"GitHub Token"');
expect(result.content).toContain('"secretLikeCount":1');
expect(result.content).toContain('"launchCommandPreview":"uvx mcp-server-github --stdio"');
expect(result.content).toContain('"suggestedServerSeed"');
expect(result.content).toContain('"name":"mcp-server-github"');
expect(result.content).toContain('"env":{"GITHUB_TOKEN":"***"}');
expect(result.content).toContain('"fullCommand":"GITHUB_TOKEN=*** uvx mcp-server-github --stdio"');
expect(result.content).not.toContain('ghp test');
expect(result.content).toContain('"recommendedTemplate":{"key":"uvx"');
expect(result.content).toContain('"canSave":true');
});
it('returns MCP argument hints and redacts sensitive inline argument values', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_mcp_draft', {
fullCommand: 'uvx mcp-server-demo --stdio --api-key=sk-real-secret --directory D:\\Work',
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"argumentHints"');
expect(result.content).toContain('"argumentDetailHints"');
expect(result.content).toContain('"businessHints"');
expect(result.content).toContain('"argument":"--api-key"');
expect(result.content).toContain('"label":"API Key"');
expect(result.content).toContain('"sensitive":true');
expect(result.content).toContain('"argument":"--directory"');
expect(result.content).toContain('"label":"授权目录"');
expect(result.content).toContain('"label":"授权目录的值"');
expect(result.content).toContain('"argsRedacted":true');
expect(result.content).toContain('"--api-key=***"');
expect(result.content).not.toContain('sk-real-secret');
});
it('returns mcp tool input schemas so the model can build arguments from discovered tool metadata', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_mcp_tool_schema', {
alias: 'github_create_issue',
}),
connections: [buildConnection()],
mcpTools: [{
alias: 'github_create_issue',
originalName: 'create_issue',
serverId: 'github-server',
serverName: 'GitHub',
title: '创建 Issue',
description: 'Create a GitHub issue',
inputSchema: {
type: 'object',
required: ['owner', 'repo', 'title'],
properties: {
owner: { type: 'string', description: '仓库 owner' },
repo: { type: 'string', description: '仓库名' },
title: { type: 'string', description: 'Issue 标题' },
state: { type: 'string', enum: ['open', 'closed'] },
},
},
}],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"alias":"github_create_issue"');
expect(result.content).toContain('"requiredParameters":["owner","repo","title"]');
expect(result.content).toContain('"path":"state"');
expect(result.content).toContain('"enumValues":["open","closed"]');
expect(result.content).toContain('调用 github_create_issue 前必须提供owner, repo, title');
});
it('returns the current ai guidance snapshot so the model can inspect active prompts and enabled skills', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_ai_guidance', {}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
userPromptSettings: {
global: '回答前先核对上下文。',
database: '生成 SQL 时只读优先。',
jvm: '',
jvmDiagnostic: '',
},
skills: [{
id: 'skill-1',
name: '结构审查',
description: '优先核对字段',
systemPrompt: '先看字段和索引,再给结论。',
enabled: true,
scopes: ['database'],
requiredTools: ['get_columns'],
}],
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"customPromptCount":2');
expect(result.content).toContain('"scope":"global"');
expect(result.content).toContain('回答前先核对上下文');
expect(result.content).toContain('"enabledSkillCount":1');
expect(result.content).toContain('"name":"结构审查"');
});
});