mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 16:53:35 +08:00
✅ test(ai): 改用行为断言验证代码块依赖边界 (#785)
## 问题背景 `upstream/dev` 的前端全量测试被 `src/testPolicy.test.ts` 阻断。`AIMessageCodeBlock.dependencyBoundary.test.ts` 通过 `readFileSync` 读取 `.ts/.tsx` 源码并用正则断言实现文本,违反项目“禁止新增源码文本断言”的测试策略。 该文件没有进入债务基线,且不属于经认可的全仓库不变式扫描,因此不能通过增加豁免隐藏问题。 ## 修复方案 只重写依赖边界测试,不修改生产代码、Vite 配置、测试策略或债务基线: - 通过模块 mock 的实际求值行为,验证 AI 代码块加载轻量 `prism-light` 入口而不是完整语言注册表。 - 先渲染普通 SQL 代码块,再切换到 Mermaid 代码块,验证 Mermaid 仅在对应组件挂载后加载、初始化并渲染。 - 直接导入 Vite 配置对象,验证 Prism 入口、语言和主题依赖都包含在 `optimizeDeps.include` 中。 这使测试执行实际模块加载与 React effect,不再把源码当字符串锁定。 ## 验证结果 | 验证项 | 命令 | 结果 | | --- | --- | --- | | 修复前基线 | `npm --prefix frontend test -- src/testPolicy.test.ts` | 按预期失败,唯一 offender 为本次修改文件 | | 聚焦测试 | `npm --prefix frontend test -- src/components/ai/messageBubble/AIMessageCodeBlock.dependencyBoundary.test.ts src/components/ai/messageBubble/AIMessageMarkdown.test.tsx src/testPolicy.test.ts` | 通过,3 个文件、11/11 | | 前端全量测试 | `npm --prefix frontend test` | 通过,421 个文件、3481/3481 | | 类型与生产构建 | `npm --prefix frontend run build` | 通过,`tsc && vite build` 成功 | | 差异检查 | `git diff --cached --check` | 通过 | ## 风险与兼容性 - 只修改测试文件,不改变运行时代码、公共 API、配置、数据格式或依赖。 - 模块 mock 能验证加载入口与加载时机,但不等同于完整的 Rollup 分块分析;最终生产构建已实际执行并通过。 - Vite 预打包依赖清单通过配置对象断言,新增或删除 AI 高亮语言时需要同步更新测试期望。 ## 回滚方式 回滚提交 `caba7b8cbb9bed38c27e1fde4c668936957a3cd0` 即可恢复原测试。回滚不影响运行时数据或配置,但会重新触发当前的测试策略失败。
This commit is contained in:
@@ -1,27 +1,133 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import React from 'react';
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./AIMessageCodeBlock.tsx', import.meta.url), 'utf8');
|
||||
const viteConfigSource = readFileSync(new URL('../../../../vite.config.ts', import.meta.url), 'utf8');
|
||||
import viteConfig from '../../../../vite.config';
|
||||
import { buildOverlayWorkbenchTheme } from '../../../utils/overlayWorkbenchTheme';
|
||||
import { AIMessageCodeBlock } from './AIMessageCodeBlock';
|
||||
|
||||
const dependencyMocks = vi.hoisted(() => ({
|
||||
completeRegistryLoad: vi.fn(),
|
||||
mermaidInitialize: vi.fn(),
|
||||
mermaidLoad: vi.fn(),
|
||||
mermaidRender: vi.fn(async () => ({ svg: '<svg>diagram</svg>' })),
|
||||
prismLightLoad: vi.fn(),
|
||||
registerLanguage: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('react-syntax-highlighter', () => {
|
||||
dependencyMocks.completeRegistryLoad();
|
||||
return { default: () => null };
|
||||
});
|
||||
|
||||
vi.mock('react-syntax-highlighter/dist/esm/prism-light', () => {
|
||||
dependencyMocks.prismLightLoad();
|
||||
return {
|
||||
default: Object.assign(() => null, {
|
||||
registerLanguage: dependencyMocks.registerLanguage,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('mermaid', () => {
|
||||
dependencyMocks.mermaidLoad();
|
||||
return {
|
||||
default: {
|
||||
initialize: dependencyMocks.mermaidInitialize,
|
||||
render: dependencyMocks.mermaidRender,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('antd', () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => children,
|
||||
message: { error: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('@ant-design/icons', () => ({
|
||||
CheckOutlined: () => null,
|
||||
CopyOutlined: () => null,
|
||||
PlayCircleOutlined: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('../../common/ResizableDraggableModal', () => ({
|
||||
default: Object.assign(() => null, { confirm: vi.fn() }),
|
||||
}));
|
||||
|
||||
const codeHighlightDependencies = [
|
||||
'react-syntax-highlighter/dist/esm/prism-light',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/bash',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/css',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/diff',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/go',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/ini',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/java',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/javascript',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/json',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/jsx',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/markdown',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/markup',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/php',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/python',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/ruby',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/rust',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/sql',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/toml',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/tsx',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/typescript',
|
||||
'react-syntax-highlighter/dist/esm/languages/prism/yaml',
|
||||
'react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus',
|
||||
'react-syntax-highlighter/dist/esm/styles/prism/vs',
|
||||
];
|
||||
|
||||
const renderCodeBlock = (className: string, children: string) => React.createElement(AIMessageCodeBlock, {
|
||||
className,
|
||||
children,
|
||||
darkMode: false,
|
||||
overlayTheme: buildOverlayWorkbenchTheme(false),
|
||||
});
|
||||
|
||||
describe('AIMessageCodeBlock dependency boundary', () => {
|
||||
it('does not pull the complete syntax-highlighter language registry into the AI panel chunk', () => {
|
||||
expect(source).not.toMatch(/from\s+['"]react-syntax-highlighter['"]/);
|
||||
expect(source).toMatch(/react-syntax-highlighter\/dist\/esm\/prism-light/);
|
||||
it('loads the lightweight syntax highlighter without the complete language registry', () => {
|
||||
expect(dependencyMocks.completeRegistryLoad).not.toHaveBeenCalled();
|
||||
expect(dependencyMocks.prismLightLoad).toHaveBeenCalledOnce();
|
||||
expect(dependencyMocks.registerLanguage).toHaveBeenCalledTimes(20);
|
||||
});
|
||||
|
||||
it('loads Mermaid only when a Mermaid fenced block is rendered', () => {
|
||||
expect(source).not.toMatch(/^import\s+mermaid\s+from\s+['"]mermaid['"];?$/m);
|
||||
expect(source).toMatch(/await\s+import\(['"]mermaid['"]\)/);
|
||||
});
|
||||
it('loads Mermaid only when a Mermaid fenced block is rendered', async () => {
|
||||
let renderer: ReactTestRenderer | undefined;
|
||||
let mermaidContainer: { innerHTML: string } | undefined;
|
||||
|
||||
it('pre-bundles every static syntax-highlighter entry before Wails opens the panel', () => {
|
||||
const syntaxHighlighterImports = Array.from(source.matchAll(
|
||||
/from\s+['"](react-syntax-highlighter\/dist\/esm\/[^'"]+)['"]/g,
|
||||
)).map((match) => match[1]);
|
||||
try {
|
||||
act(() => {
|
||||
renderer = create(renderCodeBlock('language-sql', 'SELECT 1;'), {
|
||||
createNodeMock: (element) => {
|
||||
if (element.type === 'div' && element.props.className === 'ai-mermaid-container') {
|
||||
mermaidContainer = { innerHTML: '' };
|
||||
return mermaidContainer;
|
||||
}
|
||||
return {};
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(dependencyMocks.mermaidLoad).not.toHaveBeenCalled();
|
||||
|
||||
for (const dependency of syntaxHighlighterImports) {
|
||||
expect(viteConfigSource).toContain(`'${dependency}'`);
|
||||
await act(async () => {
|
||||
renderer?.update(renderCodeBlock('language-mermaid', 'graph TD; A-->B;'));
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(dependencyMocks.mermaidLoad).toHaveBeenCalledOnce();
|
||||
expect(dependencyMocks.mermaidInitialize).toHaveBeenCalledWith({ startOnLoad: false, theme: 'default' });
|
||||
expect(dependencyMocks.mermaidRender).toHaveBeenCalledWith(expect.stringMatching(/^mermaid-/), 'graph TD; A-->B;');
|
||||
expect(mermaidContainer?.innerHTML).toBe('<svg>diagram</svg>');
|
||||
});
|
||||
} finally {
|
||||
act(() => renderer?.unmount());
|
||||
}
|
||||
});
|
||||
|
||||
it('pre-bundles every static syntax-highlighter dependency', () => {
|
||||
const includedDependencies = viteConfig.optimizeDeps?.include || [];
|
||||
expect(includedDependencies).toEqual(expect.arrayContaining(codeHighlightDependencies));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user