/** * Тесты для matcher.js и aiAdapter.js * Запуск: node test.js */ const aiAdapter = require('./aiAdapter.js'); const matcher = require('./matcher.js'); let passed = 0; let failed = 0; function test(name, fn) { try { fn(); console.log(`✓ ${name}`); passed++; } catch (error) { console.error(`✗ ${name}`); console.error(` ${error.message}`); failed++; } } function assert(condition, message) { if (!condition) { throw new Error(message || 'Assertion failed'); } } console.log('Тесты aiAdapter.js:\n'); test('Извлечение требований из текста вакансии', () => { const text = 'Требуется JavaScript разработчик с опытом от 3 лет. Знание React обязательно.'; const result = aiAdapter.generate('extractCriteria', { vacancyText: text }); assert(result.criteria.length > 0, 'Критерии не извлечены'); assert(result.criteria.some(c => c.label.toLowerCase().includes('javascript')), 'Не найдено JavaScript'); assert(result.criteria.some(c => c.label.toLowerCase().includes('react')), 'Не найдено React'); }); test('Пустой текст вакансии', () => { const result = aiAdapter.generate('extractCriteria', { vacancyText: '' }); assert(result.error, 'Нет ошибки для пустого текста'); }); test('Сопоставление резюме с критериями', () => { const criteria = [ { id: 'js', label: 'JavaScript', required: true, weight: 5, synonyms: ['js'], category: 'frontend' } ]; const resume = 'Опыт работы с JavaScript 3 года. Владею JS, React.'; const result = aiAdapter.generate('matchResume', { criteria, resumeText: resume }); assert(result.results.length === 1, 'Неверное количество результатов'); assert(result.results[0].status === 'confirmed', 'JavaScript не подтверждён'); assert(result.results[0].evidence.length > 0, 'Нет цитаты'); }); test('Отрицание не считается подтверждением', () => { const criteria = [ { id: 'react', label: 'React', required: true, weight: 5, synonyms: [], category: 'frontend' } ]; const resume = 'Без опыта работы с React. Не владею React.'; const result = aiAdapter.generate('matchResume', { criteria, resumeText: resume }); assert(result.results[0].status !== 'confirmed', 'Отрицание проигнорировано'); }); test('Синонимы работают', () => { const criteria = [ { id: 'js', label: 'JavaScript', required: true, weight: 5, synonyms: ['js', 'ecmascript'], category: 'frontend' } ]; const resume = 'Опыт коммерческой разработки на JS 2 года.'; const result = aiAdapter.generate('matchResume', { criteria, resumeText: resume }); assert(result.results[0].status === 'confirmed', 'Синоним JS не сработал'); }); test('Генерация вопросов для интервью', () => { const unmatched = [ { criterionId: 'nodejs', label: 'Node.js', status: 'clarify', required: true, weight: 3 } ]; const result = aiAdapter.generate('generateQuestions', { unmatchedCriteria: unmatched }); assert(result.questions.length > 0, 'Вопросы не сгенерированы'); assert(result.questions[0].question.includes('Node.js'), 'Вопрос не содержит название навыка'); }); console.log('\nТесты matcher.js:\n'); test('Расчёт покрытия', () => { const results = [ { status: 'confirmed', weight: 5 }, { status: 'confirmed', weight: 3 }, { status: 'notfound', weight: 2 } ]; const coverage = matcher.calculateCoverage(results); assert(coverage === 80, `Неверное покрытие: ${coverage} (ожидалось 80)`); }); test('Покрытие при пустом списке', () => { const coverage = matcher.calculateCoverage([]); assert(coverage === 0, `Неверное покрытие пустого списка: ${coverage}`); }); test('Статусы сопоставления', () => { const results = [ { status: 'confirmed' }, { status: 'clarify' }, { status: 'clarify' }, { status: 'notfound' } ]; const stats = matcher.getStatusStats(results); assert(stats.confirmed === 1, 'Неверно confirmed'); assert(stats.clarify === 2, 'Неверно clarify'); assert(stats.notfound === 1, 'Неверно notfound'); assert(stats.total === 4, 'Неверно total'); }); test('Добавление критерия', () => { const criteria = []; const newCriteria = matcher.addCustomCriterion(criteria, 'Тест', true, 4, ['синоним1', 'синоним2']); assert(newCriteria.length === 1, 'Критерий не добавлен'); assert(newCriteria[0].label === 'Тест', 'Неверная метка'); assert(newCriteria[0].weight === 4, 'Неверный вес'); assert(newCriteria[0].synonyms.length === 2, 'Неверное количество синонимов'); }); test('Удаление критерия', () => { const criteria = [ { id: '1', label: 'First' }, { id: '2', label: 'Second' } ]; const result = matcher.removeCriterion(criteria, '1'); assert(result.length === 1, 'Критерий не удалён'); assert(result[0].id === '2', 'Неверный критерий удалён'); }); test('Обновление статуса', () => { const results = [ { criterionId: '1', status: 'notfound', note: '' } ]; const updated = matcher.updateCriterionStatus(results, '1', 'clarify', 'Нужно уточнить'); assert(updated[0].status === 'clarify', 'Статус не обновлён'); assert(updated[0].note === 'Нужно уточнить', 'Примечание не обновлено'); }); test('Экспорт сводки', () => { const summary = matcher.exportSummary( 'Текст вакансии', 'Текст резюме', [{ id: '1', label: 'Требование' }], { results: [{ label: 'Требование', required: true, weight: 5, status: 'confirmed', evidence: ['цитата'] }] }, { questions: [{ label: 'Требование', question: 'Вопрос?', followUp: [] }] } ); assert(summary.coverage.percentage === 100, 'Неверное покрытие'); assert(summary.results.length > 0, 'Нет результатов'); assert(summary.interviewQuestions.length > 0, 'Нет вопросов'); }); test('Форматирование экспорта в текст', () => { const summary = { exportedAt: new Date().toISOString(), coverage: { percentage: 80, confirmed: 4, toClarify: 1, notFound: 0, total: 5 }, results: [{ requirement: 'JavaScript', type: 'Обязательное', weight: 5, status: 'Подтверждено', evidence: 'Опыт 3 года' }], interviewQuestions: [] }; const text = matcher.formatExportText(summary); assert(text.includes('HR CANDIDATE MATCHER'), 'Нет заголовка'); assert(text.includes('80%'), 'Нет процента покрытия'); assert(text.includes('JavaScript'), 'Нет требования'); }); console.log(`\n${passed} passed, ${failed} failed`); if (failed > 0) { process.exit(1); }