import { describe, it, expect, vi, beforeEach } from 'vitest'; import { detectObjects } from '../detect-objects'; import { extractFramesAtIntervals } from '@/features/discovery/services/frame-extraction.service'; import { detectObjectsInFrame } from '@/features/discovery/services/ai-vision.service'; import { db } from '@/lib/db'; import { uploadThumbnail } from '@/features/discovery/services/storage.service'; // Mock dependencies vi.mock('@/features/discovery/services/storage.service', () => ({ uploadThumbnail: vi.fn().mockResolvedValue('https://supa-storage.com/thumb.jpg') })); vi.mock('sharp', () => { return { default: vi.fn(() => ({ metadata: vi.fn().mockResolvedValue({ width: 1920, height: 1080 }), extract: vi.fn().mockReturnThis(), resize: vi.fn().mockReturnThis(), toFormat: vi.fn().mockReturnThis(), toBuffer: vi.fn().mockResolvedValue(Buffer.from('thumb')), })) }; }); vi.mock('@/features/discovery/services/frame-extraction.service', () => ({ extractFramesAtIntervals: vi.fn(), VideoUnavailableError: class VideoUnavailableError extends Error { constructor(message: string) { super(message); this.name = 'VideoUnavailableError'; } } })); vi.mock('@/features/discovery/services/ai-vision.service', () => ({ detectObjectsInFrame: vi.fn(), VisionRateLimitError: class VisionRateLimitError extends Error { }, })); vi.mock('@/lib/db', () => ({ db: { insert: vi.fn(() => ({ values: vi.fn(() => ({ returning: vi.fn().mockResolvedValue([{ id: 'det-1' }, { id: 'det-2' }]), })), })), update: vi.fn(() => ({ set: vi.fn(() => ({ where: vi.fn().mockResolvedValue(undefined), })), })), query: { youtubeVideos: { findFirst: vi.fn().mockResolvedValue({ id: 'v123', duration: 33 }), }, }, }, })); vi.mock('@/lib/db/schema', () => ({ detectedObjects: {}, youtubeVideos: {}, })); // Mock Inngest client to expose handler vi.mock('../../client', () => ({ inngest: { createFunction: vi.fn((config, trigger, handler) => ({ config, trigger, handler, fn: handler })), send: vi.fn().mockResolvedValue(undefined), } })); vi.mock('@sentry/nextjs', () => ({ captureException: vi.fn(), })); describe('Detect Objects Inngest Function', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should be defined', () => { expect(detectObjects).toBeDefined(); }); it('should execute analysis and save results', async () => { // Setup mocks vi.mocked(extractFramesAtIntervals).mockResolvedValue([ { timestamp: 0, frameBuffer: Buffer.from('f1') }, { timestamp: 10, frameBuffer: Buffer.from('f2') } ]); vi.mocked(detectObjectsInFrame).mockResolvedValue([ { name: 'Obj1', category: 'Tech', confidence: 0.9, boundingBox: { x: 10, y: 10, width: 100, height: 100 } } ]); const stepMock = { run: vi.fn(async (_name: string, fn: () => Promise) => { return await fn(); }) }; const event = { data: { videoId: 'v123', videoUrl: 'http://video.com' } }; // Execute handler const result = await detectObjects.fn({ event, step: stepMock }); expect(result.success).toBe(true); expect(result.count).toBe(2); // 2 frames × 1 detection each expect(extractFramesAtIntervals).toHaveBeenCalledWith('http://video.com', 5); // Adaptive: 5s for 33s video expect(detectObjectsInFrame).toHaveBeenCalledTimes(2); expect(uploadThumbnail).toHaveBeenCalledTimes(2); expect(db.insert).toHaveBeenCalled(); }); });