Files
MyGoNavi/frontend/src/components/ai/aiLocalToolExecutor.aiConfigInspection.test.ts
Syngnat 9038fe1bdf feat(ai): 增强 MCP 草稿校验输出
- 抽取 MCP 草稿 seed 构建逻辑供 UI 和内置工具复用

- inspect_mcp_draft 返回脱敏 suggestedServerSeed

- 同步 slash 命令、系统指导和回归测试
2026-06-11 20:53:43 +08:00

470 lines
16 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('"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 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":"结构审查"');
});
});