import * as fs from 'fs'; import * as path from 'path'; import { CONTROLLER_KEY, getClassMetadata } from '@midwayjs/core'; import { BaseController, CoolCommException, CurdOption } from '@cool-midway/core'; import { Repository } from 'typeorm'; import { AiGlossaryEntity } from '../../../src/modules/ai/entity/glossary'; import { AiGlossaryService } from '../../../src/modules/ai/service/glossary'; import { AiLocalizeService } from '../../../src/modules/ai/service/localize'; import { AdminAiGlossaryController, adminAiGlossaryCurdOption, } from '../../../src/modules/ai/controller/admin/glossary'; import { AiGlossaryItem } from '../../../src/comm/types/omni'; interface CoolControllerMeta { prefix?: string; curdOption?: CurdOption; } async function waitCoolMeta( target: new (...args: never[]) => unknown ): Promise { const started = Date.now(); while (Date.now() - started < 2000) { const meta = getClassMetadata(CONTROLLER_KEY, target) as | CoolControllerMeta | undefined; if (meta?.curdOption) { return meta; } await new Promise(resolve => setImmediate(resolve)); } throw new Error(`${target.name} 的 CoolController 元数据未就绪`); } function createMockRepo(seedItems: AiGlossaryEntity[] = []): Repository { let list = [...seedItems]; let autoId = 100; return { find: jest.fn(async (options?: { where?: Record }) => { if (!options?.where) { return list; } return list.filter(item => { for (const [k, v] of Object.entries(options.where ?? {})) { if (v !== undefined && (item as unknown as Record)[k] !== v) { return false; } } return true; }); }), save: jest.fn(async (entityOrEntities: AiGlossaryEntity | AiGlossaryEntity[]) => { if (Array.isArray(entityOrEntities)) { for (const item of entityOrEntities) { if (!item.id) { item.id = ++autoId; } list.push(item); } return entityOrEntities; } if (!entityOrEntities.id) { entityOrEntities.id = ++autoId; } list.push(entityOrEntities); return entityOrEntities; }), insert: jest.fn(async (item: AiGlossaryEntity) => { if (!item.id) { item.id = ++autoId; } list.push(item); return { identifiers: [{ id: item.id }] }; }), } as unknown as Repository; } describe('Point 06: 后台多语言 AI 润色中心与行业术语库 (Glossary)', () => { describe('1. 实体 AiGlossaryEntity 定义与字段校验', () => { it('实体属性与映射表名 ai_glossary 校验', () => { const entity = new AiGlossaryEntity(); entity.sourceTerm = 'digital business card'; entity.targetTerm = '智能数字名片'; entity.sourceLang = 'en'; entity.targetLang = 'zh-CN'; entity.domain = 'tech'; entity.notes = '核心品牌术语'; expect(entity.sourceTerm).toBe('digital business card'); expect(entity.targetTerm).toBe('智能数字名片'); expect(entity.sourceLang).toBe('en'); expect(entity.targetLang).toBe('zh-CN'); expect(entity.domain).toBe('tech'); expect(entity.notes).toBe('核心品牌术语'); }); }); describe('2. AiGlossaryService 批量导入与增删改查', () => { let service: AiGlossaryService; let mockRepo: Repository; beforeEach(() => { service = new AiGlossaryService(); mockRepo = createMockRepo(); service.aiGlossaryEntity = mockRepo; }); it('batchImport 空列表应抛异常或返回0', async () => { await expect(service.batchImport([])).rejects.toThrow(CoolCommException); }); it('batchImport 忽略缺少 sourceTerm 或 targetTerm 的无效项', async () => { const result = await service.batchImport([ { sourceTerm: '', targetTerm: '无效' }, { sourceTerm: 'CRM', targetTerm: '' }, { sourceTerm: 'NFC Card', targetTerm: 'NFC 实体卡', targetLang: 'zh-CN' }, ]); expect(result.total).toBe(3); expect(result.inserted).toBe(1); }); it('batchImport 批量导入多项成功', async () => { const items: Array> = [ { sourceTerm: 'OmniCard', targetTerm: '奥秘名片', targetLang: 'zh-CN' }, { sourceTerm: 'Radar', targetTerm: '访客雷达', targetLang: 'zh-CN', domain: 'crm' }, ]; const result = await service.batchImport(items); expect(result.inserted).toBe(2); expect(result.total).toBe(2); }); }); describe('3. AiGlossaryService 术语匹配替换 (贪心长词优先 & 领域匹配)', () => { let service: AiGlossaryService; beforeEach(() => { const seed: AiGlossaryEntity[] = [ { id: 1, sourceTerm: 'digital card', targetTerm: '电子名片', targetLang: 'zh-CN', domain: 'general', } as AiGlossaryEntity, { id: 2, sourceTerm: 'smart digital card', targetTerm: '全球智能数字名片', targetLang: 'zh-CN', domain: 'tech', } as AiGlossaryEntity, { id: 3, sourceTerm: 'lead', targetTerm: '高意向商机', targetLang: 'zh-CN', domain: 'crm', } as AiGlossaryEntity, { id: 4, sourceTerm: 'lead', targetTerm: '线索', targetLang: 'zh-CN', domain: 'general', } as AiGlossaryEntity, { id: 5, sourceTerm: 'بطاقة', targetTerm: 'بطاقة ذكية', targetLang: 'ar-SA', } as AiGlossaryEntity, ]; service = new AiGlossaryService(); const mockRepo = createMockRepo(seed); service.aiGlossaryEntity = mockRepo; }); it('长词优先贪心匹配:smart digital card 应被替换为 全球智能数字名片 而非 电子名片', async () => { const input = 'This is our smart digital card platform.'; const output = await service.replaceTerms(input, 'zh-CN', 'tech'); expect(output).toContain('全球智能数字名片'); expect(output).not.toContain('电子名片'); }); it('领域匹配优先:domain 为 crm 时 lead 替换为 高意向商机,否则为 线索', async () => { const input = 'We captured a new lead yesterday.'; const crmOutput = await service.replaceTerms(input, 'zh-CN', 'crm'); expect(crmOutput).toContain('高意向商机'); const generalOutput = await service.replaceTerms(input, 'zh-CN', 'general'); expect(generalOutput).toContain('线索'); }); it('跨目标语言隔离:目标语言 ar-SA 不应应用 zh-CN 术语', async () => { const input = 'smart digital card and بطاقة'; const output = await service.replaceTerms(input, 'ar-SA'); expect(output).toContain('بطاقة ذكية'); expect(output).not.toContain('全球智能数字名片'); }); }); describe('4. AiLocalizeService 引用术语库并强行替换', () => { let localizeService: AiLocalizeService; let glossaryService: AiGlossaryService; beforeEach(() => { glossaryService = new AiGlossaryService(); const seed: AiGlossaryEntity[] = [ { id: 1, sourceTerm: 'digital cards', targetTerm: '智能数字名片', targetLang: 'zh-CN', } as AiGlossaryEntity, ]; const mockRepo = createMockRepo(seed); glossaryService.aiGlossaryEntity = mockRepo; localizeService = new AiLocalizeService(); localizeService.aiGlossaryService = glossaryService; }); it('润色时应将术语强制替换为术语库配置的目标词', async () => { const result = await localizeService.localize({ text: 'We help global teams share digital cards.', targetLocale: 'zh-CN', tone: 'professional', }); expect(result.locale).toBe('zh-CN'); expect(result.text).toContain('智能数字名片'); }); }); describe('5. AdminAiGlossaryController 路由与元数据校验', () => { const controllerFile = path.join( __dirname, '../../../src/modules/ai/controller/admin/glossary.ts' ); let meta: CoolControllerMeta; let option: CurdOption; beforeAll(async () => { meta = await waitCoolMeta(AdminAiGlossaryController); option = meta.curdOption as CurdOption; }); it('文件路径必须符合 /admin/ai/glossary 规范', () => { expect(fs.existsSync(controllerFile)).toBe(true); expect(controllerFile.replace(/\\/g, '/')).toMatch( /modules\/ai\/controller\/admin\/glossary\.ts$/ ); }); it('路由前缀必须为 /admin/ai/glossary', () => { expect(meta.prefix).toBe('/admin/ai/glossary'); }); it('控制器应继承 BaseController 并开启 CRUD 与 serviceApis', () => { expect(AdminAiGlossaryController.prototype).toBeInstanceOf(BaseController); expect(option.service).toBe(AiGlossaryService); expect(adminAiGlossaryCurdOption.service).toBe(AiGlossaryService); expect(option.entity).toBe(AiGlossaryEntity); expect(option.api).toEqual( expect.arrayContaining(['add', 'delete', 'update', 'info', 'list', 'page']) ); expect(option.serviceApis).toEqual(expect.arrayContaining(['batchImport'])); }); }); });