Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 42s
130 lines
4.8 KiB
TypeScript
130 lines
4.8 KiB
TypeScript
// src/services/flyerAiProcessor.server.test.ts
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { FlyerAiProcessor } from './flyerAiProcessor.server';
|
|
import { AiDataValidationError } from './processingErrors';
|
|
import { logger } from './logger.server';
|
|
import type { AIService } from './aiService.server';
|
|
import type { PersonalizationRepository } from './db/personalization.db';
|
|
import type { FlyerJobData } from '../types/job-data';
|
|
|
|
vi.mock('./logger.server', () => ({
|
|
logger: {
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
debug: vi.fn(),
|
|
child: vi.fn().mockReturnThis(),
|
|
},
|
|
}));
|
|
|
|
const createMockJobData = (data: Partial<FlyerJobData>): FlyerJobData => ({
|
|
filePath: '/tmp/flyer.jpg',
|
|
originalFileName: 'flyer.jpg',
|
|
checksum: 'checksum-123',
|
|
...data,
|
|
});
|
|
|
|
describe('FlyerAiProcessor', () => {
|
|
let service: FlyerAiProcessor;
|
|
let mockAiService: AIService;
|
|
let mockPersonalizationRepo: PersonalizationRepository;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
|
|
mockAiService = {
|
|
extractCoreDataFromFlyerImage: vi.fn(),
|
|
} as unknown as AIService;
|
|
mockPersonalizationRepo = {
|
|
getAllMasterItems: vi.fn().mockResolvedValue([]),
|
|
} as unknown as PersonalizationRepository;
|
|
|
|
service = new FlyerAiProcessor(mockAiService, mockPersonalizationRepo);
|
|
});
|
|
|
|
it('should call AI service and return validated data on success', async () => {
|
|
const jobData = createMockJobData({});
|
|
const mockAiResponse = {
|
|
store_name: 'AI Store',
|
|
valid_from: '2024-01-01',
|
|
valid_to: '2024-01-07',
|
|
store_address: '123 AI St',
|
|
// FIX: Add an item to pass the new "must have items" quality check.
|
|
items: [
|
|
{
|
|
item: 'Test Item',
|
|
price_display: '$1.99',
|
|
price_in_cents: 199,
|
|
// ADDED to satisfy ExtractedFlyerItem type
|
|
quantity: 'each',
|
|
category_name: 'Grocery',
|
|
},
|
|
],
|
|
};
|
|
vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse);
|
|
|
|
const result = await service.extractAndValidateData([], jobData, logger);
|
|
|
|
expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledTimes(1);
|
|
expect(mockPersonalizationRepo.getAllMasterItems).toHaveBeenCalledTimes(1);
|
|
expect(result.data).toEqual(mockAiResponse);
|
|
expect(result.needsReview).toBe(false);
|
|
});
|
|
|
|
it('should throw AiDataValidationError if AI response has incorrect data structure', async () => {
|
|
const jobData = createMockJobData({});
|
|
// Mock AI to return a structurally invalid response (e.g., items is not an array)
|
|
const invalidResponse = {
|
|
store_name: 'Invalid Store',
|
|
items: 'not-an-array',
|
|
valid_from: null,
|
|
valid_to: null,
|
|
store_address: null,
|
|
};
|
|
vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(invalidResponse as any);
|
|
|
|
await expect(service.extractAndValidateData([], jobData, logger)).rejects.toThrow(
|
|
AiDataValidationError,
|
|
);
|
|
});
|
|
|
|
it('should pass validation even if store_name is missing', async () => {
|
|
const jobData = createMockJobData({});
|
|
const mockAiResponse = {
|
|
store_name: null, // Missing store name
|
|
items: [{ item: 'Test Item', price_display: '$1.99', price_in_cents: 199, quantity: 'each', category_name: 'Grocery' }],
|
|
// ADDED to satisfy AiFlyerDataSchema
|
|
valid_from: null,
|
|
valid_to: null,
|
|
store_address: null,
|
|
};
|
|
vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse as any);
|
|
const { logger } = await import('./logger.server');
|
|
|
|
const result = await service.extractAndValidateData([], jobData, logger);
|
|
|
|
// It should not throw, but return the data and log a warning.
|
|
expect(result.data).toEqual(mockAiResponse);
|
|
expect(result.needsReview).toBe(true);
|
|
expect(logger.warn).toHaveBeenCalledWith(expect.any(Object), expect.stringContaining('missing a store name. The transformer will use a fallback. Flagging for review.'));
|
|
});
|
|
|
|
it('should pass validation even if items array is empty', async () => {
|
|
const jobData = createMockJobData({});
|
|
const mockAiResponse = {
|
|
store_name: 'Test Store',
|
|
items: [], // Empty items array
|
|
// ADDED to satisfy AiFlyerDataSchema
|
|
valid_from: null,
|
|
valid_to: null,
|
|
store_address: null,
|
|
};
|
|
vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse);
|
|
const { logger } = await import('./logger.server');
|
|
|
|
const result = await service.extractAndValidateData([], jobData, logger);
|
|
expect(result.data).toEqual(mockAiResponse);
|
|
expect(result.needsReview).toBe(true);
|
|
expect(logger.warn).toHaveBeenCalledWith(expect.any(Object), expect.stringContaining('contains no items. The flyer will be saved with an item_count of 0. Flagging for review.'));
|
|
});
|
|
}); |