diff --git a/test.js b/test.js new file mode 100644 index 0000000..c33cfc5 --- /dev/null +++ b/test.js @@ -0,0 +1,172 @@ +/** + * Тесты для 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); +}