Files
flyer-crawler.projectium.com/src/services/flyerAiProcessor.server.test.ts
Torben Sorensen fd52a79a72
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 42s
fixin
2025-12-28 22:38:26 -08:00

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.'));
});
});