import assert from 'node:assert/strict'; import test from 'node:test'; import { createImportBatches, ImportJobProcessorService, } from '../src/services/import-job-processor.service.js'; import { FakeImportJobRepository, recipeDraft, source } from './helpers/fakes.js'; function extractedItem(jobId, rawText) { return { jobId, source: { rawText }, }; } test('가져오기 본문은 최대 3개와 합계 100,000자 단위로 묶는다', () => { const batches = createImportBatches([ extractedItem('job-1', 'a'.repeat(40_000)), extractedItem('job-2', 'b'.repeat(40_000)), extractedItem('job-3', 'c'.repeat(30_000)), extractedItem('job-4', 'd'), extractedItem('job-5', 'e'), ]); assert.deepEqual(batches.map((batch) => batch.map(({ jobId }) => jobId)), [ ['job-1', 'job-2'], ['job-3', 'job-4', 'job-5'], ]); }); test('처리 요청 하나가 대기열 전체를 3개씩 MiniMax 배치 처리한다', async () => { const importJobRepository = new FakeImportJobRepository(); for (let index = 1; index <= 4; index += 1) { await importJobRepository.enqueue({ ownerGoogleSub: 'google-user-1', url: `https://www.instagram.com/reel/Recipe${index}/`, platform: 'instagram', sourceId: `Recipe${index}`, }); } const batchJobIds = []; const processor = new ImportJobProcessorService({ importJobRepository, sourceExtractor: { async extract(url) { const sourceId = new URL(url).pathname.split('/').filter(Boolean)[1]; return { ...source, sourceUrl: url, sourceId, rawText: `${sourceId} 본문`, }; }, }, recipeParser: { async parseMany(entries) { batchJobIds.push(entries.map(({ jobId }) => jobId)); return entries.map(({ jobId }) => ({ jobId, recipe: recipeDraft })); }, }, recipeCreator: { async create({ source: recipeSource }) { return { _id: `recipe-${recipeSource.sourceId}` }; }, }, logger: { error() {} }, }); processor.start('google-user-1'); await processor.waitForIdle('google-user-1'); assert.deepEqual(batchJobIds.map((ids) => ids.length), [3, 1]); assert.deepEqual(await importJobRepository.listVisible('google-user-1'), []); assert.ok([...importJobRepository.jobs.values()].every(({ status }) => status === 'completed')); });