Files
linshen 8467b96263 feat(test): 实现智能测试系统并改进测试策略
- 实现智能测试系统,统一测试入口
- 完成 Pro 路由重构和 Image 模式评估测试
- 为 Image 模式添加 data-testid 支持
- 完善 E2E 测试并修复选择器策略
- 优化 Seedream、OpenAI 和 SiliconFlow 适配器
- 移除调试日志,保留错误和警告日志
- 重命名 Pro 模式子模式(system/user → multi/variable)
- 清理测试代码并改进稳定性
2026-01-10 11:23:00 +08:00

348 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 变量提取服务 - 真实API集成测试
*
* 测试变量提取服务与真实LLM API的集成
* 只有在环境变量存在时才执行
*/
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { createVariableExtractionService } from '../../../src/services/variable-extraction/service';
import { createTemplateManager } from '../../../src/services/template/manager';
import { createTemplateLanguageService } from '../../../src/services/template/languageService';
import { LocalStorageProvider } from '../../../src/services/storage/localStorageProvider';
import {
createRealLLMTestContext,
hasAvailableProvider,
printAvailableProviders,
type RealLLMTestContext,
} from '../../helpers/real-llm';
import type {
IVariableExtractionService,
VariableExtractionRequest,
} from '../../../src/services/variable-extraction/types';
import type { ITemplateManager } from '../../../src/services/template/types';
const RUN_REAL_API = process.env.RUN_REAL_API === '1';
describe.skipIf(!RUN_REAL_API)('VariableExtractionService - Real API Integration', () => {
let context: RealLLMTestContext | undefined;
let variableExtractionService: IVariableExtractionService;
let templateManager: ITemplateManager;
let storage: LocalStorageProvider;
beforeAll(() => {
printAvailableProviders();
if (!hasAvailableProvider()) {
}
});
beforeEach(async () => {
// 先创建存储和模板管理器
storage = new LocalStorageProvider();
await storage.clearAll();
const languageService = createTemplateLanguageService(storage);
templateManager = createTemplateManager(storage, languageService);
// 创建真实LLM测试上下文它会使用自己的存储和modelManager
context = await createRealLLMTestContext({
paramOverrides: {
temperature: 0.7,
// 不使用max_tokens让系统使用默认值
},
});
if (!context) {
return;
}
// 使用context返回的modelManager创建变量提取服务
variableExtractionService = createVariableExtractionService(
context.llmService,
context.modelManager, // 使用context的modelManager
templateManager
);
});
describe('基础变量提取测试', () => {
it.skipIf(!hasAvailableProvider())('应该能成功提取简单提示词中的变量', async () => {
if (!context) {
return;
}
const request: VariableExtractionRequest = {
promptContent: '请写一篇关于春天的文章字数要求在500字以内。',
extractionModelKey: context.modelKey,
existingVariableNames: [],
};
const result = await variableExtractionService.extract(request);
// 验证返回结构
expect(result).toBeDefined();
expect(result.variables).toBeInstanceOf(Array);
expect(result.summary).toBeDefined();
expect(typeof result.summary).toBe('string');
// 打印结果
if (result.variables.length > 0) {
result.variables.forEach((v, index) => {
});
// 验证第一个变量的结构
const firstVar = result.variables[0];
expect(firstVar.name).toBeDefined();
expect(typeof firstVar.name).toBe('string');
expect(firstVar.value).toBeDefined();
expect(typeof firstVar.value).toBe('string');
expect(firstVar.position).toBeDefined();
expect(firstVar.position.originalText).toBeDefined();
expect(typeof firstVar.position.occurrence).toBe('number');
expect(firstVar.position.occurrence).toBeGreaterThan(0);
expect(firstVar.reason).toBeDefined();
expect(typeof firstVar.reason).toBe('string');
// 验证变量名符合规范(中文/英文/数字/下划线,不以数字开头)
expect(firstVar.name).toMatch(/^[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*$/);
}
}, 60000);
it.skipIf(!hasAvailableProvider())('应该能提取包含多个变量的复杂提示词', async () => {
if (!context) {
return;
}
const request: VariableExtractionRequest = {
promptContent: `作为一名专业的小说作家,请创作一篇科幻小说。
要求:
- 主题:人工智能
- 风格:悬疑紧张
- 字数3000字
- 目标读者:成年人
- 叙事视角:第一人称
请确保故事情节引人入胜,人物性格鲜明。`,
extractionModelKey: context.modelKey,
existingVariableNames: [],
};
const result = await variableExtractionService.extract(request);
// 应该提取到多个变量(主题、风格、字数、目标读者、叙事视角等)
expect(result.variables.length).toBeGreaterThan(0);
if (result.variables.length > 0) {
result.variables.forEach((v, index) => {
if (v.category) {
}
});
// 验证所有变量都有有效的定位信息
result.variables.forEach((v) => {
expect(v.position.originalText).toBeTruthy();
expect(request.promptContent).toContain(v.position.originalText);
});
}
}, 60000);
it.skipIf(!hasAvailableProvider())('应该避免与已存在变量重名', async () => {
if (!context) {
return;
}
const request: VariableExtractionRequest = {
promptContent: '请写一篇关于春天的文章字数要求在500字以内。',
extractionModelKey: context.modelKey,
existingVariableNames: ['season', 'topic', '季节', '主题', 'word_count'],
};
const result = await variableExtractionService.extract(request);
if (result.variables.length > 0) {
result.variables.forEach((v, index) => {
// 验证没有重名
expect(request.existingVariableNames).not.toContain(v.name);
});
}
}, 60000);
});
describe('错误处理测试', () => {
it.skipIf(!hasAvailableProvider())('应该在提示词为空时抛出验证错误', async () => {
if (!context) {
return;
}
const request: VariableExtractionRequest = {
promptContent: '',
extractionModelKey: context.modelKey,
existingVariableNames: [],
};
await expect(variableExtractionService.extract(request)).rejects.toThrow();
});
it.skipIf(!hasAvailableProvider())('应该在模型不存在时抛出模型错误', async () => {
if (!context) {
return;
}
const request: VariableExtractionRequest = {
promptContent: '测试提示词',
extractionModelKey: 'non-existent-model',
existingVariableNames: [],
};
await expect(variableExtractionService.extract(request)).rejects.toThrow();
});
});
describe('特殊场景测试', () => {
it.skipIf(!hasAvailableProvider())('应该能处理包含变量标记{{}}的提示词', async () => {
if (!context) {
return;
}
const request: VariableExtractionRequest = {
promptContent: '请根据{{用户输入}}生成一篇关于人工智能的文章。',
extractionModelKey: context.modelKey,
existingVariableNames: ['用户输入'],
};
const result = await variableExtractionService.extract(request);
if (result.variables.length > 0) {
result.variables.forEach((v, index) => {
});
}
// 变量可能包括"人工智能"等内容
expect(result).toBeDefined();
}, 60000);
it.skipIf(!hasAvailableProvider())('应该能处理纯英文提示词', async () => {
if (!context) {
return;
}
const request: VariableExtractionRequest = {
promptContent: 'Write a story about artificial intelligence in 500 words.',
extractionModelKey: context.modelKey,
existingVariableNames: [],
};
const result = await variableExtractionService.extract(request);
if (result.variables.length > 0) {
result.variables.forEach((v, index) => {
});
// 验证变量名符合规范
result.variables.forEach((v) => {
expect(v.name).toMatch(/^[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*$/);
});
}
}, 60000);
it.skipIf(!hasAvailableProvider())('应该能处理没有明显变量的提示词', async () => {
if (!context) {
return;
}
const request: VariableExtractionRequest = {
promptContent: '你好!',
extractionModelKey: context.modelKey,
existingVariableNames: [],
};
const result = await variableExtractionService.extract(request);
// 应该返回空数组或极少变量
expect(result.variables).toBeInstanceOf(Array);
expect(result.summary).toBeDefined();
}, 60000);
});
describe('端到端工作流测试', () => {
it.skipIf(!hasAvailableProvider())('应该完成完整的变量提取→替换流程', async () => {
if (!context) {
return;
}
const originalPrompt = '请写一篇关于春天的文章字数要求在500字以内风格要轻松愉快。';
// 1. 提取变量
const extractRequest: VariableExtractionRequest = {
promptContent: originalPrompt,
extractionModelKey: context.modelKey,
existingVariableNames: [],
};
const extractResult = await variableExtractionService.extract(extractRequest);
if (extractResult.variables.length > 0) {
// 2. 模拟替换过程(从后往前替换)
let replacedPrompt = originalPrompt;
const sortedVariables = [...extractResult.variables].sort((a, b) => {
const indexA = findOccurrenceIndex(originalPrompt, a.position.originalText, a.position.occurrence);
const indexB = findOccurrenceIndex(originalPrompt, b.position.originalText, b.position.occurrence);
return indexB - indexA;
});
for (const variable of sortedVariables) {
const { originalText, occurrence } = variable.position;
const placeholder = `{{${variable.name}}}`;
const index = findOccurrenceIndex(replacedPrompt, originalText, occurrence);
if (index !== -1) {
replacedPrompt =
replacedPrompt.substring(0, index) +
placeholder +
replacedPrompt.substring(index + originalText.length);
}
}
extractResult.variables.forEach((v, index) => {
});
// 验证替换后的提示词包含变量占位符
extractResult.variables.forEach((v) => {
expect(replacedPrompt).toContain(`{{${v.name}}}`);
});
// 验证替换后的提示词不再包含被替换的原文(除非是未被替换的部分)
// 注意:这个验证比较复杂,因为原文可能在多处出现
}
}, 60000);
});
});
/**
* 辅助函数查找文本第N次出现的索引位置
*/
function findOccurrenceIndex(text: string, searchText: string, occurrence: number): number {
let count = 0;
let index = -1;
while (count < occurrence) {
index = text.indexOf(searchText, index + 1);
if (index === -1) {
return -1;
}
count++;
}
return index;
}