// 순수함수 회귀 테스트. 실행(로컬, 의존성 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,
  buildPartOutline,
  partitionOutlineItems,
  partGroupViews,
  groupKeyByChunkId,
  FRONT_MATTER_KEY,
  FRONT_MATTER_LABEL,
  sectionTypeLabel,
  type DocumentSection,
} from './headingPath.ts';

let _id = 0;
function sec(p: Partial<DocumentSection>): 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**<sup>2</sup>'), '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**<sup>1</sup> > 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('collapseWindows: bodyText — 정상 leaf 는 자기 본문, split-parent 는 window 본문만 이어붙임', () => {
  // 정상 leaf → 자기 text 가 본문
  const leaf = collapseWindows([sec({ heading_path: 'Intro', node_type: null, text: '서론 본문' })]);
  assert.equal(leaf[0].bodyText, '서론 본문');

  // split-parent(heading 줄뿐) + window 2개 → window 본문만 순서대로 합침(헤딩 제외)
  const split = collapseWindows([
    sec({ heading_path: 'Article 5', node_type: 'chapter_split', is_leaf: false, char_start: 120, text: '# Article 5' }),
    sec({ heading_path: 'Article 5', node_type: 'window', is_leaf: true, text: '본문 조각1' }),
    sec({ heading_path: 'Article 5', node_type: 'window', is_leaf: true, text: '본문 조각2' }),
  ]);
  assert.equal(split.length, 1);
  assert.equal(split[0].bodyText, '본문 조각1\n\n본문 조각2', 'split-parent heading 제외, window 본문만 합침');

  // legacy window 런(선행 split-parent 없음) → 첫 window 자기 본문 + 흡수 조각
  const legacy = collapseWindows([
    sec({ heading_path: 'Pearson', node_type: 'window', text: 'p1' }),
    sec({ heading_path: 'Pearson', node_type: 'window', text: 'p2' }),
  ]);
  assert.equal(legacy.length, 1);
  assert.equal(legacy[0].bodyText, 'p1\n\np2');
});

test('collapseWindows: 절-레벨 분석 집계 — windowed 절은 window 멤버에서 type 다수결/conf 평균/summaries 합본', () => {
  // split-parent(분석 없음) + window 3개(요약·유형·신뢰도 보유) → 대표에 집계
  const out = collapseWindows([
    sec({ heading_path: 'Sec A', node_type: 'section_split', is_leaf: false, char_start: 10, text: '# Sec A', section_type: null, summary: null, confidence: null }),
    sec({ heading_path: 'Sec A', node_type: 'window', text: 'b1', section_type: 'requirement', summary: '요약1', confidence: 0.9 }),
    sec({ heading_path: 'Sec A', node_type: 'window', text: 'b2', section_type: 'requirement', summary: '요약2', confidence: 0.8 }),
    sec({ heading_path: 'Sec A', node_type: 'window', text: 'b3', section_type: 'overview', summary: '', confidence: 1.0 }),
  ]);
  assert.equal(out.length, 1);
  assert.equal(out[0].sectionType, 'requirement', '다수결 = requirement(2) > overview(1)');
  assert.ok(Math.abs(out[0].confidence! - 0.9) < 1e-9, '평균 (0.9+0.8+1.0)/3 = 0.9');
  assert.deepEqual(out[0].summaries, ['요약1', '요약2'], '빈 요약 제외, 순서 유지');

  // 단일 leaf 는 대표 자신의 분석
  const single = collapseWindows([sec({ heading_path: 'X', node_type: null, text: 'body', section_type: 'definition', summary: '정의 요약', confidence: 0.7 })]);
  assert.equal(single[0].sectionType, 'definition');
  assert.equal(single[0].confidence, 0.7);
  assert.deepEqual(single[0].summaries, ['정의 요약']);

  // 분석 전혀 없는 절 → null/빈
  const none = collapseWindows([sec({ heading_path: 'Y', node_type: null, text: 'body' })]);
  assert.equal(none[0].sectionType, null);
  assert.equal(none[0].confidence, null);
  assert.deepEqual(none[0].summaries, []);
});

test('collapseWindows: 비인접 window 도 parent_id 로 split-parent 에 흡수 (빈 split 행 방지)', () => {
  // 실데이터 버그: split-parent(chunk_index 1143)와 그 window(1233~)가 비인접 → 인접 흡수 실패로
  // 빈 split 행 + 별도 window-그룹 행 2개로 쪼개짐. parent_id 링크로 정확히 합친다.
  const out = collapseWindows([
    sec({ chunk_id: 10, heading_path: 'FOREWORD', node_type: 'section_split', is_leaf: false, char_start: 5, text: '# FOREWORD' }),
    sec({ chunk_id: 11, heading_path: 'POLICY', node_type: null, text: '정책 본문' }), // 사이에 낀 다른 절
    sec({ chunk_id: 12, heading_path: 'FOREWORD', node_type: 'window', parent_id: 10, text: '서문 조각1', section_type: 'overview', summary: '요약A', confidence: 0.9 }),
    sec({ chunk_id: 13, heading_path: 'FOREWORD', node_type: 'window', parent_id: 10, text: '서문 조각2', section_type: 'overview', summary: '요약B', confidence: 0.8 }),
  ]);
  assert.equal(out.length, 2, 'FOREWORD(split, window 흡수) + POLICY = 2행 (빈 split 행 없음)');
  assert.equal(out[0].section.chunk_id, 10, '대표 = split-parent(char_start 보유)');
  assert.equal(out[0].bodyText, '서문 조각1\n\n서문 조각2', '비인접 window 본문을 split-parent 에 흡수');
  assert.equal(out[0].fragmentCount, 2);
  assert.equal(out[0].sectionType, 'overview');
  assert.deepEqual(out[0].summaries, ['요약A', '요약B']);
  assert.equal(out[1].section.chunk_id, 11, '사이 낀 절은 별도 행 유지');
  assert.equal(out[1].bodyText, '정책 본문');
});

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);
});

// ── D9: cleanHeading ASME 개정바 ðNÞ strip ──
test('cleanHeading: ASME 개정바 ðNÞ 통째 제거 (가운데 25 안 남김)', () => {
  assert.equal(
    cleanHeading('<sup>ð</sup>**25**<sup>Þ</sup> **PG-5.4 Size Limits**'),
    'PG-5.4 Size Limits',
  );
  // 개정바 없는 일반 제목은 그대로 (회귀)
  assert.equal(cleanHeading('#### **PG-2 SERVICE LIMITATIONS**'.replace(/^#+\s*/, '')), 'PG-2 SERVICE LIMITATIONS');
});

// ── D7: buildPartOutline — front-matter 분리 + PART 그룹 ──
test('buildPartOutline: front-matter 분리 + PART 그룹', () => {
  const sections = [
    sec({ heading_path: 'TABLE OF CONTENTS', section_title: 'TABLE OF CONTENTS' }),
    sec({ heading_path: 'Honors and Awards Committee', section_title: 'Honors and Awards Committee' }),
    sec({ heading_path: 'PART PG GENERAL > PG-1 SCOPE', section_title: 'PG-1 SCOPE' }),
    sec({ heading_path: 'PART PG GENERAL > PG-2 SERVICE', section_title: 'PG-2 SERVICE' }),
    sec({ heading_path: 'PART PW > PW-1 SCOPE', section_title: 'PW-1 SCOPE' }),
  ];
  const o = buildPartOutline(sections);
  assert.equal(o.hasParts, true);
  assert.equal(o.frontMatter.length, 2); // TOC + Committee
  assert.equal(o.groups.length, 2); // PART PG, PART PW
  assert.equal(o.groups[0].key, 'PART PG GENERAL');
  assert.equal(o.groups[0].items.length, 2); // PG-1, PG-2
  assert.equal(o.groups[1].key, 'PART PW');
  assert.equal(o.groups[1].items.length, 1);
});

test('buildPartOutline: split-parent + window 가 같은 PART 그룹에서 1항목으로 흡수', () => {
  const sections = [
    sec({ heading_path: 'PART PG GENERAL > PG-27 CYL', section_title: 'PG-27 CYL', node_type: 'section_split', chunk_id: 100, text: 'PG-27 CYL' }),
    sec({ heading_path: 'PART PG GENERAL > PG-27 CYL', section_title: 'PG-27 CYL', node_type: 'window', parent_id: 100, text: 'body part 1' }),
    sec({ heading_path: 'PART PG GENERAL > PG-27 CYL', section_title: 'PG-27 CYL', node_type: 'window', parent_id: 100, text: 'body part 2' }),
  ];
  const o = buildPartOutline(sections);
  assert.equal(o.hasParts, true);
  assert.equal(o.groups.length, 1);
  assert.equal(o.groups[0].items.length, 1); // split-parent + 2 window → 1 항목
  assert.equal(o.groups[0].items[0].fragmentCount, 2);
});

test('buildPartOutline: content part 없으면 hasParts=false (폴백 신호)', () => {
  const o = buildPartOutline([sec({ heading_path: 'Intro', section_title: 'Intro' })]);
  assert.equal(o.hasParts, false);
  assert.equal(o.groups.length, 0);
});

test('buildPartOutline: PART/SUBSECTION 마커 없으면(항목코드만) hasParts=false → 폴백', () => {
  // 실 ASME 코드(5180/5210)는 PART/SUBSECTION 마커를 갖는다. PART 가 0 인 문서(항목코드만)는
  // 접을 PART 가 없으므로 hasParts=false → 호출자가 groupOrFlat/flat 으로 폴백.
  const o = buildPartOutline([
    sec({ heading_path: 'FOREWORD', section_title: 'FOREWORD' }),
    sec({ heading_path: null, section_title: 'U-1 적용범위' }),
  ]);
  assert.equal(o.hasParts, false);
  assert.equal(o.groups.length, 0);
});

test('buildPartOutline: (NON)MANDATORY APPENDIX 도 최상위 섹션 경계 — 마지막 PART 흡수 방지', () => {
  // 5180 실측: 부록을 마커로 안 잡으면 마지막 PART(PHRSG)가 부록 289항목을 carry-forward 흡수(=300).
  const o = buildPartOutline([
    sec({ heading_path: 'PART PHRSG REQUIREMENTS > PHRSG-1', section_title: 'PHRSG-1' }),
    sec({ heading_path: 'PHRSG-2 SCOPE', section_title: 'PHRSG-2' }), // PHRSG 로 carry
    sec({ heading_path: 'MANDATORY APPENDIX IV LOCAL THIN AREAS', section_title: '...' }),
    sec({ heading_path: 'IV-1 GENERAL', section_title: 'IV-1' }), // APPENDIX IV 로 carry
    sec({ heading_path: 'NONMANDATORY APPENDIX A EXPLANATION', section_title: '...' }),
  ]);
  assert.deepEqual(o.groups.map((g) => [g.key.slice(0, 24), g.items.length]), [
    ['PART PHRSG REQUIREMENTS', 2],          // PHRSG-1 + PHRSG-2(carry), 부록 안 섞임
    ['MANDATORY APPENDIX IV LO', 2],         // 부록 헤딩 + IV-1(carry)
    ['NONMANDATORY APPENDIX A ', 1],
  ]);
});

test('buildPartOutline: 본문 cross-ref/문장 false PART 차단 (5210 stale 패턴)', () => {
  // 혼합대소문자 'Part D…' · 코드 뒤 비대문자(한글) 문장 'PART UW 규정은…' · 비대문자 코드 'PART 층이…'
  // = 전부 본문이라 PART 아님. 깨끗한 PART 0 → hasParts=false → flat 폴백(가짜 그룹 0).
  const o = buildPartOutline([
    sec({ heading_path: 'Part D, Subpart 3의 해당 재료', section_title: 'Part D…' }),
    sec({ heading_path: 'PART UW 규정은 용접에 의해 제작되는', section_title: 'PART UW 규정은…' }),
    sec({ heading_path: 'PART 층이 진 구조로 조립되는', section_title: 'PART 층이…' }),
  ]);
  assert.equal(o.hasParts, false);
});

test('buildPartOutline: SUBSECTION 마커도 PART 경계로 인식(Sec VIII)', () => {
  const o = buildPartOutline([
    sec({ heading_path: 'TOC', section_title: 'TOC' }),
    sec({ heading_path: 'SUBSECTION A GENERAL > UG-1', section_title: 'UG-1' }),
    sec({ heading_path: 'SUBSECTION B > UW-1', section_title: 'UW-1' }),
  ]);
  assert.equal(o.hasParts, true);
  assert.equal(o.frontMatter.length, 1);
  assert.deepEqual(o.groups.map((g) => g.key), ['SUBSECTION A GENERAL', 'SUBSECTION B']);
});

// ── D8: partitionOutlineItems — 이미 collapse 된 OutlineItem 재배치(인스턴스 보존) ──
test('partitionOutlineItems: flat outline 의 인스턴스를 그대로 재배치(재-collapse 없음)', () => {
  const sections = [
    sec({ heading_path: 'TABLE OF CONTENTS', section_title: 'TABLE OF CONTENTS' }),
    sec({ heading_path: 'PART PG GENERAL > PG-1 SCOPE', section_title: 'PG-1 SCOPE' }),
    sec({ heading_path: 'PART PG GENERAL > PG-2 SERVICE', section_title: 'PG-2 SERVICE' }),
    sec({ heading_path: 'PART PW > PW-1 SCOPE', section_title: 'PW-1 SCOPE' }),
  ];
  const flat = collapseWindows(sections); // 컴포넌트의 outline 과 동일 경로
  const o = partitionOutlineItems(flat);
  assert.equal(o.hasParts, true);
  assert.equal(o.frontMatter.length, 1);
  assert.equal(o.groups.length, 2);
  // ★ 인스턴스 동일성: 재배치된 item 이 flat outline 의 바로 그 객체여야 selectedSectionId 정합.
  assert.ok(o.frontMatter[0] === flat[0], 'front-matter item = flat[0] 인스턴스');
  assert.ok(o.groups[0].items[0] === flat[1], 'PART PG 첫 item = flat[1] 인스턴스');
  assert.ok(o.groups[1].items[0] === flat[3], 'PART PW item = flat[3] 인스턴스');
  // chunk_id 집합이 flat 과 정확히 일치(클릭→selectedSectionId 조회 실패 없음).
  const flatIds = flat.map((it) => it.section.chunk_id).sort();
  const partIds = [...o.frontMatter, ...o.groups.flatMap((g) => g.items)]
    .map((it) => it.section.chunk_id).sort();
  assert.deepEqual(partIds, flatIds);
});

test('partitionOutlineItems: 비-PART top-segment 항목은 직전 PART 로 carry-forward (marker 트리 불규칙 흡수)', () => {
  // ★ 5180 실측 패턴: PART 아래 직접 중첩 안 된 항목('PG-28'·'GENERAL')의 top-segment 가 PART 가
  //   아니다 → 단순 segs[0] 그룹핑이면 가짜 그룹 폭발. carry-forward 가 직전 PART 로 흡수해야 한다.
  const items = collapseWindows([
    sec({ heading_path: 'TOC', section_title: 'TOC' }),
    sec({ heading_path: 'PART PG GENERAL > PG-1', section_title: 'PG-1' }),
    sec({ heading_path: 'PG-28 EXTERNAL PRESSURE', section_title: 'PG-28' }), // top-seg ≠ PART → carry
    sec({ heading_path: 'OPENINGS AND COMPENSATION', section_title: 'OPENINGS' }), // carry
    sec({ heading_path: 'PART PW > PW-1', section_title: 'PW-1' }),
    sec({ heading_path: 'GENERAL', section_title: 'GENERAL' }), // PART PW 로 carry
  ]);
  const o = partitionOutlineItems(items);
  assert.equal(o.hasParts, true);
  assert.equal(o.frontMatter.length, 1);
  assert.equal(o.groups.length, 2, 'PART PG / PART PW 단 2그룹(가짜 그룹 0)');
  assert.equal(o.groups[0].key, 'PART PG GENERAL');
  assert.equal(o.groups[0].items.length, 3, 'PG-1 + PG-28 + OPENINGS carry');
  assert.equal(o.groups[1].key, 'PART PW');
  assert.equal(o.groups[1].items.length, 2, 'PW-1 + GENERAL carry');
  // carry 된 항목도 인스턴스 보존(클릭 정합)
  assert.ok(o.groups[0].items[1].section.section_title === 'PG-28');
});

test('partitionOutlineItems: buildPartOutline 과 그룹 구조 동치(collapse→partition == partition∘collapse)', () => {
  const sections = [
    sec({ heading_path: 'PART PG > PG-27 CYL', section_title: 'PG-27 CYL', node_type: 'section_split', chunk_id: 100, text: 'PG-27 CYL' }),
    sec({ heading_path: 'PART PG > PG-27 CYL', section_title: 'PG-27 CYL', node_type: 'window', parent_id: 100, text: 'b1' }),
    sec({ heading_path: 'PART PG > PG-27 CYL', section_title: 'PG-27 CYL', node_type: 'window', parent_id: 100, text: 'b2' }),
    sec({ heading_path: 'PART PW > PW-1', section_title: 'PW-1' }),
  ];
  const viaBuild = buildPartOutline(sections);
  const viaPartition = partitionOutlineItems(collapseWindows(sections));
  assert.equal(viaBuild.hasParts, viaPartition.hasParts);
  assert.deepEqual(viaBuild.groups.map((g) => [g.key, g.items.length]), viaPartition.groups.map((g) => [g.key, g.items.length]));
  // window 흡수 후 PART PG 는 1 항목(fragmentCount 2).
  assert.equal(viaPartition.groups[0].items.length, 1);
  assert.equal(viaPartition.groups[0].items[0].fragmentCount, 2);
});

// ── D8: partGroupViews / groupKeyByChunkId — 렌더 그룹 평탄화 + auto-expand 역인덱스 ──
test('partGroupViews: front-matter 를 첫 그룹(sentinel key)으로, 이어 PART 그룹', () => {
  const sections = [
    sec({ heading_path: 'TOC', section_title: 'TOC' }),
    sec({ heading_path: 'PART PG > PG-1', section_title: 'PG-1' }),
    sec({ heading_path: 'PART PW > PW-1', section_title: 'PW-1' }),
  ];
  const views = partGroupViews(buildPartOutline(sections));
  assert.equal(views.length, 3);
  assert.equal(views[0].key, FRONT_MATTER_KEY);
  assert.equal(views[0].label, FRONT_MATTER_LABEL);
  assert.equal(views[0].isFrontMatter, true);
  assert.equal(views[1].key, 'PART PG');
  assert.equal(views[1].label, 'PART PG');
  assert.equal(views[1].isFrontMatter, false);
  assert.equal(views[2].key, 'PART PW');
  // 모든 key 유일(Svelte each key 안전)
  const keys = views.map((v) => v.key);
  assert.equal(new Set(keys).size, keys.length);
});

test('partGroupViews: front-matter 없으면 PART 그룹만(첫 그룹 sentinel 없음)', () => {
  const sections = [
    sec({ heading_path: 'PART PG > PG-1', section_title: 'PG-1' }),
    sec({ heading_path: 'PART PW > PW-1', section_title: 'PW-1' }),
  ];
  const views = partGroupViews(buildPartOutline(sections));
  assert.equal(views.length, 2);
  assert.ok(views.every((v) => !v.isFrontMatter));
  assert.equal(views[0].key, 'PART PG');
});

test('groupKeyByChunkId: 대표 chunk_id → 소속 group key (auto-expand 역인덱스)', () => {
  const sections = [
    sec({ chunk_id: 1, heading_path: 'TOC', section_title: 'TOC' }),
    sec({ chunk_id: 2, heading_path: 'PART PG > PG-1', section_title: 'PG-1' }),
    sec({ chunk_id: 3, heading_path: 'PART PG > PG-2', section_title: 'PG-2' }),
    sec({ chunk_id: 4, heading_path: 'PART PW > PW-1', section_title: 'PW-1' }),
  ];
  const views = partGroupViews(buildPartOutline(sections));
  const idx = groupKeyByChunkId(views);
  assert.equal(idx.get(1), FRONT_MATTER_KEY);
  assert.equal(idx.get(2), 'PART PG');
  assert.equal(idx.get(3), 'PART PG');
  assert.equal(idx.get(4), 'PART PW');
  assert.equal(idx.get(999), undefined);
});
