diff --git a/frontend/src/components/ai/aiLocalToolExecutor.connectionInspection.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.connectionInspection.test.ts new file mode 100644 index 0000000..6ec3b93 --- /dev/null +++ b/frontend/src/components/ai/aiLocalToolExecutor.connectionInspection.test.ts @@ -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): 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('分析仓库'); + }); +}); diff --git a/frontend/src/components/ai/aiLocalToolExecutor.externalSqlInspection.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.externalSqlInspection.test.ts new file mode 100644 index 0000000..69df613 --- /dev/null +++ b/frontend/src/components/ai/aiLocalToolExecutor.externalSqlInspection.test.ts @@ -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): 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(); + }); +}); diff --git a/frontend/src/components/ai/aiLocalToolExecutor.localAssetsInspection.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.localAssetsInspection.test.ts new file mode 100644 index 0000000..59a73f1 --- /dev/null +++ b/frontend/src/components/ai/aiLocalToolExecutor.localAssetsInspection.test.ts @@ -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): 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'); + }); +}); diff --git a/frontend/src/components/ai/aiLocalToolExecutor.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.test.ts index 3f557ba..738d865 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.test.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.test.ts @@ -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', {