"""Tests for MarkdownFileChunker (markdown parser + wikilink extraction). Wikilink convention here is strict: targets are taken literally, no short-form basename search, no implicit ``.md``, no folder-note expansion. ``lint:dangling`` handles validation; the parser is just a markdown-to-FileNode transformer. """ # pylint: disable=protected-access import asyncio import os import tempfile from unittest.mock import patch from reme.components.file_chunker import DefaultFileChunker, MarkdownFileChunker class temp_chdir: """Context manager to temporarily chdir into a path and restore on exit.""" def __init__(self, path): self.path = path self.old = None def __enter__(self): self.old = os.getcwd() os.chdir(self.path) return self def __exit__(self, *exc): os.chdir(self.old) def _write_md(tmpdir: str, name: str, body: str) -> str: """Drop a markdown file under tmpdir, return its relative path (matches cwd).""" if "/" in name: os.makedirs(os.path.join(tmpdir, os.path.dirname(name)), exist_ok=True) with open(os.path.join(tmpdir, name), "w", encoding="utf-8") as f: f.write(body) return name def test_parse_empty_file(): """An empty .md → FileNode, no chunks, no links.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): path = _write_md(tmp, "x.md", "") chunker = MarkdownFileChunker() node, chunks = await chunker.chunk(path) assert node.path == "x.md" assert chunks == [] assert node.links == [] print("✓ test_parse_empty_file passed") asyncio.run(run()) def test_invalid_utf8_is_replaced_without_modifying_source(): """Bad source bytes degrade the derived index but remain untouched on disk.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): source = b"# valid\ncontent before \xcd content after\n" path = os.path.join(tmp, "invalid.md") with open(path, "wb") as f: f.write(source) chunker = MarkdownFileChunker() with patch.object(chunker.logger, "warning") as warning: node, chunks = await chunker.chunk("invalid.md") assert node.path == "invalid.md" assert "content before \ufffd content after" in "\n".join(chunk.text for chunk in chunks) with open(path, "rb") as f: assert f.read() == source warning.assert_called_once_with( "Invalid utf-8 in invalid.md at byte 23 (bytes: cd); " "indexed with replacement characters; source file unchanged", ) asyncio.run(run()) def test_invalid_utf8_strict_policy_still_raises(): """Strict mode remains available when callers require exact decoding.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): with open(os.path.join(tmp, "invalid.md"), "wb") as f: f.write(b"valid\xcdinvalid") chunker = MarkdownFileChunker(invalid_encoding_policy="strict") try: await chunker.chunk("invalid.md") except UnicodeDecodeError as exc: assert exc.start == 5 else: raise AssertionError("strict policy must reject invalid UTF-8") asyncio.run(run()) def test_constructor_preserves_positional_arguments(): """The decoding policy must not shift the established positional parameters.""" chunker = MarkdownFileChunker("utf-8", 5000, False, 10, True, ["name"]) assert chunker.encoding == "utf-8" assert chunker.chunk_byte_size == 5000 assert chunker.embed_toc is False assert chunker.max_ast_sections == 10 assert chunker.include_frontmatter_in_metadata is True assert chunker.include_frontmatter_keys_in_metadata == ["name"] def test_plain_text_fallback_normalizes_newlines(): """Markdown fallback chunks stay stable for equivalent platform newlines.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): path = os.path.join(tmp, "fallback.md") with open(path, "wb") as f: f.write(b"# One\r\nbody\r# Two\r\nbody\r\n") chunker = MarkdownFileChunker(max_ast_sections=0) _, original_chunks = await chunker.chunk("fallback.md") with open(path, "wb") as f: f.write(b"# One\nbody\n# Two\nbody\n") _, normalized_chunks = await chunker.chunk("fallback.md") assert [chunk.text for chunk in original_chunks] == [chunk.text for chunk in normalized_chunks] assert [chunk.id for chunk in original_chunks] == [chunk.id for chunk in normalized_chunks] asyncio.run(run()) def test_invalid_ascii_is_replaced_for_markdown(): """Markdown byte accounting accepts the configured codec's replacement text.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): source = b"# valid\ncontent before \xff content after\n" path = os.path.join(tmp, "invalid.md") with open(path, "wb") as f: f.write(source) chunker = MarkdownFileChunker(encoding="ascii") _, chunks = await chunker.chunk("invalid.md") assert "content before ? content after" in "\n".join(chunk.text for chunk in chunks) with open(path, "rb") as f: assert f.read() == source asyncio.run(run()) def test_parse_frontmatter_only(): """A file with only frontmatter (no body) → no chunks, no links.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): path = _write_md(tmp, "fm.md", "---\nname: t\n---\n") chunker = MarkdownFileChunker() node, chunks = await chunker.chunk(path) assert node.front_matter.name == "t" assert chunks == [] assert node.links == [] print("✓ test_parse_frontmatter_only passed") asyncio.run(run()) def test_parse_frontmatter_metadata_is_opt_in(): """Chunk metadata preserves the old empty default unless explicitly enabled.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): body = ( "---\n" "name: locomo-event\n" "description: Jon lost his job\n" "conversation_date: 2023-01-19\n" "---\n" "Jon said he lost his job today.\n" ) path = _write_md(tmp, "daily/2023-01-19/locomo-event.md", body) chunker = MarkdownFileChunker(chunk_byte_size=500) _, chunks = await chunker.chunk(path) assert len(chunks) == 1 assert chunks[0].metadata == {} chunker = MarkdownFileChunker(chunk_byte_size=500, include_frontmatter_in_metadata=True) _, chunks = await chunker.chunk(path) assert len(chunks) == 1 assert chunks[0].metadata == { "name": "locomo-event", "description": "Jon lost his job", "conversation_date": "2023-01-19", } print("✓ test_parse_frontmatter_metadata_is_opt_in passed") asyncio.run(run()) def test_parse_frontmatter_metadata_keys_allowlist(): """``include_frontmatter_keys_in_metadata`` restricts copied keys to an allow-list.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): body = ( "---\n" "name: locomo-event\n" "description: Jon lost his job\n" "conversation_date: 2023-01-19\n" "---\n" "Jon said he lost his job today.\n" ) path = _write_md(tmp, "daily/2023-01-19/locomo-event.md", body) # Allow-list restricted to a single key. chunker = MarkdownFileChunker( chunk_byte_size=500, include_frontmatter_in_metadata=True, include_frontmatter_keys_in_metadata=["conversation_date"], ) _, chunks = await chunker.chunk(path) assert len(chunks) == 1 assert chunks[0].metadata == {"conversation_date": "2023-01-19"} # Allow-list with a key not present in frontmatter is a no-op for that key. chunker = MarkdownFileChunker( chunk_byte_size=500, include_frontmatter_in_metadata=True, include_frontmatter_keys_in_metadata=["conversation_date", "absent"], ) _, chunks = await chunker.chunk(path) assert chunks[0].metadata == {"conversation_date": "2023-01-19"} # Empty allow-list (not None) keeps the legacy "all non-empty keys" behavior. chunker = MarkdownFileChunker( chunk_byte_size=500, include_frontmatter_in_metadata=True, include_frontmatter_keys_in_metadata=[], ) _, chunks = await chunker.chunk(path) assert chunks[0].metadata == { "name": "locomo-event", "description": "Jon lost his job", "conversation_date": "2023-01-19", } # Allow-list is ignored when the master toggle is off (back-compat default). chunker = MarkdownFileChunker( chunk_byte_size=500, include_frontmatter_in_metadata=False, include_frontmatter_keys_in_metadata=["conversation_date"], ) _, chunks = await chunker.chunk(path) assert chunks[0].metadata == {} print("✓ test_parse_frontmatter_metadata_keys_allowlist passed") asyncio.run(run()) def test_parse_small_body_one_chunk(): """A body shorter than chunk_byte_size produces exactly one chunk that contains the body.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): body = "# Hello\n\nthis is a small body." path = _write_md(tmp, "small.md", body) chunker = MarkdownFileChunker(chunk_byte_size=500) node, chunks = await chunker.chunk(path) assert len(chunks) == 1 assert "this is a small body" in chunks[0].text assert node.chunk_ids == [chunks[0].id] print("✓ test_parse_small_body_one_chunk passed") asyncio.run(run()) def test_parse_small_children_are_cached_without_recursive_calls(): """An oversized parent greedily caches fitting children without recursing into them.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): sections = "\n\n".join(f"# Section-{i}\n\n{'x' * 30}" for i in range(6)) path = _write_md(tmp, "small-children.md", sections) chunker = MarkdownFileChunker(chunk_byte_size=100, embed_toc=False) with patch.object( chunker, "_chunk_node", wraps=chunker._chunk_node, ) as chunk_node: _, chunks = await chunker.chunk(path) assert chunk_node.call_count == 1 assert 1 < len(chunks) < 6 assert all(len(chunk.text.encode("utf-8")) <= chunker.chunk_byte_size for chunk in chunks) asyncio.run(run()) def test_parse_child_cache_flushes_at_exact_limit(): """A cache reaching ``chunk_byte_size`` is finalized before the next child.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): path = _write_md(tmp, "exact-cache.md", f"# A\n\n{'x' * 95}\n\n# B\n\ny") chunker = MarkdownFileChunker(chunk_byte_size=100) _, chunks = await chunker.chunk(path) assert len(chunks) == 2 assert len(chunks[0].text) == chunker.chunk_byte_size assert chunks[0].text.startswith("# A") assert chunks[1].text == "# B\n\ny" asyncio.run(run()) def test_parse_oversized_child_flushes_parent_cache(): """Recursive child chunks do not merge across the parent cache boundary.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): leaves = "\n\n".join(f"## L{i}\n\n{'x' * 10}" for i in range(6)) body = f"# A\n\na\n\n# Large\n\n{leaves}\n\n# C\n\nc" path = _write_md(tmp, "recursive-boundary.md", body) chunker = MarkdownFileChunker(chunk_byte_size=100, embed_toc=False) _, chunks = await chunker.chunk(path) assert len(chunks) == 4 assert chunks[0].text == "# A\n\na" assert chunks[-2].text == f"## L4\n\n{'x' * 10}\n\n## L5\n\n{'x' * 10}" assert chunks[-1].text == "# C\n\nc" asyncio.run(run()) def test_parse_oversized_body_splits(): """A body exceeding chunk_byte_size triggers multiple chunks.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): paras = "\n\n".join(f"paragraph {i} with some content text here." for i in range(50)) body = "# H\n\n" + paras path = _write_md(tmp, "big.md", body) chunker = MarkdownFileChunker(chunk_byte_size=200) _, chunks = await chunker.chunk(path) assert len(chunks) > 1 assert all(len(chunk.text.encode("utf-8")) <= chunker.chunk_byte_size for chunk in chunks) print("✓ test_parse_oversized_body_splits passed") asyncio.run(run()) def test_parse_chunk_ids_match_node_chunk_ids(): """node.chunk_ids is the ordered list of chunk hashes.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): paras = "\n\n".join(f"para {i} body content here." for i in range(40)) body = "# H\n\n" + paras path = _write_md(tmp, "p.md", body) chunker = MarkdownFileChunker(chunk_byte_size=200) node, chunks = await chunker.chunk(path) assert node.chunk_ids == [c.id for c in chunks] print("✓ test_parse_chunk_ids_match_node_chunk_ids passed") asyncio.run(run()) def test_parse_links_literal_targets(): """Wikilink targets are taken verbatim — full path → FileLink.target_path.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): body = "see [[topics/Alice.md]] and [[topics/Bob.md#sec]]" path = _write_md(tmp, "note.md", body) chunker = MarkdownFileChunker() node, _ = await chunker.chunk(path) pairs = {(link.target_path, link.target_anchor) for link in node.links} assert ("topics/Alice.md", None) in pairs assert ("topics/Bob.md", "sec") in pairs # source_path always equals the node's own path for link in node.links: assert link.source_path == node.path print("✓ test_parse_links_literal_targets passed") asyncio.run(run()) def test_parse_links_short_and_no_ext_kept_literally(): """Short and no-ext forms are NOT resolved — they're stored as-is. The parser does no resolution; whether the target exists is a ``lint:dangling`` concern. ``[[Alice]]`` becomes ``target_path='Alice'`` and will be flagged dangling unless a node with literal path 'Alice' actually exists. """ async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): body = "see [[Alice]] and [[topics/Alice]] but also [[topics/Alice.md]]" path = _write_md(tmp, "note.md", body) chunker = MarkdownFileChunker() node, _ = await chunker.chunk(path) targets = {link.target_path for link in node.links} assert targets == {"Alice", "topics/Alice", "topics/Alice.md"} print("✓ test_parse_links_short_and_no_ext_kept_literally passed") asyncio.run(run()) def test_parse_links_legacy_relation_text_is_ignored(): """Old relation wrappers do not prevent their inner wikilinks from parsing.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): body = "extends:: [[A.md]]\n\nsome [concerns:: [[B.md]]] inline\n" path = _write_md(tmp, "note.md", body) chunker = MarkdownFileChunker() node, _ = await chunker.chunk(path) assert {link.target_path for link in node.links} == {"A.md", "B.md"} print("✓ test_parse_links_legacy_relation_text_is_ignored passed") asyncio.run(run()) def test_parse_links_deduped(): """Repeated wikilinks with the same target and anchor emit one FileLink.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): body = "[[A.md]] again [[A.md]] and [[A.md]]" path = _write_md(tmp, "note.md", body) chunker = MarkdownFileChunker() node, _ = await chunker.chunk(path) assert len([link for link in node.links if link.target_path == "A.md"]) == 1 print("✓ test_parse_links_deduped passed") asyncio.run(run()) def test_parse_min_chunk_byte_size_clamped(): """chunk_byte_size below 100 should be clamped to 100.""" chunker = MarkdownFileChunker(chunk_byte_size=10) assert chunker.chunk_byte_size == 100 print("✓ test_parse_min_chunk_byte_size_clamped passed") def test_parse_embed_toc_prefixes_chunk_text(): """When embed_toc=True, chunks emitted inside a section are prefixed by the heading.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): body = "# Top\n\n## Sub\n\nbody-content" path = _write_md(tmp, "toc.md", body) chunker = MarkdownFileChunker(chunk_byte_size=200, embed_toc=True) _, chunks = await chunker.chunk(path) # Single small section fits; check that the heading appears in text. assert any("Top" in c.text for c in chunks) print("✓ test_parse_embed_toc_prefixes_chunk_text passed") asyncio.run(run()) def test_parse_embed_toc_is_enabled_by_default(): """The default adds bounded ancestor breadcrumbs without a full outline.""" chunker = MarkdownFileChunker() assert chunker.embed_toc is True assert chunker.max_ast_sections == 100 def test_count_sections_ignores_fenced_headings_and_supports_setext(): """The AST preflight counts real headings without parsing fenced examples.""" content = "# Real\n\n```markdown\n# Fake\nFake too\n---\n```\n\nSetext\n===\n" assert MarkdownFileChunker._count_sections(content) == 2 assert MarkdownFileChunker._count_sections(content, stop_after=1) == 2 def test_parse_excessive_sections_uses_plain_text_without_ast(): """Inherited fallback is standalone, bounded, and bypasses mistletoe.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): sections = "\n\n".join(f"# Section-{i}\n\n{'x' * 480} [[target.md]]" for i in range(101)) body = f"---\nname: fallback\n---\n{sections}" path = _write_md(tmp, "fallback.md", body) chunker = MarkdownFileChunker( chunk_byte_size=10000, embed_toc=True, max_ast_sections=100, include_frontmatter_in_metadata=True, ) await chunker.start() try: with ( patch( "mistletoe.block_token.Document", side_effect=AssertionError("fallback must not construct an AST"), ), patch.object( chunker, "chunk_content", wraps=chunker.chunk_content, ) as chunk_content, ): node, chunks = await chunker.chunk(path) assert chunk_content.call_count == 1 finally: await chunker.close() assert isinstance(chunker, DefaultFileChunker) assert len(chunks) > 1 assert max(len(chunk.text.encode("utf-8")) for chunk in chunks) <= chunker.chunk_byte_size assert chunks[0].start_line == 4 assert node.front_matter.name == "fallback" assert node.chunk_ids == [chunk.id for chunk in chunks] assert {(link.target_path, link.source_path) for link in node.links} == { ("target.md", "fallback.md"), } assert all(chunk.metadata == {"name": "fallback"} for chunk in chunks) for i in (0, 100): assert any(f"# Section-{i}" in chunk.text for chunk in chunks) asyncio.run(run()) def test_parse_section_limit_is_inclusive_for_ast(): """A document at the configured section limit still takes the AST path.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): path = _write_md(tmp, "at-limit.md", "# A\n\na\n\n# B\n\nb") chunker = MarkdownFileChunker(max_ast_sections=2) with patch.object(chunker, "_build_tree", wraps=chunker._build_tree) as build_tree: _, chunks = await chunker.chunk(path) assert build_tree.call_count == 1 assert len(chunks) == 1 asyncio.run(run()) def test_parse_small_sections_are_merged_and_headings_preserved(): """Adjacent small sections share chunks without losing their headings.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): sections = "\n\n".join(f"## Section-{i:03d}\n\nfact-{i:03d}" for i in range(40)) path = _write_md(tmp, "sections.md", f"# Root\n\n{sections}") chunker = MarkdownFileChunker(chunk_byte_size=200, embed_toc=False) _, chunks = await chunker.chunk(path) assert 1 < len(chunks) < 40 for i in range(40): heading = f"## Section-{i:03d}" assert sum(chunk.text.count(heading) for chunk in chunks) == 1 asyncio.run(run()) def test_parse_embed_toc_uses_breadcrumbs_without_sibling_duplication(): """TOC context repeats ancestors, not parallel section headings.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): sections = "\n\n".join(f"## Parallel-{i:03d}\n\nfact-{i:03d}" for i in range(40)) path = _write_md(tmp, "breadcrumbs.md", f"# Root\n\n{sections}") chunker = MarkdownFileChunker(chunk_byte_size=200, embed_toc=True) _, chunks = await chunker.chunk(path) assert len(chunks) > 1 assert all("# Root" in chunk.text for chunk in chunks) for i in range(40): heading = f"## Parallel-{i:03d}" assert sum(chunk.text.count(heading) for chunk in chunks) == 1 asyncio.run(run()) def test_parse_heading_heavy_output_grows_linearly(): """A thousand parallel sections remain a small, linear number of chunks.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): sections = "\n\n".join(f"## Observation-{i:04d}\n\nsynthetic fact" for i in range(1000)) body = f"# Root\n\n{sections}" path = _write_md(tmp, "linear.md", body) chunker = MarkdownFileChunker(chunk_byte_size=10000, embed_toc=True, max_ast_sections=None) _, chunks = await chunker.chunk(path) assert len(chunks) < 10 assert sum(len(chunk.text) for chunk in chunks) < 2 * len(body) asyncio.run(run()) def test_parse_nested_sections_merge_within_recursive_context(): """Nested continuations retain their branch breadcrumb after recursive merging.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): branches = [] for branch, fact in (("A", "a"), ("B", "b")): leaves = "\n\n".join(f"### {branch}{i}\n\n{fact * 45}" for i in range(4)) branches.append(f"## {branch}\n\n{leaves}") branch_text = "\n\n".join(branches) path = _write_md(tmp, "nested.md", f"# Root\n\n{branch_text}") chunker = MarkdownFileChunker(chunk_byte_size=150, embed_toc=True) _, chunks = await chunker.chunk(path) assert len(chunks) == 5 assert all(len(chunk.text.encode("utf-8")) <= chunker.chunk_byte_size for chunk in chunks) assert chunks[0].text == "# Root" assert chunks[2].text.startswith("# Root\n\n## A\n\n### A2") assert chunks[3].text.startswith("# Root\n\n## B\n\n### B0") assert "## B" not in chunks[2].text asyncio.run(run()) def test_parse_breadcrumbs_and_part_markers_share_byte_budget(): """Breadcrumbs and part labels cannot push multi-byte chunks over budget.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): content_lines = "\n".join(f"内容-{i}-" * 6 for i in range(4)) body = f"# {'根' * 15}\n\n## {'枝' * 15}\n\n### {'叶' * 15}\n\n{content_lines}" path = _write_md(tmp, "breadcrumb-budget.md", body) chunker = MarkdownFileChunker( chunk_byte_size=100, embed_toc=True, max_ast_sections=None, ) _, chunks = await chunker.chunk(path) assert len(chunks) > 1 assert any(chunk.text.startswith("[Part ") for chunk in chunks) assert all(len(chunk.text.encode("utf-8")) <= chunker.chunk_byte_size for chunk in chunks) asyncio.run(run()) def test_parse_frontmatter_preserves_original_line_numbers(): """Chunk line ranges are 1-based and refer to the original file, including frontmatter.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): body = "---\nname: t\n---\n# H\nline 1\nline 2\n" path = _write_md(tmp, "front-lines.md", body) chunker = MarkdownFileChunker(chunk_byte_size=500) _, chunks = await chunker.chunk(path) assert len(chunks) == 1 assert chunks[0].start_line == 4 assert chunks[0].end_line == 6 print("✓ test_parse_frontmatter_preserves_original_line_numbers passed") asyncio.run(run()) def test_parse_frontmatter_offsets_split_table_rows(): """Split table row ranges include the YAML frontmatter line offset.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): rows = "".join(f"| {i} | {i} |\n" for i in range(12)) body = "---\nname: t\n---\n| A | B |\n|---|---|\n" + rows path = _write_md(tmp, "front-table.md", body) chunker = MarkdownFileChunker(chunk_byte_size=100) _, chunks = await chunker.chunk(path) assert len(chunks) > 1 assert chunks[0].start_line == 6 assert chunks[0].end_line >= chunks[0].start_line print("✓ test_parse_frontmatter_offsets_split_table_rows passed") asyncio.run(run()) def test_parse_bad_frontmatter_does_not_abort_chunking(): """Invalid YAML frontmatter is ignored while the markdown body still chunks.""" async def run(): with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): body = "---\nname: [\n---\n# H\nbody\n" path = _write_md(tmp, "bad-frontmatter.md", body) chunker = MarkdownFileChunker(chunk_byte_size=500) node, chunks = await chunker.chunk(path) assert node.front_matter.name == "" assert len(chunks) == 1 assert chunks[0].start_line == 4 assert "body" in chunks[0].text print("✓ test_parse_bad_frontmatter_does_not_abort_chunking passed") asyncio.run(run()) if __name__ == "__main__": print("\n=== MarkdownFileChunker tests ===") test_parse_empty_file() test_parse_frontmatter_only() test_parse_frontmatter_metadata_is_opt_in() test_parse_small_body_one_chunk() test_parse_small_children_are_cached_without_recursive_calls() test_parse_child_cache_flushes_at_exact_limit() test_parse_oversized_child_flushes_parent_cache() test_parse_oversized_body_splits() test_parse_chunk_ids_match_node_chunk_ids() test_parse_links_literal_targets() test_parse_links_short_and_no_ext_kept_literally() test_parse_links_legacy_relation_text_is_ignored() test_parse_links_deduped() test_parse_min_chunk_byte_size_clamped() test_parse_embed_toc_prefixes_chunk_text() test_parse_embed_toc_is_enabled_by_default() test_count_sections_ignores_fenced_headings_and_supports_setext() test_parse_excessive_sections_uses_plain_text_without_ast() test_parse_section_limit_is_inclusive_for_ast() test_parse_small_sections_are_merged_and_headings_preserved() test_parse_embed_toc_uses_breadcrumbs_without_sibling_duplication() test_parse_heading_heavy_output_grows_linearly() test_parse_nested_sections_merge_within_recursive_context() test_parse_breadcrumbs_and_part_markers_share_byte_budget() test_parse_frontmatter_preserves_original_line_numbers() test_parse_frontmatter_offsets_split_table_rows() test_parse_bad_frontmatter_does_not_abort_chunking() print("\n所有测试通过!")