| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- import { ConfigService } from '@nestjs/config';
- import { Test } from '@nestjs/testing';
- import { PrismaService } from '../../prisma/prisma.service';
- import { AiSimulationClient } from './ai-simulation.client';
- import { SimulationsService } from './simulations.service';
- describe('SimulationsService', () => {
- let service: SimulationsService;
- const aiClient = {
- execute: jest.fn(),
- getResult: jest.fn(),
- };
- const configService = {
- getOrThrow: jest.fn((key: string) => {
- if (key === 'aiSimulation.refreshIntervalMs') {
- return 10_000;
- }
- throw new Error(`Unexpected config key: ${key}`);
- }),
- };
- const prisma = {
- customer: {
- findFirst: jest.fn(),
- },
- aiHairSimulation: {
- findFirst: jest.fn(),
- findMany: jest.fn(),
- },
- aiHairSimulationTask: {
- findMany: jest.fn(),
- update: jest.fn(),
- },
- hairTemplate: {
- findMany: jest.fn(),
- count: jest.fn(),
- },
- $transaction: jest.fn(),
- };
- beforeEach(async () => {
- jest.resetAllMocks();
- configService.getOrThrow.mockImplementation((key: string) => {
- if (key === 'aiSimulation.refreshIntervalMs') {
- return 10_000;
- }
- throw new Error(`Unexpected config key: ${key}`);
- });
- const moduleRef = await Test.createTestingModule({
- providers: [
- SimulationsService,
- { provide: PrismaService, useValue: prisma },
- { provide: AiSimulationClient, useValue: aiClient },
- { provide: ConfigService, useValue: configService },
- ],
- }).compile();
- service = moduleRef.get(SimulationsService);
- });
- it('returns only public stylist templates for the current stylist', async () => {
- prisma.hairTemplate.findMany.mockResolvedValue([
- {
- id: BigInt(11),
- templateName: 'Layer Bob',
- coverUrl: 'https://cdn.example.com/bob.png',
- category: 'SHORT',
- tags: 'airy,bob',
- description: 'short bob',
- templateUrl: 'https://cdn.example.com/bob.zip',
- isSystemTemplate: 'NO',
- },
- ]);
- prisma.hairTemplate.count.mockResolvedValue(1);
- const result = await service.listTemplateOptions(BigInt(7), {
- source: 'STYLIST',
- page: 1,
- pageSize: 12,
- });
- expect(prisma.hairTemplate.findMany).toHaveBeenCalledWith(
- expect.objectContaining({
- where: expect.objectContaining({
- isDeleted: 0,
- status: 'PUBLIC',
- isSystemTemplate: 'NO',
- hairstylistId: BigInt(7),
- }),
- }),
- );
- expect(result.items[0]).toMatchObject({
- id: '11',
- templateName: 'Layer Bob',
- tags: ['airy', 'bob'],
- isSystemTemplate: false,
- });
- });
- it('returns only public system templates for the system tab', async () => {
- prisma.hairTemplate.findMany.mockResolvedValue([
- {
- id: BigInt(21),
- templateName: 'Classic Medium',
- coverUrl: '',
- category: 'MEDIUM',
- tags: '',
- description: null,
- templateUrl: 'https://cdn.example.com/medium.zip',
- isSystemTemplate: 'YES',
- },
- ]);
- prisma.hairTemplate.count.mockResolvedValue(1);
- const result = await service.listTemplateOptions(BigInt(7), {
- source: 'SYSTEM',
- page: 1,
- pageSize: 12,
- });
- expect(prisma.hairTemplate.findMany).toHaveBeenCalledWith(
- expect.objectContaining({
- where: expect.objectContaining({
- isDeleted: 0,
- status: 'PUBLIC',
- isSystemTemplate: 'YES',
- }),
- }),
- );
- expect(result.items[0].id).toBe('21');
- });
- it('creates one simulation and one task row per selected template', async () => {
- prisma.customer.findFirst.mockResolvedValue({ id: BigInt(5) });
- prisma.hairTemplate.findMany.mockResolvedValue([
- {
- id: BigInt(11),
- templateUrl: 'https://cdn.example.com/11.zip',
- isSystemTemplate: 'NO',
- hairstylistId: BigInt(7),
- status: 'PUBLIC',
- isDeleted: 0,
- },
- {
- id: BigInt(12),
- templateUrl: 'https://cdn.example.com/12.zip',
- isSystemTemplate: 'YES',
- hairstylistId: null,
- status: 'PUBLIC',
- isDeleted: 0,
- },
- ]);
- aiClient.execute
- .mockResolvedValueOnce({ taskId: 'task-11' })
- .mockResolvedValueOnce({ taskId: 'task-12' });
- const createSimulationRecord = jest.fn().mockResolvedValue({ id: BigInt(91) });
- const createTaskRecords = jest.fn().mockResolvedValue({ count: 2 });
- prisma.$transaction.mockImplementation(async (callback: (tx: {
- aiHairSimulation: { create: jest.Mock };
- aiHairSimulationTask: { createMany: jest.Mock };
- }) => Promise<{ simulationId: string }>) =>
- callback({
- aiHairSimulation: {
- create: createSimulationRecord,
- },
- aiHairSimulationTask: {
- createMany: createTaskRecords,
- },
- }),
- );
- const result = await service.createSimulation(BigInt(7), {
- customerId: '5',
- originImage: 'https://cdn.example.com/origin.jpg',
- templateIds: ['11', '12'],
- simulateRemark: 'soft volume',
- });
- expect(aiClient.execute).toHaveBeenNthCalledWith(
- 1,
- 'https://cdn.example.com/11.zip',
- 'https://cdn.example.com/origin.jpg',
- );
- expect(createSimulationRecord).toHaveBeenCalledWith({
- data: expect.objectContaining({
- templateIds: JSON.stringify(['11', '12']),
- }),
- });
- expect(result).toEqual({ simulationId: '91' });
- });
- it('rejects customer ids outside the current stylist scope', async () => {
- prisma.customer.findFirst.mockResolvedValue(null);
- await expect(
- service.createSimulation(BigInt(7), {
- customerId: '999',
- originImage: 'https://cdn.example.com/origin.jpg',
- templateIds: ['11'],
- }),
- ).rejects.toThrow('顾客不存在');
- });
- it('refreshes stale processing tasks and returns PARTIAL_SUCCESS when mixed', async () => {
- prisma.aiHairSimulation.findFirst.mockResolvedValue({
- id: BigInt(91),
- customerId: BigInt(5),
- hairstylistId: BigInt(7),
- originImage: 'https://cdn.example.com/origin.jpg',
- simulateRemark: 'airy',
- createTime: new Date('2026-06-28T09:00:00Z'),
- });
- prisma.customer.findFirst.mockResolvedValue({
- id: BigInt(5),
- name: 'Mika',
- avatar: 'https://cdn.example.com/mika.png',
- phone: '090',
- expectContact: 'LINE',
- });
- prisma.aiHairSimulationTask.findMany
- .mockResolvedValueOnce([
- {
- id: BigInt(1),
- templateId: '11',
- customerId: BigInt(5),
- taskId: 'task-11',
- taskStatus: 'PROCESSING',
- resultImages: null,
- errorMsg: null,
- taskStartTime: new Date('2026-06-28T10:00:00Z'),
- taskFinishTime: null,
- updateTime: new Date(Date.now() - 20_000),
- isDeleted: 0,
- },
- {
- id: BigInt(2),
- templateId: '12',
- customerId: BigInt(5),
- taskId: 'task-12',
- taskStatus: 'FAILED',
- resultImages: null,
- errorMsg: 'template invalid',
- taskStartTime: new Date('2026-06-28T10:00:00Z'),
- taskFinishTime: new Date('2026-06-28T10:02:00Z'),
- updateTime: new Date('2026-06-28T10:02:00Z'),
- isDeleted: 0,
- },
- ])
- .mockResolvedValueOnce([
- {
- id: BigInt(1),
- templateId: '11',
- customerId: BigInt(5),
- taskId: 'task-11',
- taskStatus: 'SUCCESSES',
- resultImages: JSON.stringify(['https://cdn.example.com/result.png']),
- errorMsg: null,
- taskStartTime: new Date('2026-06-28T10:00:00Z'),
- taskFinishTime: new Date('2026-06-28T10:01:00Z'),
- updateTime: new Date('2026-06-28T10:01:00Z'),
- isDeleted: 0,
- },
- {
- id: BigInt(2),
- templateId: '12',
- customerId: BigInt(5),
- taskId: 'task-12',
- taskStatus: 'FAILED',
- resultImages: null,
- errorMsg: 'template invalid',
- taskStartTime: new Date('2026-06-28T10:00:00Z'),
- taskFinishTime: new Date('2026-06-28T10:02:00Z'),
- updateTime: new Date('2026-06-28T10:02:00Z'),
- isDeleted: 0,
- },
- ]);
- prisma.hairTemplate.findMany.mockResolvedValue([
- {
- id: BigInt(11),
- templateName: 'Layer Bob',
- coverUrl: '',
- category: 'SHORT',
- isSystemTemplate: 'NO',
- },
- {
- id: BigInt(12),
- templateName: 'Classic Medium',
- coverUrl: '',
- category: 'MEDIUM',
- isSystemTemplate: 'YES',
- },
- ]);
- aiClient.getResult.mockResolvedValue({
- status: 'successes',
- message: '',
- images: ['https://cdn.example.com/result.png'],
- });
- const result = await service.getSimulationDetail(BigInt(7), '91');
- expect(aiClient.getResult).toHaveBeenCalledWith('task-11');
- expect(prisma.aiHairSimulationTask.update).toHaveBeenCalledWith(
- expect.objectContaining({
- data: expect.objectContaining({
- taskStatus: 'SUCCESSES',
- resultImages: JSON.stringify(['https://cdn.example.com/result.png']),
- }),
- }),
- );
- expect(result.createdAt).toBe('2026-06-28T09:00:00.000Z');
- expect(result.overallStatus).toBe('PARTIAL_SUCCESS');
- });
- it('lists flattened customer gallery cards in descending simulation time order', async () => {
- prisma.customer.findFirst.mockResolvedValue({ id: BigInt(5) });
- prisma.aiHairSimulation.findMany.mockResolvedValue([
- {
- id: BigInt(91),
- customerId: BigInt(5),
- hairstylistId: BigInt(7),
- createTime: new Date('2026-06-28T09:00:00Z'),
- isDeleted: 0,
- },
- {
- id: BigInt(88),
- customerId: BigInt(5),
- hairstylistId: BigInt(7),
- createTime: new Date('2026-06-27T09:00:00Z'),
- isDeleted: 0,
- },
- ]);
- prisma.aiHairSimulationTask.findMany.mockResolvedValue([
- {
- id: BigInt(1),
- simulationId: BigInt(91),
- templateId: '11',
- taskId: 'task-11',
- taskStatus: 'SUCCESSES',
- resultImages: JSON.stringify([
- 'https://cdn.example.com/result-2.png',
- 'https://cdn.example.com/result-1.png',
- ]),
- isDeleted: 0,
- },
- {
- id: BigInt(2),
- simulationId: BigInt(88),
- templateId: '12',
- taskId: 'task-12',
- taskStatus: 'FAILED',
- resultImages: null,
- isDeleted: 0,
- },
- ]);
- prisma.hairTemplate.findMany.mockResolvedValue([
- {
- id: BigInt(11),
- templateName: 'Layer Bob',
- },
- ]);
- const result = await service.listCustomerSimulationGallery(BigInt(7), '5');
- expect(result.items).toEqual([
- {
- simulationId: '91',
- taskId: 'task-11',
- imageUrl: 'https://cdn.example.com/result-2.png',
- templateName: 'Layer Bob',
- createdAt: '2026-06-28T09:00:00.000Z',
- taskStatus: 'SUCCESSES',
- },
- {
- simulationId: '91',
- taskId: 'task-11',
- imageUrl: 'https://cdn.example.com/result-1.png',
- templateName: 'Layer Bob',
- createdAt: '2026-06-28T09:00:00.000Z',
- taskStatus: 'SUCCESSES',
- },
- ]);
- });
- });
|