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 { AiCopilotService } from '../../../src/modules/ai/service/copilot'; import { AdminAiCopilotController, adminAiCopilotCurdOption, } from '../../../src/modules/ai/controller/admin/copilot'; import { CopilotRequest } from '../../../src/comm/types/omni'; /** * CoolController 元数据(脚本路径解析为异步) */ interface CoolControllerMeta { prefix?: string; curdOption?: CurdOption; } /** * generate 入参:契约 + 可选意向分 */ type CopilotGenerateInput = CopilotRequest & { intentScore?: number; }; /** * 等待 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)); } describe('T40: 销售跟进话术生成 (modules/ai copilot)', () => { describe('1. AiCopilotService.generate 入参校验', () => { let service: AiCopilotService; beforeEach(() => { service = new AiCopilotService(); }); it('无名且无 leadId 应抛 CoolCommException', async () => { await expect(service.generate({} as CopilotRequest)).rejects.toThrow( CoolCommException ); await expect( service.generate({ name: ' ', locale: 'en-US' }) ).rejects.toThrow(CoolCommException); await expect( service.generate({ leadId: '', name: '' }) ).rejects.toThrow(CoolCommException); }); it('仅有 leadId 无库时可生成,脚本非空', async () => { const result = await service.generate({ leadId: 12 }); expect(result.script.trim().length).toBeGreaterThan(0); expect(result.channel).toBe('email'); expect(result.locale).toBe('en-US'); }); }); describe('2. 渠道模板与意向诊断', () => { let service: AiCopilotService; beforeEach(() => { service = new AiCopilotService(); }); it('email / whatsapp 脚本不同', async () => { const req: CopilotGenerateInput = { name: 'Ada Chen' }; const email = await service.generate({ ...req, channel: 'email' }); const whatsapp = await service.generate({ ...req, channel: 'whatsapp' }); expect(email.channel).toBe('email'); expect(whatsapp.channel).toBe('whatsapp'); expect(email.script).toBeTruthy(); expect(whatsapp.script).toBeTruthy(); expect(email.script).not.toBe(whatsapp.script); expect(email.script).toContain('Ada Chen'); expect(whatsapp.script).toContain('Ada Chen'); }); it('高意向 diagnosis 不同', async () => { const low = await service.generate({ name: 'Ada Chen', intentScore: 10, } as CopilotGenerateInput); const high = await service.generate({ name: 'Ada Chen', intentScore: 90, } as CopilotGenerateInput); expect(low.diagnosis).toBeTruthy(); expect(high.diagnosis).toBeTruthy(); expect(high.diagnosis).not.toBe(low.diagnosis); }); it('缺省 channel=email、locale=en-US,英文不含中文', async () => { const result = await service.generate({ name: 'Ada Chen' }); expect(result.channel).toBe('email'); expect(result.locale).toBe('en-US'); expect(/[\u4e00-\u9fff]/.test(result.script)).toBe(false); expect(/[\u4e00-\u9fff]/.test(result.diagnosis)).toBe(false); }); it('zh-CN 含中文', async () => { const result = await service.generate({ name: '陈安', locale: 'zh-CN', channel: 'email', }); expect(result.locale).toBe('zh-CN'); expect(/[\u4e00-\u9fff]/.test(result.script)).toBe(true); expect(/[\u4e00-\u9fff]/.test(result.diagnosis)).toBe(true); expect(result.script).toContain('陈安'); }); it('兼容 serviceApis 单对象入参且脚本非空', async () => { const body: CopilotGenerateInput = { name: 'Ada Chen', locale: 'zh-CN', channel: 'whatsapp', intentScore: 80, }; const result = await service.generate(body); expect(result.channel).toBe('whatsapp'); expect(result.locale).toBe('zh-CN'); expect(result.script.trim().length).toBeGreaterThan(0); expect(/[\u4e00-\u9fff]/.test(result.script)).toBe(true); }); }); describe('3. AdminAiCopilotController /admin/ai/copilot', () => { const controllerFile = path.join( __dirname, '../../../src/modules/ai/controller/admin/copilot.ts' ); let meta: CoolControllerMeta; let option: CurdOption; beforeAll(async () => { meta = await waitCoolMeta(AdminAiCopilotController); option = meta.curdOption as CurdOption; }); it('文件路径应符合 /admin/ai/copilot 自动路由约定', () => { expect(fs.existsSync(controllerFile)).toBe(true); expect(controllerFile.replace(/\\/g, '/')).toMatch( /modules\/ai\/controller\/admin\/copilot\.ts$/ ); }); it('路由前缀应为 /admin/ai/copilot', () => { expect(meta.prefix).toBe('/admin/ai/copilot'); }); it('控制器应继承 BaseController,绑定 AiCopilotService,不开 CRUD', () => { expect(AdminAiCopilotController.prototype).toBeInstanceOf(BaseController); expect(option.service).toBe(AiCopilotService); expect(adminAiCopilotCurdOption.service).toBe(AiCopilotService); expect(option.api || []).toEqual([]); }); it('serviceApis 应包含 generate', () => { const methods = serviceApiMethods(option.serviceApis); expect(methods).toContain('generate'); expect(serviceApiMethods(adminAiCopilotCurdOption.serviceApis)).toContain( 'generate' ); }); }); });