Files
MyGoNavi/frontend/src/components/AIChatPanel.message-boundary.test.tsx
Syngnat f0afff68c4 ♻️ refactor(ai-chat): 拆分本地工具调用链
- 抽出 useAIChatLocalTools 承载工具执行、熔断和回灌模型逻辑

- 补齐重试消息的工具上下文依赖,避免配置变更后使用旧闭包

- 增加 hook 行为测试并同步 MCP 指南断言
2026-06-09 21:18:39 +08:00

113 lines
8.2 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
const source = readFileSync(new URL('./AIChatPanel.tsx', import.meta.url), 'utf8');
const boundarySource = readFileSync(new URL('./ai/AIMessageRenderBoundary.tsx', import.meta.url), 'utf8');
const conversationViewSource = readFileSync(new URL('./ai/AIChatPanelConversationView.tsx', import.meta.url), 'utf8');
const derivedStateSource = readFileSync(new URL('./ai/aiChatPanelDerivedState.ts', import.meta.url), 'utf8');
const autoContextSource = readFileSync(new URL('./ai/useAIChatAutoContext.ts', import.meta.url), 'utf8');
const payloadDispatchSource = readFileSync(new URL('./ai/aiChatPayloadDispatch.ts', import.meta.url), 'utf8');
const planContextSource = readFileSync(new URL('./ai/useAIChatPlanContexts.ts', import.meta.url), 'utf8');
const resizeSource = readFileSync(new URL('./ai/useAIChatPanelResize.ts', import.meta.url), 'utf8');
const runtimeResourcesSource = readFileSync(new URL('./ai/useAIChatRuntimeResources.ts', import.meta.url), 'utf8');
const sessionStateSource = readFileSync(new URL('./ai/useAIChatSessionState.ts', import.meta.url), 'utf8');
const titleGeneratorSource = readFileSync(new URL('./ai/useAIChatSessionTitleGenerator.ts', import.meta.url), 'utf8');
const localToolsSource = readFileSync(new URL('./ai/useAIChatLocalTools.ts', import.meta.url), 'utf8');
const streamSubscriptionSource = readFileSync(new URL('./ai/useAIChatStreamSubscription.ts', import.meta.url), 'utf8');
const inspectionGuidanceSource = readFileSync(new URL('./ai/aiSystemInspectionGuidance.ts', import.meta.url), 'utf8');
const systemContextSource = readFileSync(new URL('./ai/aiSystemContextMessages.ts', import.meta.url), 'utf8');
const runtimeSource = readFileSync(new URL('../utils/aiChatRuntime.ts', import.meta.url), 'utf8');
describe('AIChatPanel message render isolation', () => {
it('keeps per-message render failures scoped to the broken bubble', () => {
expect(source).toContain("import AIChatPanelConversationView from './ai/AIChatPanelConversationView';");
expect(boundarySource).toContain('class AIMessageRenderBoundary extends React.Component');
expect(source).toContain('[AI Message Render Error]');
expect(conversationViewSource).toContain("import AIMessageRenderBoundary from './AIMessageRenderBoundary';");
expect(boundarySource).toContain('这条 AI 消息渲染失败,已自动隔离');
expect(source).toContain('__gonaviLastAIMessageRenderError');
expect(conversationViewSource).toContain('<AIMessageRenderBoundary');
expect(conversationViewSource).toContain('onDeleteMessage={onDeleteMessage}');
});
it('loads user prompt settings and appends them as system messages', () => {
expect(source).toContain("import { useAIChatRuntimeResources } from './ai/useAIChatRuntimeResources';");
expect(source).toContain('useAIChatRuntimeResources({ onOpenSettings })');
expect(runtimeResourcesSource).toContain('AIGetUserPromptSettings');
expect(runtimeResourcesSource).toContain("window.addEventListener('gonavi:ai:config-changed'");
expect(systemContextSource).toContain('以下是当前用户的自定义补充提示词');
expect(systemContextSource).toContain("appendCustomPromptGroup(systemMessages, ['database']");
});
it('loads MCP tools and skills into the runtime tool chain', () => {
expect(runtimeResourcesSource).toContain('AIListMCPTools');
expect(runtimeResourcesSource).toContain('AIGetSkills');
expect(source).toContain("import { useAIChatLocalTools } from './ai/useAIChatLocalTools';");
expect(localToolsSource).toContain('executeLocalAIToolCall');
expect(systemContextSource).toContain('以下是当前启用的 Skill');
expect(source).toContain('buildAvailableAIChatTools');
});
it('teaches the runtime to use deeper schema tools when analyzing structure details', () => {
expect(systemContextSource).toContain('get_indexes、get_foreign_keys、get_triggers、get_table_ddl');
expect(systemContextSource).toContain('inspect_active_tab 读取当前活动页签上下文');
expect(systemContextSource).toContain('inspect_workspace_tabs 盘点当前工作区');
expect(inspectionGuidanceSource).toContain('inspect_current_connection');
expect(inspectionGuidanceSource).toContain('inspect_external_sql_directories');
expect(inspectionGuidanceSource).toContain('inspect_external_sql_file');
expect(localToolsSource).toContain('tabs: currentState.tabs');
expect(localToolsSource).toContain('activeTabId: currentState.activeTabId');
expect(localToolsSource).toContain('externalSQLDirectories: currentState.externalSQLDirectories');
expect(localToolsSource).toContain('toolContextMap: toolContextMapRef.current');
expect(localToolsSource).toContain('buildToolResultMessage');
});
it('extracts chat runtime helpers so context compression and error cleanup stay out of the panel file', () => {
expect(source).toContain("import { dispatchAIChatPayload } from './ai/aiChatPayloadDispatch';");
expect(source).toContain("import { useAIChatStreamSubscription } from './ai/useAIChatStreamSubscription';");
expect(source).toContain('compressContextIfNeeded, getDynamicMaxContextChars');
expect(source).toContain('useAIChatStreamSubscription({');
expect(source).toContain('useAIChatLocalTools({');
expect(runtimeSource).toContain('export const getDynamicMaxContextChars');
expect(runtimeSource).toContain('export const compressContextIfNeeded');
expect(runtimeSource).toContain('export const sanitizeErrorMsg');
expect(payloadDispatchSource).toContain('export const dispatchAIChatPayload');
expect(payloadDispatchSource).toContain('sanitizeErrorMsg');
expect(localToolsSource).toContain('compressContextIfNeeded');
expect(localToolsSource).toContain('dispatchAIChatPayload');
expect(streamSubscriptionSource).toContain('EventsOn(eventName, handler);');
expect(streamSubscriptionSource).toContain('请直接使用 function call 调用工具执行操作');
expect(streamSubscriptionSource).toContain('executeLocalTools(existing.tool_calls!, doneAssistantId)');
expect(runtimeSource).toContain('⚙️ 对话已超载,正在启动记忆压缩');
});
it('keeps the v2 history mode sorted by the latest updated session first', () => {
expect(source).toContain("import { useAIChatSessionState } from './ai/useAIChatSessionState';");
expect(source).toContain('const panelHistorySessions = useMemo(');
expect(sessionStateSource).toContain('right.updatedAt - left.updatedAt');
expect(sessionStateSource).toContain("const sid = aiActiveSessionId || 'session-fallback';");
expect(source).toContain('buildAIChatInlineHistorySessions(orderedAISessions)');
expect(derivedStateSource).toContain('export const buildAIChatInlineHistorySessions');
expect(derivedStateSource).toContain('sessions.slice(0, limit)');
expect(source).toContain('sessions={panelHistorySessions}');
});
it('extracts plan-context, auto-context, title, and resize hooks so the panel file stays focused on orchestration', () => {
expect(source).toContain("import { useAIChatPlanContexts } from './ai/useAIChatPlanContexts';");
expect(source).toContain("import { useAIChatAutoContext } from './ai/useAIChatAutoContext';");
expect(source).toContain("import { useAIChatSessionTitleGenerator } from './ai/useAIChatSessionTitleGenerator';");
expect(source).toContain("import { useAIChatPanelResize } from './ai/useAIChatPanelResize';");
expect(source).toContain("import { useAIChatLocalTools } from './ai/useAIChatLocalTools';");
expect(planContextSource).toContain('export const useAIChatPlanContexts');
expect(planContextSource).toContain('pendingJVMPlanContextRef');
expect(autoContextSource).toContain('export const useAIChatAutoContext');
expect(autoContextSource).toContain('DBShowCreateTable');
expect(titleGeneratorSource).toContain('export const useAIChatSessionTitleGenerator');
expect(titleGeneratorSource).toContain('Failed to auto-generate title');
expect(resizeSource).toContain('export const useAIChatPanelResize');
expect(resizeSource).toContain('document.body.style.pointerEvents = \'none\'');
expect(localToolsSource).toContain('export const useAIChatLocalTools');
expect(localToolsSource).toContain('MAX_TOOL_CALL_ROUNDS');
});
});