mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 10:29:52 +08:00
♻️ refactor(ai-tests): 拆分连接与本地资产探针测试
- 将连接能力、外部 SQL、本地查询资产探针测试拆入独立文件 - 继续缩减 aiLocalToolExecutor.test.ts,降低单文件维护成本 - 通过定向测试和生产构建验证
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
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 connection inspection tools', () => {
|
||||
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('分析仓库');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { AIToolCall, ExternalSQLDirectory, 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 external SQL inspection tools', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
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 local asset inspection tools', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { AIMCPToolDescriptor, AIToolCall, ExternalSQLDirectory, SavedConnection } from '../../types';
|
||||
import type { AIMCPToolDescriptor, AIToolCall, SavedConnection } from '../../types';
|
||||
import { buildToolResultMessage, executeLocalAIToolCall } from './aiLocalToolExecutor';
|
||||
|
||||
const buildConnection = (): SavedConnection => ({
|
||||
@@ -169,280 +169,6 @@ describe('aiLocalToolExecutor', () => {
|
||||
expect(result.content).toContain('CREATE TABLE orders');
|
||||
});
|
||||
|
||||
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({
|
||||
@@ -755,123 +481,6 @@ describe('aiLocalToolExecutor', () => {
|
||||
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', {
|
||||
|
||||
Reference in New Issue
Block a user