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 { AiLocalizeService } from '../../../src/modules/ai/service/localize'; import { AdminAiLocalizeController, adminAiLocalizeCurdOption, } from '../../../src/modules/ai/controller/admin/localize'; import { LocalizeRequest, LocalizeTone } from '../../../src/comm/types/omni'; /** * CoolController 元数据(脚本路径解析为异步) */ interface CoolControllerMeta { prefix?: string; curdOption?: CurdOption; } /** * 等待 CoolController 将 CRUD 配置写入类元数据 */ 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 serviceApiMethods(apis: CurdOption['serviceApis'] = []): string[] { return apis.map(api => (typeof api === 'string' ? api : api.method)); } const SAMPLE_TEXT = 'We help global teams share digital cards.'; function localizeReq( extra?: Partial ): LocalizeRequest { return { text: extra?.text ?? SAMPLE_TEXT, targetLocale: extra?.targetLocale ?? 'en-US', tone: extra?.tone, }; } describe('T39: AI 多语言本地化润色 (modules/ai localize)', () => { describe('1. AiLocalizeService.localize 入参校验', () => { let service: AiLocalizeService; beforeEach(() => { service = new AiLocalizeService(); }); it('缺 text 应抛 CoolCommException', async () => { await expect( service.localize({ text: '', targetLocale: 'en-US', }) ).rejects.toThrow(CoolCommException); await expect( service.localize({ text: ' ', targetLocale: 'en-US', }) ).rejects.toThrow(CoolCommException); await expect( service.localize({ targetLocale: 'en-US', } as LocalizeRequest) ).rejects.toThrow(CoolCommException); }); it('缺 targetLocale 应抛 CoolCommException', async () => { await expect( service.localize({ text: SAMPLE_TEXT, targetLocale: '', }) ).rejects.toThrow(CoolCommException); await expect( service.localize({ text: SAMPLE_TEXT, targetLocale: ' ', }) ).rejects.toThrow(CoolCommException); await expect( service.localize({ text: SAMPLE_TEXT, } as LocalizeRequest) ).rejects.toThrow(CoolCommException); }); }); describe('2. 语气骨架与 locale 包裹', () => { let service: AiLocalizeService; beforeEach(() => { service = new AiLocalizeService(); }); it('三种 tone 返回不同文本', async () => { const tones: LocalizeTone[] = ['professional', 'direct', 'storytelling']; const texts = await Promise.all( tones.map(tone => service.localize(localizeReq({ tone })).then(r => r.text) ) ); expect(new Set(texts).size).toBe(3); expect(texts.every(t => t.trim().length > 0)).toBe(true); }); it('tone 缺省为 professional', async () => { const implicit = await service.localize(localizeReq()); const explicit = await service.localize( localizeReq({ tone: 'professional' }) ); expect(implicit.tone).toBe('professional'); expect(implicit.text).toBe(explicit.text); }); it('zh-CN 回复含中文', async () => { const result = await service.localize( localizeReq({ targetLocale: 'zh-CN' }) ); expect(result.locale).toBe('zh-CN'); expect(result.tone).toBe('professional'); expect(/[\u4e00-\u9fff]/.test(result.text)).toBe(true); expect(result.text).toContain(SAMPLE_TEXT); }); it('兼容 serviceApis 单对象入参并回传 locale / tone', async () => { const body: LocalizeRequest = { text: SAMPLE_TEXT, targetLocale: 'ar-SA', tone: 'direct', }; const result = await service.localize(body); expect(result.locale).toBe('ar-SA'); expect(result.tone).toBe('direct'); expect(result.text.trim().length).toBeGreaterThan(0); }); }); describe('3. AdminAiLocalizeController /admin/ai/localize', () => { const controllerFile = path.join( __dirname, '../../../src/modules/ai/controller/admin/localize.ts' ); let meta: CoolControllerMeta; let option: CurdOption; beforeAll(async () => { meta = await waitCoolMeta(AdminAiLocalizeController); option = meta.curdOption as CurdOption; }); it('文件路径应符合 /admin/ai/localize 自动路由约定', () => { expect(fs.existsSync(controllerFile)).toBe(true); expect(controllerFile.replace(/\\/g, '/')).toMatch( /modules\/ai\/controller\/admin\/localize\.ts$/ ); }); it('路由前缀应为 /admin/ai/localize', () => { expect(meta.prefix).toBe('/admin/ai/localize'); }); it('控制器应继承 BaseController,绑定 AiLocalizeService,不开 CRUD', () => { expect(AdminAiLocalizeController.prototype).toBeInstanceOf(BaseController); expect(option.service).toBe(AiLocalizeService); expect(adminAiLocalizeCurdOption.service).toBe(AiLocalizeService); expect(option.api || []).toEqual([]); }); it('serviceApis 应包含 localize', () => { const methods = serviceApiMethods(option.serviceApis); expect(methods).toContain('localize'); expect(serviceApiMethods(adminAiLocalizeCurdOption.serviceApis)).toContain( 'localize' ); }); }); });