Files
MyGoNavi/frontend/src/components/ai/aiLocalToolExecutor.test.ts
Syngnat ce06bea744 ♻️ refactor(ai-tools): 拆分 SQL 风险探针执行器
- 将 inspect_sql_risk 执行逻辑从聚合探针执行器中拆出

- 将 SQL 风险工具调用测试迁移到独立测试文件

- 保持本地工具调用行为不变并降低后续扩展成本
2026-06-09 22:31:30 +08:00

1230 lines
39 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import type { AIMCPToolDescriptor, AIToolCall, ExternalSQLDirectory, SavedConnection } from '../../types';
import { buildToolResultMessage, 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', () => {
it('caches validated table context after get_tables succeeds', async () => {
const toolContextMap = new Map();
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('get_tables', { connectionId: 'conn-1', dbName: 'crm' }),
connections: [buildConnection()],
mcpTools: [],
toolContextMap,
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn().mockResolvedValue({
success: true,
data: [{ Table: 'users' }, { Table: 'orders' }],
}),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('users');
expect(toolContextMap.get('conn-1:crm')).toEqual({
connectionId: 'conn-1',
dbName: 'crm',
tables: ['users', 'orders'],
});
});
it('returns the current active tab snapshot so the model can inspect the editor draft directly', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_active_tab', {
includeContent: true,
}),
connections: [buildConnection()],
tabs: [{
id: 'tab-query-1',
title: '订单查询',
type: 'query',
connectionId: 'conn-1',
dbName: 'crm',
query: 'SELECT id, status FROM orders WHERE status = \'paid\'',
filePath: 'D:/sql/orders.sql',
}],
activeTabId: 'tab-query-1',
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"hasActiveTab":true');
expect(result.content).toContain('"type":"query"');
expect(result.content).toContain('"connectionName":"主库"');
expect(result.content).toContain('"contentKind":"sql"');
expect(result.content).toContain('SELECT id, status FROM orders');
});
it('returns a workspace tab overview so the model can inspect which editors are currently open', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_workspace_tabs', {
limit: 2,
includeContent: true,
}),
connections: [buildConnection()],
tabs: [
{
id: 'tab-query-1',
title: '订单查询',
type: 'query',
connectionId: 'conn-1',
dbName: 'crm',
query: 'SELECT * FROM orders WHERE status = \'paid\'',
},
{
id: 'tab-table-1',
title: 'users',
type: 'table',
connectionId: 'conn-1',
dbName: 'crm',
tableName: 'users',
},
{
id: 'tab-redis-1',
title: '缓存命令',
type: 'redis-command',
connectionId: 'conn-1',
query: 'GET order:1',
redisDB: 2,
},
],
activeTabId: 'tab-query-1',
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"activeTabId":"tab-query-1"');
expect(result.content).toContain('"totalTabs":3');
expect(result.content).toContain('"returnedTabs":2');
expect(result.content).toContain('"truncated":true');
expect(result.content).toContain('"isActive":true');
expect(result.content).toContain('"title":"订单查询"');
expect(result.content).toContain('SELECT * FROM orders');
});
it('returns the current linked AI context so the model can inspect which table schemas are already mounted', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_ai_context', {
includeDDL: true,
ddlLimit: 80,
}),
connections: [buildConnection()],
activeContext: {
connectionId: 'conn-1',
dbName: 'crm',
},
aiContexts: {
'conn-1:crm': [
{
dbName: 'crm',
tableName: 'orders',
ddl: 'CREATE TABLE orders (id bigint primary key, status varchar(32), amount decimal(10,2));',
},
],
},
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"hasActiveContext":true');
expect(result.content).toContain('"connectionName":"主库"');
expect(result.content).toContain('"tableName":"orders"');
expect(result.content).toContain('"includeDDL":true');
expect(result.content).toContain('CREATE TABLE orders');
});
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 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,
},
]),
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":1');
expect(result.content).toContain('"name":"Browser"');
expect(result.content).toContain('"launchCommandPreview":"uvx mcp-server-browser"');
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":"node / uvx / python"');
expect(result.content).toContain('PowerShell $env:KEY=VALUE;');
expect(result.content).toContain('"title":"uvx 工具"');
expect(result.content).toContain('"exampleLaunchPreview":"uvx some-mcp-server"');
});
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":"结构审查"');
});
it('returns the current connection snapshot so the model can inspect host, db, and ssh state', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_current_connection', {}),
connections: [{
id: 'conn-1',
name: '主库',
config: {
type: 'mysql',
host: '10.188.101.184',
port: 1523,
user: 'glzc',
database: 'crm',
useSSH: true,
ssh: {
host: '192.168.66.28',
port: 22,
user: 'wyeye',
},
},
}],
activeContext: {
connectionId: 'conn-1',
dbName: 'crm',
},
tabs: [{
id: 'tab-query-1',
title: '订单分析',
type: 'query',
connectionId: 'conn-1',
dbName: 'crm',
query: 'select * from orders limit 20',
}],
activeTabId: 'tab-query-1',
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"hasActiveConnection":true');
expect(result.content).toContain('"connectionName":"主库"');
expect(result.content).toContain('"host":"10.188.101.184"');
expect(result.content).toContain('"port":1523');
expect(result.content).toContain('"activeDbName":"crm"');
expect(result.content).toContain('"useSSH":true');
expect(result.content).toContain('"sshHost":"192.168.66.28"');
expect(result.content).toContain('"activeTabType":"query"');
});
it('returns the current connection capability snapshot so the model can inspect supported UI actions', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_connection_capabilities', {}),
connections: [{
id: 'conn-1',
name: '分析库',
config: {
type: 'clickhouse',
host: '10.10.1.30',
port: 8123,
user: 'default',
database: 'analytics',
},
}],
activeContext: {
connectionId: 'conn-1',
dbName: 'analytics',
},
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"connectionName":"分析库"');
expect(result.content).toContain('"resolvedType":"clickhouse"');
expect(result.content).toContain('"supportsCreateDatabase":true');
expect(result.content).toContain('"supportsRenameDatabase":false');
expect(result.content).toContain('"forceReadOnlyQueryResult":true');
expect(result.content).toContain('force_readonly_query_result');
});
it('returns the local saved connections snapshot so the model can find matching data sources by type or keyword', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_saved_connections', {
type: 'mysql',
keyword: '订单',
}),
connections: [
{
id: 'conn-1',
name: '订单主库',
config: {
type: 'mysql',
host: '10.10.1.18',
port: 3306,
user: 'root',
database: 'crm',
useSSH: true,
ssh: {
host: '192.168.1.8',
port: 22,
user: 'ops',
},
},
},
{
id: 'conn-2',
name: '分析仓库',
config: {
type: 'postgres',
host: '10.10.1.20',
port: 5432,
user: 'analyst',
database: 'dw',
},
},
],
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"totalMatched":1');
expect(result.content).toContain('"typeBreakdown":{"mysql":1}');
expect(result.content).toContain('"name":"订单主库"');
expect(result.content).toContain('"useSSH":true');
expect(result.content).not.toContain('分析仓库');
});
it('returns configured external sql directories so the model can locate local script assets', async () => {
const externalSQLDirectories: ExternalSQLDirectory[] = [
{
id: 'dir-1',
name: '报表脚本',
path: 'D:/sql/reports',
connectionId: 'conn-1',
dbName: 'crm',
createdAt: 2,
},
{
id: 'dir-2',
name: '运维脚本',
path: 'D:/sql/ops',
createdAt: 1,
},
];
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_external_sql_directories', {
keyword: '报表',
}),
connections: [buildConnection()],
tabs: [
{
id: 'tab-1',
title: '日报.sql',
type: 'query',
connectionId: 'conn-1',
dbName: 'crm',
filePath: 'D:/sql/reports/daily.sql',
query: 'select 1',
},
],
mcpTools: [],
toolContextMap: new Map(),
externalSQLDirectories,
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"totalMatched":1');
expect(result.content).toContain('"name":"报表脚本"');
expect(result.content).toContain('"connectionName":"主库"');
expect(result.content).toContain('"openFileTabCount":1');
expect(result.content).toContain('日报.sql');
expect(result.content).not.toContain('运维脚本');
});
it('reads a configured external sql file so the model can inspect script content directly', async () => {
const readSQLFile = vi.fn().mockResolvedValue({
success: true,
data: {
content: 'SELECT * FROM orders WHERE status = \'paid\';',
filePath: 'D:/sql/reports/daily.sql',
name: 'daily.sql',
},
});
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_external_sql_file', {
filePath: 'D:/sql/reports/daily.sql',
previewCharLimit: 18,
}),
connections: [buildConnection()],
tabs: [
{
id: 'tab-1',
title: 'daily.sql',
type: 'query',
connectionId: 'conn-1',
dbName: 'crm',
filePath: 'D:/sql/reports/daily.sql',
query: 'select 1',
},
],
mcpTools: [],
toolContextMap: new Map(),
externalSQLDirectories: [
{
id: 'dir-1',
name: '报表脚本',
path: 'D:/sql/reports',
connectionId: 'conn-1',
dbName: 'crm',
createdAt: 1,
},
],
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
readSQLFile,
},
});
expect(result.success).toBe(true);
expect(readSQLFile).toHaveBeenCalledWith('D:/sql/reports/daily.sql');
expect(result.content).toContain('"fileName":"daily.sql"');
expect(result.content).toContain('"connectionName":"主库"');
expect(result.content).toContain('"hasOpenTab":true');
expect(result.content).toContain('SELECT * FROM orde');
});
it('blocks external sql file reads outside configured directories', async () => {
const readSQLFile = vi.fn();
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_external_sql_file', {
filePath: 'D:/private/secret.sql',
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
externalSQLDirectories: [
{
id: 'dir-1',
name: '报表脚本',
path: 'D:/sql/reports',
connectionId: 'conn-1',
dbName: 'crm',
createdAt: 1,
},
],
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
readSQLFile,
},
});
expect(result.success).toBe(false);
expect(result.content).toContain('目标文件不在已配置的外部 SQL 目录中');
expect(readSQLFile).not.toHaveBeenCalled();
});
it('blocks execute_sql when the AI safety check rejects the statement', async () => {
const query = vi.fn();
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('execute_sql', {
connectionId: 'conn-1',
dbName: 'crm',
sql: 'DELETE FROM users',
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
getColumns: vi.fn(),
getIndexes: vi.fn(),
getForeignKeys: vi.fn(),
getTriggers: vi.fn(),
showCreateTable: vi.fn(),
query,
checkSQL: vi.fn().mockResolvedValue({
allowed: false,
operationType: 'DELETE',
}),
},
});
expect(result.success).toBe(false);
expect(result.content).toContain('安全策略拦截');
expect(query).not.toHaveBeenCalled();
});
it('returns a cross-table column summary for get_all_columns', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('get_all_columns', {
connectionId: 'conn-1',
dbName: 'crm',
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
getAllColumns: vi.fn().mockResolvedValue({
success: true,
data: [
{ TableName: 'users', Name: 'email', Type: 'varchar(255)', Comment: '用户邮箱' },
{ TableName: 'orders', Name: 'user_id', Type: 'bigint', Comment: '关联用户' },
],
}),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"tableCount":2');
expect(result.content).toContain('"tableName":"users"');
expect(result.content).toContain('"name":"email"');
});
it('returns index definitions and resolves the tool label for MCP descriptors', async () => {
const mcpTools: AIMCPToolDescriptor[] = [{
alias: 'custom_tool',
originalName: 'custom_tool',
serverId: 'server-1',
serverName: 'demo',
title: '自定义探针',
description: '',
}];
const indexResult = await executeLocalAIToolCall({
toolCall: buildToolCall('get_indexes', {
connectionId: 'conn-1',
dbName: 'crm',
tableName: 'users',
}),
connections: [buildConnection()],
mcpTools,
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
getColumns: vi.fn(),
getIndexes: vi.fn().mockResolvedValue({
success: true,
data: [{ keyName: 'idx_users_email', nonUnique: 0 }],
}),
getForeignKeys: vi.fn(),
getTriggers: vi.fn(),
showCreateTable: vi.fn(),
query: vi.fn(),
},
});
const message = buildToolResultMessage({
id: 'msg-1',
timestamp: 1,
toolCall: buildToolCall('custom_tool', {}),
execution: {
content: 'ok',
success: true,
toolName: '自定义探针',
},
});
expect(indexResult.success).toBe(true);
expect(indexResult.content).toContain('idx_users_email');
expect(message.tool_name).toBe('自定义探针');
});
it('previews sample rows for a table without forcing the model to handwrite select limit sql', async () => {
const query = vi.fn().mockResolvedValue({
success: true,
data: [
{ id: 1, status: 'paid', amount: 120.5 },
{ id: 2, status: 'pending', amount: null },
],
});
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('preview_table_rows', {
connectionId: 'conn-1',
dbName: 'crm',
tableName: 'orders',
limit: 5,
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
getColumns: vi.fn(),
getIndexes: vi.fn(),
getForeignKeys: vi.fn(),
getTriggers: vi.fn(),
showCreateTable: vi.fn(),
query,
},
});
expect(result.success).toBe(true);
expect(query).toHaveBeenCalledWith(expect.anything(), 'crm', 'SELECT * FROM `orders` LIMIT 5 OFFSET 0');
expect(result.content).toContain('"tableName":"orders"');
expect(result.content).toContain('"status":"paid"');
expect(result.content).toContain('"rowCount":2');
});
it('returns a full table snapshot bundle with optional sample rows in one tool call', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_table_bundle', {
connectionId: 'conn-1',
dbName: 'crm',
tableName: 'orders',
includeSampleRows: true,
sampleLimit: 2,
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
getColumns: vi.fn().mockResolvedValue({
success: true,
data: [{ Field: 'id', Type: 'bigint', Null: 'NO', Comment: '主键' }],
}),
getIndexes: vi.fn().mockResolvedValue({
success: true,
data: [{ keyName: 'PRIMARY', seqInIndex: 1 }],
}),
getForeignKeys: vi.fn().mockResolvedValue({
success: true,
data: [{ columnName: 'user_id', refTable: 'users' }],
}),
getTriggers: vi.fn().mockResolvedValue({
success: true,
data: [{ triggerName: 'orders_bi' }],
}),
showCreateTable: vi.fn().mockResolvedValue({
success: true,
data: [{ ddl: 'CREATE TABLE orders (...)' }],
}),
query: vi.fn().mockResolvedValue({
success: true,
data: [{ id: 1, status: 'paid' }, { id: 2, status: 'pending' }],
}),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"tableName":"orders"');
expect(result.content).toContain('"field":"id"');
expect(result.content).toContain('"keyName":"PRIMARY"');
expect(result.content).toContain('"triggerName":"orders_bi"');
expect(result.content).toContain('"sampleRows"');
expect(result.content).toContain('"status":"paid"');
});
it('returns recent sql logs and supports filtering only failed statements', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_recent_sql_logs', {
limit: 2,
status: 'error',
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
sqlLogs: [
{
id: 'log-1',
timestamp: 3,
sql: 'DELETE FROM users WHERE id = 9',
status: 'error',
duration: 120,
message: 'permission denied',
dbName: 'crm',
},
{
id: 'log-2',
timestamp: 2,
sql: 'SELECT * FROM users LIMIT 10',
status: 'success',
duration: 18,
dbName: 'crm',
affectedRows: 10,
},
{
id: 'log-3',
timestamp: 1,
sql: 'UPDATE orders SET status = \'paid\' WHERE id = 1',
status: 'error',
duration: 95,
message: 'row lock timeout',
dbName: 'crm',
},
],
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"status":"error"');
expect(result.content).toContain('"totalMatched":2');
expect(result.content).toContain('permission denied');
expect(result.content).toContain('row lock timeout');
expect(result.content).not.toContain('SELECT * FROM users LIMIT 10');
});
it('returns a recent sql activity summary so the model can quickly spot writes, ddl, and repeated failures', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_recent_sql_activity', {
limit: 3,
activityKind: 'write',
dbName: 'crm',
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
sqlLogs: [
{
id: 'log-1',
timestamp: 4,
sql: 'DELETE FROM users WHERE id = 9',
status: 'error',
duration: 120,
message: 'permission denied',
dbName: 'crm',
},
{
id: 'log-2',
timestamp: 3,
sql: 'UPDATE orders SET status = \'paid\' WHERE id = 1',
status: 'error',
duration: 95,
message: 'row lock timeout',
dbName: 'crm',
},
{
id: 'log-3',
timestamp: 2,
sql: 'ALTER TABLE orders ADD COLUMN note varchar(32)',
status: 'success',
duration: 160,
dbName: 'crm',
},
{
id: 'log-4',
timestamp: 1,
sql: 'SELECT * FROM users LIMIT 10',
status: 'success',
duration: 18,
dbName: 'crm',
affectedRows: 10,
},
],
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"activityKind":"write"');
expect(result.content).toContain('"totalMatched":2');
expect(result.content).toContain('"writeCount":2');
expect(result.content).toContain('"statementTypeBreakdown":{"delete":1,"update":1}');
expect(result.content).toContain('permission denied');
expect(result.content).toContain('row lock timeout');
expect(result.content).not.toContain('ALTER TABLE orders');
expect(result.content).not.toContain('SELECT * FROM users LIMIT 10');
});
it('returns local saved queries so the model can reuse historical sql scripts', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_saved_queries', {
keyword: '支付',
connectionId: 'conn-1',
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
savedQueries: [
{
id: 'saved-1',
name: '支付订单核对',
sql: 'SELECT * FROM orders WHERE status = \'paid\'',
connectionId: 'conn-1',
dbName: 'crm',
createdAt: 2,
},
{
id: 'saved-2',
name: '用户列表',
sql: 'SELECT * FROM users',
connectionId: 'conn-1',
dbName: 'crm',
createdAt: 1,
},
],
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"totalMatched":1');
expect(result.content).toContain('支付订单核对');
expect(result.content).toContain('"connectionName":"主库"');
expect(result.content).toContain('status = \'paid\'');
});
it('returns local ai chat sessions so the model can locate previous conversations by title or preview', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_ai_sessions', {
keyword: '支付',
limit: 5,
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
aiChatSessions: [
{ id: 'session-1', title: '支付异常排查', updatedAt: 200 },
{ id: 'session-2', title: '用户列表', updatedAt: 100 },
],
aiChatHistory: {
'session-1': [
{ id: 'msg-1', role: 'user', content: '帮我排查支付超时', timestamp: 101 },
{ id: 'msg-2', role: 'assistant', content: '先看最近错误日志', timestamp: 102 },
],
'session-2': [
{ id: 'msg-3', role: 'user', content: '列出最近注册用户', timestamp: 103 },
],
},
activeSessionId: 'session-2',
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"totalMatched":1');
expect(result.content).toContain('支付异常排查');
expect(result.content).toContain('帮我排查支付超时');
expect(result.content).toContain('先看最近错误日志');
expect(result.content).not.toContain('列出最近注册用户');
});
it('returns sql snippets so the model can inspect local query templates', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_sql_snippets', {
keyword: '支付',
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
sqlSnippets: [
{
id: 'snippet-1',
prefix: 'sel',
name: 'SELECT 模板',
body: 'SELECT * FROM ${1:table};',
isBuiltin: true,
createdAt: 1,
},
{
id: 'snippet-2',
prefix: 'pay',
name: '支付模板',
description: '支付对账',
body: 'SELECT * FROM pay_orders WHERE created_at >= ${1:start};',
isBuiltin: false,
createdAt: 2,
},
],
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"totalMatched":1');
expect(result.content).toContain('"prefix":"pay"');
expect(result.content).toContain('"customCount":1');
expect(result.content).toContain('pay_orders');
});
it('returns a database overview bundle with per-table column previews in one tool call', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_database_bundle', {
connectionId: 'conn-1',
dbName: 'crm',
tableLimit: 5,
perTableColumnLimit: 1,
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn().mockResolvedValue({
success: true,
data: [{ Table: 'users' }, { Table: 'orders' }],
}),
getAllColumns: vi.fn().mockResolvedValue({
success: true,
data: [
{ TableName: 'users', Name: 'id', Type: 'bigint', Comment: '主键' },
{ TableName: 'users', Name: 'email', Type: 'varchar(255)', Comment: '邮箱' },
{ TableName: 'orders', Name: 'id', Type: 'bigint', Comment: '主键' },
],
}),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"dbName":"crm"');
expect(result.content).toContain('"tableCount":2');
expect(result.content).toContain('"tableName":"users"');
expect(result.content).toContain('"columnCount":2');
expect(result.content).toContain('"truncatedColumns":true');
});
});