diff --git a/deploy/app.js b/deploy/app.js index e37cd1a..901aa44 100644 --- a/deploy/app.js +++ b/deploy/app.js @@ -4,8 +4,7 @@ var VM_API = 'https://contracts.kube5s.ru'; var UPLOAD_URL = VM_API + '/upload'; var CONVERT_URL = VM_API + '/convert-doc'; var UNZIP_URL = VM_API + '/unzip-upload'; -var UNZIP_URL = 'https://contracts.kube5s.ru/unzip-upload'; -var SITE_URL = ''; // same origin for api calls +// SITE_URL удалён (Фаза 4) — не использовался var fileInput = document.getElementById('fileInput'); var fileTable = document.getElementById('fileTable'); @@ -464,8 +463,6 @@ async function showText(i) { window.closeModal = closeModal; window.showText = showText; -// ── Промпт ─────────────────────────────────────────────────── -var promptEditor = document.getElementById('promptEditor'); // ── Промпты (версионные) ───────────────────────────────────── var promptRole = 'diff'; // текущая выбранная роль var promptEditor = document.getElementById('promptEditor'); diff --git a/deploy/tests.js b/deploy/tests.js index 2fdc15f..8c5bc8f 100644 --- a/deploy/tests.js +++ b/deploy/tests.js @@ -117,6 +117,16 @@ eq(statusToHTML({ kind: 'parsed', count: 5, elapsed: '2.3' }), '✗ тест', 'error с текстом'); eq(statusToHTML({ kind: 'error' }), '✗ Неизвестная ошибка', 'error без текста'); +// Краевые случаи +eq(statusToHTML(undefined), '', 'undefined → пусто'); +eq(statusToHTML({ kind: 'uploading' }), '↑ 0%', 'uploading без pct → 0%'); +eq(statusToHTML({ kind: 'uploading', pct: -5 }), '↑ -5%', 'uploading отрицательный pct'); +eq(statusToHTML({ kind: 'uploading', pct: 100 }), '↑ 100%', 'uploading 100%'); +eq(statusToHTML({ kind: 'parsed', count: 0 }), '✓ 0 эл.', 'parsed count=0'); +eq(statusToHTML({ kind: 'parsed', count: 5, elapsed: '0' }), '✓ 5 эл. (0с)', 'parsed elapsed=0'); +eq(statusToHTML({ kind: 'error', text: '' }), '✗ Неизвестная ошибка', 'error с пустым текстом → дефолт'); +eq(statusToHTML({ kind: 'unknown_kind' }), '', 'неизвестный kind → пусто'); + // ── Тесты: applyParseResult ────────────────────────────────── console.log('\n=== applyParseResult ==='); @@ -149,6 +159,25 @@ applyParseResult(e4, null); eq(e4.status.kind, 'error', 'null: kind=error'); eq(e4.status.text, 'Неизвестная ошибка', 'null: дефолтный текст'); +// parsed с пустыми полями +var e5 = {}; +applyParseResult(e5, { status: 'parsed' }); +eq(e5.status.kind, 'parsed', 'parsed без element_count: kind=parsed'); +eq(e5.status.count, undefined, 'parsed без element_count: count=undefined'); +// parsed с element_count=0 +var e6 = {}; +applyParseResult(e6, { status: 'parsed', element_count: 0 }); +eq(e6.status.count, 0, 'parsed element_count=0'); +// error без текста ошибки +var e7 = {}; +applyParseResult(e7, { status: 'error' }); +eq(e7.status.text, 'ошибка парсинга', 'error без текста: дефолт "ошибка парсинга"'); +// entry с уже существующими полями (не должны мешать) +var e8 = { existing: true, parsed: false }; +applyParseResult(e8, { status: 'parsed', element_count: 7 }, '3.0'); +assert(e8.existing === true, 'старое поле не затёрто'); +eq(e8.status.count, 7, 'новые данные поверх старых'); + // ── Тесты: reconcileSelection ──────────────────────────────── console.log('\n=== reconcileSelection ==='); @@ -166,6 +195,23 @@ eq(result[1].name, 'c.docx', 'c.docx остался'); // Исходный массив не мутирован eq(files.length, 3, 'исходный массив не мутирован'); +// Пустые входы +var emptyResult = reconcileSelection([], []); +eq(emptyResult.length, 0, 'пустые массивы → пусто'); + +// Все совпадают +var allMatch = reconcileSelection([{name:'a'}], [{name:'a'}]); +eq(allMatch.length, 1, 'все совпадают → 1'); + +// Ни один не совпадает +var noMatch = reconcileSelection([{name:'a'},{name:'b'}], [{name:'c'},{name:'d'}]); +eq(noMatch.length, 0, 'нет совпадений → пусто'); + +// Дубликаты имён в newFiles (должен работать — Set) +var dupNames = [{name:'a'}, {name:'a'}, {name:'b'}]; +var dupResult = reconcileSelection([{name:'a'},{name:'b'}], dupNames); +eq(dupResult.length, 2, 'дубликаты в newFiles — Set обрабатывает'); + // ── Тесты: renderGroupCard ─────────────────────────────────── console.log('\n=== renderGroupCard ==='); @@ -199,6 +245,22 @@ var groupRunning = { var htmlRunning = renderGroupCard(groupRunning, 1); contains(htmlRunning, 'disabled', 'кнопка disabled при running'); +// Без контрагента +var groupNoCP = { contract_number: '001', documents: [] }; +var htmlNoCP = renderGroupCard(groupNoCP, 5); +contains(htmlNoCP, 'контрагент не определён', 'без counterparty: заглушка'); + +// Неизвестный doc_type +var groupUnknown = { contract_number: 'X', counterparty: 'Y', documents: [{ doc_type: 'unknown_type', filename: 'f.docx' }] }; +var htmlUnknown = renderGroupCard(groupUnknown, 6); +contains(htmlUnknown, 'unknown_type', 'неизвестный тип: выводится как есть'); + +// Пустой список документов +var groupEmpty = { contract_number: 'E', counterparty: 'Empty', documents: [] }; +var htmlEmpty = renderGroupCard(groupEmpty, 7); +contains(htmlEmpty, 'Договор №E', 'пустые документы: карточка рендерится'); +contains(htmlEmpty, 'Сравнить', 'пустые документы: кнопка есть'); + // ── Тесты: renderGroupCardDone ─────────────────────────────── console.log('\n=== renderGroupCardDone ==='); @@ -277,6 +339,23 @@ applyCompareEvent(sections, { type: 'extract_error', supplement_id: 's2', filena eq(sections['s2'].status, 'error', 'новая секция с ошибкой'); eq(sections['s2'].filename, 'bad.docx', 'filename для ошибочной секции'); +// Неизвестный тип события — не должен падать +applyCompareEvent(sections, { type: 'unknown_type', supplement_id: 's3' }); +assert(sections['s3'] === undefined, 'неизвестный тип: секция НЕ создана'); + +// llm_done для несуществующей секции — не должен падать +applyCompareEvent(sections, { type: 'llm_done', supplement_id: 's99', ops_count: 1 }); + +// apply_error (должен работать как extract_error) +applyCompareEvent(sections, { type: 'apply_error', supplement_id: 's4', filename: 'apply_err.docx', error: 'apply boom' }); +eq(sections['s4'].status, 'error', 'apply_error: секция с ошибкой создана'); +eq(sections['s4'].error, 'apply boom', 'apply_error: текст ошибки'); + +// applied без summary +applyCompareEvent(sections, { type: 'applied', supplement_id: 's1', ops: [] }); +eq(sections['s1'].status, 'applied', 'applied без summary: status ок'); +eq(sections['s1'].ops.length, 0, 'applied с пустыми ops'); + // ── Тесты: renderCompareSectionHeader ──────────────────────── console.log('\n=== renderCompareSectionHeader ==='); @@ -300,6 +379,21 @@ contains(tableHtml, 'Стойка', 'имя услуги'); contains(tableHtml, '100', 'цена'); contains(tableHtml, '2024-01-01', 'дата'); +// Пустые ops +var emptyTable = renderCompareOpsTable([]); +contains(emptyTable, '
| ', 'null new_row: пустые ячейки (не падает)'); + +// ops с неизвестным action (без класса) +var unknownAct = renderCompareOpsTable([{ action: 'MERGE', new_row: {} }]); +assert(unknownAct.indexOf('diff-added') === -1, 'MERGE: нет diff-added'); +assert(unknownAct.indexOf('diff-deleted') === -1, 'MERGE: нет diff-deleted'); +assert(unknownAct.indexOf('diff-changed') === -1, 'MERGE: нет diff-changed'); + // ── Тесты: renderCompareSectionBody ────────────────────────── console.log('\n=== renderCompareSectionBody ==='); @@ -319,12 +413,120 @@ contains(bodyHtml, '-0', 'deleted=0'); contains(bodyHtml, '?2', 'unresolved=2'); contains(bodyHtml, 'diff-changed', 'UPDATE → diff-changed'); -// ── Итоги ──────────────────────────────────────────────────── -console.log('\n========================================'); -console.log('PASS: ' + passed); -console.log('FAIL: ' + failed); -console.log('========================================'); +// Без unresolved +var secNoUnresolved = { status: 'applied', summary: { added: 1, updated: 0, deleted: 0 }, ops: [] }; +var bodyNoUnr = renderCompareSectionBody(secNoUnresolved); +assert(bodyNoUnr.indexOf('?') === -1, 'без unresolved: нет знака ?'); -if (failed > 0) { - process.exit(1); +// Отрицательные значения summary (не должно быть, но не падать) +var secNeg = { status: 'applied', summary: { added: -1, updated: 0, deleted: 0 }, ops: [{ action: 'ADD', new_row: {} }] }; +var bodyNeg = renderCompareSectionBody(secNeg); +contains(bodyNeg, '+-1', 'отрицательные summary: рендерится без ошибок'); + +// ops с частичным new_row (не все поля) +var secPartial = { status: 'applied', summary: { added: 1 }, ops: [ + { action: 'ADD', new_row: { name: 'Только имя' } } +]}; +var bodyPartial = renderCompareSectionBody(secPartial); +contains(bodyPartial, 'Только имя', 'частичный new_row: имя есть'); +// Проверим что нет undefined в выводе +assert(bodyPartial.indexOf('undefined') === -1, 'нет undefined в выводе'); + +// ── Тесты: escHtml ─────────────────────────────────────────── +console.log('\n=== escHtml ==='); + +eq(escHtml(null), '', 'escHtml null → пусто'); +eq(escHtml(undefined), '', 'escHtml undefined → пусто'); +eq(escHtml('hello'), 'hello', 'escHtml обычный текст'); +eq(escHtml(' |