Files
MyGoNavi/frontend/src/components/AIChatPanel.message-boundary.test.tsx
2026-06-22 15:12:42 +08:00

101 lines
5.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { t as catalogTranslate } from '../i18n/catalog';
const source = readFileSync(new URL('./AIChatPanel.tsx', import.meta.url), 'utf8');
const testSource = readFileSync(new URL('./AIChatPanel.message-boundary.test.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 REQUIRED_RENDER_BOUNDARY_KEYS = [
'ai_chat.message.render_error.title',
'ai_chat.message.render_error.body',
'ai_chat.message.render_error.unknown',
'ai_chat.message.render_error.retry',
'ai_chat.message.render_error.delete',
] as const;
describe('AIChatPanel merge resolution', () => {
it('clears conflict markers from the merged files', () => {
expect(source).not.toMatch(/^<{7}|^={7}|^>{7}/m);
expect(testSource).not.toMatch(/^<{7}|^={7}|^>{7}/m);
});
it('keeps dev split architecture while retaining render-boundary isolation', () => {
expect(source).toContain("import AIChatPanelConversationView from './ai/AIChatPanelConversationView';");
expect(source).toContain("import { useAIChatRuntimeResources } from './ai/useAIChatRuntimeResources';");
expect(source).toContain("import { useAIChatStreamSubscription } from './ai/useAIChatStreamSubscription';");
expect(source).toContain("import { useAIChatLocalTools } from './ai/useAIChatLocalTools';");
expect(boundarySource).toContain('class AIMessageRenderBoundary extends React.Component');
expect(conversationViewSource).toContain("import AIMessageRenderBoundary from './AIMessageRenderBoundary';");
expect(conversationViewSource).toContain('<AIMessageRenderBoundary');
expect(source).toContain('onMessageRenderError={handleMessageRenderError}');
expect(source).toContain('__gonaviLastAIMessageRenderError');
expect(source).toContain('[AI Message Render Error]');
});
it('keeps render-boundary recovery chrome translated through catalog keys', () => {
expect(boundarySource).toContain('useOptionalI18n()');
expect(boundarySource).toContain("catalogTranslate('en-US'");
for (const key of REQUIRED_RENDER_BOUNDARY_KEYS) {
expect(catalogTranslate('en-US', key)).not.toBe(key);
expect(catalogTranslate('zh-CN', key)).not.toBe(key);
expect(boundarySource).toContain(key);
}
for (const oldCopy of [
'这条 AI 消息渲染失败,已自动隔离',
'其余对话仍可继续使用。你可以先删除这条异常消息,再继续操作。',
'未知渲染错误',
'重试渲染',
'删除这条消息',
]) {
expect(boundarySource).not.toContain(oldCopy);
}
});
it('restores panel-level i18n orchestration for composer notices and send lifecycle text', () => {
expect(source).toContain("import { useI18n } from '../i18n/provider';");
expect(source).toContain("import type { AIComposerNoticeDescriptor } from '../utils/aiComposerNotice';");
expect(source).toContain("import { buildAIComposerNotice } from '../utils/aiComposerNotice';");
expect(source).toContain("const { t } = useI18n();");
expect(source).toContain("const [composerNoticeState, setComposerNoticeState] = useState<AIComposerNoticeDescriptor | null>(null);");
expect(source).toContain("buildAIComposerNotice(t, composerNoticeState) ?? runtimeComposerNotice");
expect(source).toContain("setComposerNoticeState({ kind: 'missing_provider' });");
expect(source).toContain("setComposerNoticeState({ kind: 'provider_incomplete', issues: readiness.issues });");
expect(source).toContain("setComposerNoticeState({ kind: 'missing_model' });");
expect(source).toContain('const chatMessages = [...messages, userMsg].map((message) => toAIRequestMessage(message, t));');
expect(source).toContain('toAIRequestMessage(userMsg, t)');
for (const key of [
'ai_chat.panel.status.model_connecting',
'ai_chat.panel.status.waking_engine',
'ai_chat.panel.status.waiting_response',
'ai_chat.panel.status.memory_summary',
'ai_chat.panel.message.service_not_ready',
]) {
expect(source).toContain(`t('${key}'`);
}
expect(source).not.toContain('buildMissingProviderNotice');
expect(source).not.toContain('buildIncompleteProviderNotice');
expect(source).not.toContain('buildMissingModelNotice');
});
it('keeps translated session and insight chrome in the panel layer instead of falling back to hardcoded copy', () => {
expect(source).toContain("() => orderedAISessions.find((session) => session.id === sid)?.title || t('ai_chat.panel.session.default_title')");
expect(source).toContain("title: session.title || t('ai_chat.panel.session.default_title')");
expect(source).toContain('buildAIChatInsights({');
expect(source).toContain('translate: t,');
expect(derivedStateSource).toContain("translate('ai_chat.panel.insight.context.linked_title', { count: contextCount })");
expect(derivedStateSource).toContain("translate('ai_chat.panel.insight.context.linked_body', { tables: tablePreview })");
expect(derivedStateSource).toContain("translate('ai_chat.panel.insight.query.slowest_title', { duration: Math.round(slowest.duration).toLocaleString() })");
expect(derivedStateSource).toContain("translate('ai_chat.panel.insight.status.recent_body', { count: recentLogs.length })");
expect(derivedStateSource).toContain("translate('ai_chat.panel.insight.write.detected_title', { count: writeCount })");
expect(source).not.toContain("|| '新对话'");
});
});