import assert from 'node:assert/strict'; import test from 'node:test'; import { recipeDraftSchema, recipePatchSchema } from '../src/schemas/recipe.schema.js'; import { normalizeModelJson, normalizeRecipePayload, RecipeParserService, } from '../src/services/recipe-parser.service.js'; import { recipeDraft } from './helpers/fakes.js'; test('유효한 AI recipe 결과를 허용한다', () => { assert.deepEqual(recipeDraftSchema.parse(recipeDraft), recipeDraft); }); test('재료 이름이나 단계가 비어 있으면 거부한다', () => { const invalid = structuredClone(recipeDraft); invalid.ingredientGroups[0].items[0].name = ''; assert.equal(recipeDraftSchema.safeParse(invalid).success, false); }); test('MiniMax reasoning 및 markdown fence에서 JSON만 분리한다', () => { const value = normalizeModelJson('reasoning\n```json\n{"title":"test"}\n```'); assert.equal(value, '{"title":"test"}'); }); test('부분 수정은 보내지 않은 nullable/default 필드를 만들지 않는다', () => { assert.deepEqual(recipePatchSchema.parse({ title: '새 제목' }), { title: '새 제목' }); }); test('태그를 표준 태그로 정리하고 최대 3개만 유지한다', () => { const input = structuredClone(recipeDraft); input.tags = [' #한식 ', '한식', ' QUICK MEAL ', '감자요리', '중화요리', '샐러드']; assert.deepEqual(recipeDraftSchema.parse(input).tags, ['한식', '간단요리', '중식']); assert.deepEqual( recipePatchSchema.parse({ tags: ['##찌개', ' 샐러드 ', '식단', '고기'] }).tags, ['국물', '샐러드', '다이어트'], ); }); test('AI가 비어 있는 재료 그룹 라벨을 반환하면 구조 라벨만 보완한다', () => { const input = structuredClone(recipeDraft); input.ingredientGroups[0].name = null; const normalized = normalizeRecipePayload(input); assert.equal(normalized.ingredientGroups[0].name, '재료'); assert.deepEqual(normalized.ingredientGroups[0].items, recipeDraft.ingredientGroups[0].items); }); test('MiniMax 사고 과정 분리와 충분한 출력 한도를 요청한다', async () => { let request; const client = { chat: { completions: { async create(parameters) { request = parameters; const output = structuredClone(recipeDraft); output.ingredientGroups[0].name = null; return { choices: [{ message: { content: JSON.stringify(output) } }] }; }, }, }, }; const service = new RecipeParserService({ client, model: 'MiniMax-M2.7' }); const result = await service.parse({ platform: 'youtube', title: '김치찌개', rawText: '[12.4] 김치를 볶는다.', }); assert.equal(request.reasoning_split, true); assert.equal(request.max_completion_tokens, 8192); assert.match(request.messages[0].content, /최대 3개/); assert.match(request.messages[0].content, /한식, 중식/); assert.match(request.messages[0].content, /외국어.*자연스러운 한국어로 번역/); assert.match(request.messages[0].content, /수량, 온도, 시간 값은 변환하지 않습니다/); assert.equal(result.ingredientGroups[0].name, '재료'); });