// 순수함수 회귀 테스트. 실행(로컬, 의존성 0): node --test src/lib/utils/headingPath.test.ts // (Node ≥23 또는 22.6+ --experimental-strip-types — TS 타입 네이티브 strip.) import { test } from 'node:test'; import assert from 'node:assert/strict'; import { cleanHeading, pathSegments, collapseWindows, groupOrFlat, sectionTypeLabel, type DocumentSection, } from './headingPath.ts'; let _id = 0; function sec(p: Partial): DocumentSection { return { chunk_id: ++_id, section_title: null, heading_path: null, level: null, node_type: null, is_leaf: true, section_type: null, summary: null, confidence: null, ...p, }; } test('cleanHeading: 마크다운/HTML 잔재 strip', () => { assert.equal(cleanHeading('**UG-5 PLATE**2'), 'UG-5 PLATE'); assert.equal(cleanHeading(' **DESIGN** '), 'DESIGN'); assert.equal(cleanHeading('a b\tc'), 'a b c'); assert.equal(cleanHeading(null), ''); assert.equal(cleanHeading(''), ''); }); test('pathSegments: > 분할 + 정제', () => { assert.deepEqual(pathSegments('**A** > **B**1 > C'), ['A', 'B', 'C']); assert.deepEqual(pathSegments(null), []); assert.deepEqual(pathSegments(' '), []); }); test('sectionTypeLabel: 한글 매핑 + passthrough', () => { assert.equal(sectionTypeLabel('requirement'), '요건'); assert.equal(sectionTypeLabel('unknown_type'), 'unknown_type'); assert.equal(sectionTypeLabel(null), null); }); test('collapseWindows: 연속 동일 heading window 만 dedupe, 순서 유지', () => { const input = [ sec({ heading_path: 'Intro', node_type: null }), sec({ heading_path: 'Pearson', node_type: 'window' }), sec({ heading_path: 'Pearson', node_type: 'window' }), sec({ heading_path: 'Pearson', node_type: 'window' }), sec({ heading_path: 'Conf', node_type: null }), sec({ heading_path: 'Pearson', node_type: 'window' }), // 비연속 → 새 항목 ]; const out = collapseWindows(input); assert.equal(out.length, 4); assert.equal(out[0].fragmentCount, 1); // Intro assert.equal(out[1].fragmentCount, 3); // Pearson ×3 합침 assert.equal(out[2].fragmentCount, 1); // Conf assert.equal(out[3].fragmentCount, 1); // 비연속 Pearson // 순서 보존 assert.deepEqual( out.map((o) => cleanHeading(o.section.heading_path)), ['Intro', 'Pearson', 'Conf', 'Pearson'], ); }); test('[C2] collapseWindows: split-parent + window 들 → rail 1행, 대표=split-parent(char_start 보유)', () => { const input = [ sec({ section_title: 'Article 5', heading_path: 'Article 5', node_type: 'chapter_split', is_leaf: false, char_start: 120 }), sec({ section_title: 'Article 5', heading_path: 'Article 5', node_type: 'window', is_leaf: true, char_start: null }), sec({ section_title: 'Article 5', heading_path: 'Article 5', node_type: 'window', is_leaf: true, char_start: null }), ]; const out = collapseWindows(input); assert.equal(out.length, 1, 'split-parent + 2 window → rail 1행'); // 대표 = split-parent (char_start 보유) → jump 성립 assert.equal(out[0].section.node_type, 'chapter_split'); assert.equal(out[0].section.char_start, 120); assert.equal(out[0].fragmentCount, 2, 'window 조각 수 = 2 (split-parent 자신 제외)'); }); test('groupOrFlat: 적은 그룹 + 낮은 기타% → group (5140-류)', () => { // 3 top segment × 4 = 12절, window 없음 → group_count 3, 기타 0% const sections: DocumentSection[] = []; for (const top of ['장1', '장2', '장3']) { for (let i = 0; i < 4; i++) sections.push(sec({ heading_path: `${top} > 절${i}` })); } const layout = groupOrFlat(sections); assert.equal(layout.mode, 'group'); assert.equal(layout.groups.length, 3); assert.deepEqual(layout.groups.map((g) => g.key), ['장1', '장2', '장3']); // 등장순서 assert.equal(layout.groups[0].items.length, 4); }); test('groupOrFlat: 기타% ≥ 50 → flat 강등 (5186/5225-류)', () => { const sections: DocumentSection[] = [ sec({ heading_path: 'A > a1' }), sec({ heading_path: 'B > b1' }), sec({ node_type: 'window', heading_path: 'W1' }), sec({ node_type: 'window', heading_path: 'W2' }), sec({ node_type: 'section_split', heading_path: 'S1' }), sec({ node_type: 'window', heading_path: 'W3' }), // 기타 4/6 = 66.7% ]; const layout = groupOrFlat(sections); assert.equal(layout.mode, 'flat'); assert.ok(layout.items.length > 0); }); test('groupOrFlat: group_count > 30 → flat 강등', () => { const sections: DocumentSection[] = []; for (let i = 0; i < 31; i++) sections.push(sec({ heading_path: `seg${i} > x` })); const layout = groupOrFlat(sections); assert.equal(layout.mode, 'flat'); }); test('groupOrFlat: 빈 입력 → flat, 항목 0', () => { const layout = groupOrFlat([]); assert.equal(layout.mode, 'flat'); assert.equal(layout.items.length, 0); });