| from importlib.util import module_from_spec, spec_from_file_location |
| from pathlib import Path |
| import sys |
| from tempfile import TemporaryDirectory |
| import unittest |
|
|
|
|
| def load_sync_module(): |
| script_path = ( |
| Path(__file__).resolve().parents[1] / "scripts" / "sync_docs_to_wiki.py" |
| ) |
| spec = spec_from_file_location("sync_docs_to_wiki", script_path) |
| if spec is None or spec.loader is None: |
| raise ImportError(f"Unable to load module from {script_path}") |
| module = module_from_spec(spec) |
| sys.modules[spec.name] = module |
| spec.loader.exec_module(module) |
| return module |
|
|
|
|
| class SyncDocsHelpersTest(unittest.TestCase): |
| def test_page_name_for_nested_markdown_source(self): |
| module = load_sync_module() |
|
|
| self.assertEqual( |
| module.page_name_for_source("zh/deploy/astrbot/docker.md"), |
| "zh-deploy-astrbot-docker", |
| ) |
|
|
| def test_strip_frontmatter_removes_leading_block(self): |
| module = load_sync_module() |
|
|
| source = "---\nlayout: home\n---\n\n# Title\n" |
|
|
| self.assertEqual(module.strip_frontmatter(source), "# Title\n") |
|
|
| def test_module_does_not_expose_removed_wrapper_helpers(self): |
| module = load_sync_module() |
|
|
| self.assertFalse(hasattr(module, "get_link_resolver")) |
| self.assertFalse(hasattr(module, "resolve_source_path")) |
| self.assertFalse(hasattr(module, "compute_managed_files")) |
| self.assertFalse(hasattr(module, "MANAGED_FILENAMES")) |
| self.assertFalse(hasattr(module, "find_candidates_by_suffix")) |
|
|
| def test_module_exposes_consolidated_helper_names(self): |
| module = load_sync_module() |
|
|
| self.assertTrue(hasattr(module, "prepare_candidate_path")) |
| self.assertTrue(hasattr(module, "resolve_link_path")) |
| self.assertTrue(hasattr(module, "LANG_CONFIG")) |
| self.assertTrue(hasattr(module, "Segment")) |
| self.assertTrue(hasattr(module, "iter_segments")) |
|
|
| def test_parse_doc_target_returns_base_and_anchor(self): |
| module = load_sync_module() |
|
|
| self.assertEqual( |
| module.parse_doc_target("/deploy/guide#intro"), |
| ("/deploy/guide", "#intro"), |
| ) |
| self.assertIsNone(module.parse_doc_target("https://example.com/guide")) |
| self.assertIsNone(module.parse_doc_target("../images/diagram.png")) |
| self.assertIsNone(module.parse_doc_target("#intro")) |
|
|
| def test_iter_markdown_links_handles_whitespace_before_target(self): |
| module = load_sync_module() |
|
|
| links = list(module.iter_markdown_links("See [Guide]\n(guide.md).\n")) |
|
|
| self.assertEqual([link.target for link in links], ["guide.md"]) |
|
|
| def test_iter_segments_splits_text_inline_and_fenced_code(self): |
| module = load_sync_module() |
|
|
| segments = list( |
| module.iter_segments( |
| "Start [Guide](/guide) `code [Guide](/guide)`\n\n```md\n[Guide](/guide)\n```\nTail\n" |
| ) |
| ) |
|
|
| self.assertEqual( |
| [(segment.kind, segment.text) for segment in segments], |
| [ |
| ("text", "Start [Guide](/guide) "), |
| ("inline_code", "`code [Guide](/guide)`"), |
| ("text", "\n\n"), |
| ("code_block", "```md\n[Guide](/guide)\n```"), |
| ("text", "\nTail\n"), |
| ], |
| ) |
|
|
| def test_rewrite_links_handles_absolute_same_language_links(self): |
| module = load_sync_module() |
|
|
| resolver = module.LinkResolver(Path(__file__).resolve().parents[1]) |
|
|
| content = "See [Docker](/deploy/astrbot/docker).\n" |
|
|
| self.assertEqual( |
| module.rewrite_links( |
| content, |
| source_path="zh/what-is-astrbot.md", |
| resolver=resolver, |
| ), |
| "See [Docker](zh-deploy-astrbot-docker).\n", |
| ) |
|
|
| def test_rewrite_links_handles_relative_links(self): |
| module = load_sync_module() |
|
|
| resolver = module.LinkResolver(Path(__file__).resolve().parents[1]) |
|
|
| content = "Use [Dify](../agent-runners/dify.md).\n" |
|
|
| self.assertEqual( |
| module.rewrite_links( |
| content, |
| source_path="zh/providers/dify.md", |
| resolver=resolver, |
| ), |
| "Use [Dify](zh-providers-agent-runners-dify).\n", |
| ) |
|
|
| def test_rewrite_links_handles_rewritten_root_paths(self): |
| module = load_sync_module() |
|
|
| resolver = module.LinkResolver(Path(__file__).resolve().parents[1]) |
|
|
| content = "See [Connecting Model Services](/config/providers/start).\n" |
|
|
| self.assertEqual( |
| module.rewrite_links( |
| content, |
| source_path="zh/what-is-astrbot.md", |
| resolver=resolver, |
| ), |
| "See [Connecting Model Services](zh-providers-start).\n", |
| ) |
|
|
| def test_rewrite_links_handles_internal_links_with_parentheses(self): |
| module = load_sync_module() |
|
|
| with TemporaryDirectory() as temp_dir: |
| source_root = Path(temp_dir) / "docs" |
| (source_root / "zh").mkdir(parents=True) |
| (source_root / "zh" / "index.md").write_text( |
| "See [Guide](/guide(test)).\n", |
| encoding="utf-8", |
| ) |
| (source_root / "zh" / "guide(test).md").write_text( |
| "# Guide\n", |
| encoding="utf-8", |
| ) |
| resolver = module.LinkResolver(source_root) |
|
|
| self.assertEqual( |
| module.rewrite_links( |
| "See [Guide](/guide(test)).\n", |
| source_path="zh/index.md", |
| resolver=resolver, |
| ), |
| "See [Guide](zh-guide(test)).\n", |
| ) |
|
|
| def test_rewrite_links_leaves_local_asset_links_unchanged(self): |
| module = load_sync_module() |
|
|
| with TemporaryDirectory() as temp_dir: |
| source_root = Path(temp_dir) / "docs" |
| (source_root / "zh" / "use").mkdir(parents=True) |
| (source_root / "zh" / "images").mkdir(parents=True) |
| (source_root / "zh" / "use" / "guide.md").write_text( |
| "# Guide\n", encoding="utf-8" |
| ) |
| (source_root / "zh" / "images" / "diagram.png").write_bytes(b"png") |
| resolver = module.LinkResolver(source_root) |
|
|
| content = "\n" |
|
|
| self.assertEqual( |
| module.rewrite_links( |
| content, |
| source_path="zh/use/guide.md", |
| resolver=resolver, |
| ), |
| content, |
| ) |
|
|
| def test_rewrite_links_skips_fenced_code_blocks(self): |
| module = load_sync_module() |
|
|
| with TemporaryDirectory() as temp_dir: |
| source_root = Path(temp_dir) / "docs" |
| (source_root / "zh").mkdir(parents=True) |
| (source_root / "zh" / "index.md").write_text("# Home\n", encoding="utf-8") |
| (source_root / "zh" / "guide.md").write_text("# Guide\n", encoding="utf-8") |
| resolver = module.LinkResolver(source_root) |
|
|
| content = "```md\n[Guide](/guide)\n```\n\nSee [Guide](/guide).\n" |
|
|
| self.assertEqual( |
| module.rewrite_links( |
| content, |
| source_path="zh/index.md", |
| resolver=resolver, |
| ), |
| "```md\n[Guide](/guide)\n```\n\nSee [Guide](zh-guide).\n", |
| ) |
|
|
| def test_rewrite_links_skips_inline_code(self): |
| module = load_sync_module() |
|
|
| with TemporaryDirectory() as temp_dir: |
| source_root = Path(temp_dir) / "docs" |
| (source_root / "zh").mkdir(parents=True) |
| (source_root / "zh" / "index.md").write_text("# Home\n", encoding="utf-8") |
| (source_root / "zh" / "guide.md").write_text("# Guide\n", encoding="utf-8") |
| resolver = module.LinkResolver(source_root) |
|
|
| content = "Use `[Guide](/guide)` literally, then See [Guide](/guide).\n" |
|
|
| self.assertEqual( |
| module.rewrite_links( |
| content, |
| source_path="zh/index.md", |
| resolver=resolver, |
| ), |
| "Use `[Guide](/guide)` literally, then See [Guide](zh-guide).\n", |
| ) |
|
|
| def test_link_resolver_resolves_source_paths(self): |
| module = load_sync_module() |
|
|
| with TemporaryDirectory() as temp_dir: |
| source_root = Path(temp_dir) / "docs" |
| (source_root / "zh" / "deploy").mkdir(parents=True) |
| (source_root / "zh" / "index.md").write_text("# Home\n", encoding="utf-8") |
| (source_root / "zh" / "deploy" / "guide.md").write_text( |
| "# Guide\n", encoding="utf-8" |
| ) |
|
|
| resolver = module.LinkResolver(source_root) |
|
|
| self.assertEqual( |
| resolver.resolve_markdown_target("/deploy/guide#intro", "zh/index.md"), |
| ("zh/deploy/guide.md", "#intro"), |
| ) |
|
|
| def test_resolve_link_path_resolves_relative_target(self): |
| module = load_sync_module() |
|
|
| with TemporaryDirectory() as temp_dir: |
| source_root = Path(temp_dir) / "docs" |
| (source_root / "zh" / "providers").mkdir(parents=True) |
| (source_root / "zh" / "agent-runners").mkdir(parents=True) |
| (source_root / "zh" / "providers" / "dify.md").write_text( |
| "# Dify\n", |
| encoding="utf-8", |
| ) |
| (source_root / "zh" / "agent-runners" / "dify.md").write_text( |
| "# Agent Runner\n", |
| encoding="utf-8", |
| ) |
|
|
| self.assertEqual( |
| module.resolve_link_path( |
| base_target="../agent-runners/dify.md", |
| source_path="zh/providers/dify.md", |
| source_root=source_root, |
| source_pages=module.discover_source_pages(str(source_root)), |
| ).resolved_path, |
| "zh/agent-runners/dify.md", |
| ) |
|
|
| def test_build_home_page_uses_language_config(self): |
| module = load_sync_module() |
|
|
| self.assertIn( |
| module.LANG_CONFIG["zh"]["home_intro"], module.build_home_page("zh") |
| ) |
| self.assertIn( |
| module.LANG_CONFIG["en"]["home_intro"], module.build_home_page("en") |
| ) |
|
|
| def test_prepare_candidate_path_normalizes_suffix_and_alias(self): |
| module = load_sync_module() |
|
|
| self.assertEqual( |
| module.prepare_candidate_path( |
| module.PurePosixPath("zh/config/providers/../providers/start") |
| ), |
| module.PurePosixPath("zh/providers/start.md"), |
| ) |
|
|
| def test_find_existing_source_path_matches_language_bounded_suffixes(self): |
| module = load_sync_module() |
|
|
| self.assertEqual( |
| module.find_existing_source_path( |
| candidate=module.PurePosixPath("zh/bar/guide.md"), |
| source_root=Path("/tmp/nonexistent"), |
| source_pages=( |
| "zh/bar/guide.md", |
| "zh/foo/bar/guide.md", |
| "zh/foobar/guide.md", |
| "en/bar/guide.md", |
| ), |
| ).ambiguous_matches, |
| ("zh/bar/guide.md", "zh/foo/bar/guide.md"), |
| ) |
|
|
| def test_build_page_info_returns_page_info_dataclass(self): |
| module = load_sync_module() |
|
|
| with TemporaryDirectory() as temp_dir: |
| source_root = Path(temp_dir) / "docs" |
| (source_root / "zh").mkdir(parents=True) |
| (source_root / "zh" / "index.md").write_text( |
| "# 中文首页\n", encoding="utf-8" |
| ) |
|
|
| resolver = module.LinkResolver(source_root) |
| page_info = module.build_page_info( |
| source_root=source_root, |
| source_path="zh/index.md", |
| resolver=resolver, |
| ) |
|
|
| self.assertIsInstance(page_info, module.PageInfo) |
| self.assertEqual(page_info.page_name, "zh-index") |
|
|
| def test_build_page_info_uses_display_ready_group(self): |
| module = load_sync_module() |
|
|
| with TemporaryDirectory() as temp_dir: |
| source_root = Path(temp_dir) / "docs" |
| (source_root / "zh" / "agent-runners").mkdir(parents=True) |
| (source_root / "zh" / "agent-runners" / "guide.md").write_text( |
| "# Guide\n", |
| encoding="utf-8", |
| ) |
|
|
| resolver = module.LinkResolver(source_root) |
| page_info = module.build_page_info( |
| source_root=source_root, |
| source_path="zh/agent-runners/guide.md", |
| resolver=resolver, |
| ) |
|
|
| self.assertEqual(page_info.group, "agent runners") |
|
|
| def test_sync_writes_pages_and_sidebar(self): |
| module = load_sync_module() |
|
|
| with TemporaryDirectory() as temp_dir: |
| source_root = Path(temp_dir) / "docs" |
| wiki_root = Path(temp_dir) / "wiki" |
| (source_root / "zh").mkdir(parents=True) |
| (source_root / "en").mkdir(parents=True) |
|
|
| (source_root / "zh" / "index.md").write_text( |
| "---\nlayout: home\n---\n\n# 中文首页\n\nSee [Guide](/deploy/guide).\n", |
| encoding="utf-8", |
| ) |
| (source_root / "zh" / "deploy").mkdir(parents=True) |
| (source_root / "zh" / "deploy" / "guide.md").write_text( |
| "# 部署指南\n", |
| encoding="utf-8", |
| ) |
| (source_root / "en" / "index.md").write_text( |
| "# English Home\n\nSee [Guide](/en/deploy/guide).\n", |
| encoding="utf-8", |
| ) |
| (source_root / "en" / "deploy").mkdir(parents=True) |
| (source_root / "en" / "deploy" / "guide.md").write_text( |
| "# Deployment Guide\n", |
| encoding="utf-8", |
| ) |
|
|
| module.sync_docs_to_wiki(source_root=source_root, wiki_root=wiki_root) |
|
|
| self.assertTrue((wiki_root / "Home.md").exists()) |
| self.assertTrue((wiki_root / "Home-en.md").exists()) |
| self.assertTrue((wiki_root / "_Sidebar.md").exists()) |
| self.assertTrue((wiki_root / "zh-index.md").exists()) |
| self.assertTrue((wiki_root / "en-index.md").exists()) |
| self.assertIn( |
| "[Guide](zh-deploy-guide)", |
| (wiki_root / "zh-index.md").read_text(encoding="utf-8"), |
| ) |
|
|
| def test_sync_preserves_unknown_wiki_pages(self): |
| module = load_sync_module() |
|
|
| with TemporaryDirectory() as temp_dir: |
| source_root = Path(temp_dir) / "docs" |
| wiki_root = Path(temp_dir) / "wiki" |
| (source_root / "zh").mkdir(parents=True) |
| (source_root / "en").mkdir(parents=True) |
|
|
| (source_root / "zh" / "index.md").write_text( |
| "# 中文首页\n", encoding="utf-8" |
| ) |
| (source_root / "en" / "index.md").write_text( |
| "# English Home\n", encoding="utf-8" |
| ) |
|
|
| wiki_root.mkdir(parents=True) |
| handwritten = wiki_root / "zh-handwritten.md" |
| handwritten.write_text("# Keep me\n", encoding="utf-8") |
|
|
| module.sync_docs_to_wiki(source_root=source_root, wiki_root=wiki_root) |
|
|
| self.assertTrue(handwritten.exists()) |
|
|
| def test_find_unresolved_doc_links_reports_ambiguous_matches(self): |
| module = load_sync_module() |
|
|
| with TemporaryDirectory() as temp_dir: |
| source_root = Path(temp_dir) / "docs" |
| (source_root / "zh" / "foo").mkdir(parents=True) |
| (source_root / "zh" / "bar").mkdir(parents=True) |
| (source_root / "zh" / "index.md").write_text( |
| "See [Guide](/guide).\n", |
| encoding="utf-8", |
| ) |
| (source_root / "zh" / "foo" / "guide.md").write_text( |
| "# Foo\n", encoding="utf-8" |
| ) |
| (source_root / "zh" / "bar" / "guide.md").write_text( |
| "# Bar\n", encoding="utf-8" |
| ) |
|
|
| unresolved = module.find_unresolved_doc_links(source_root) |
|
|
| self.assertEqual( |
| unresolved, |
| [ |
| "zh/index.md -> /guide (ambiguous: zh/bar/guide.md, zh/foo/guide.md)", |
| ], |
| ) |
|
|
| def test_resolver_does_not_match_partial_path_segments(self): |
| module = load_sync_module() |
|
|
| with TemporaryDirectory() as temp_dir: |
| source_root = Path(temp_dir) / "docs" |
| (source_root / "zh" / "foobar").mkdir(parents=True) |
| (source_root / "zh" / "index.md").write_text( |
| "See [Guide](/bar/guide).\n", |
| encoding="utf-8", |
| ) |
| (source_root / "zh" / "foobar" / "guide.md").write_text( |
| "# Guide\n", |
| encoding="utf-8", |
| ) |
|
|
| resolver = module.LinkResolver(source_root) |
|
|
| self.assertEqual( |
| resolver.resolve_markdown_target("/bar/guide", "zh/index.md"), |
| (None, ""), |
| ) |
|
|
| def test_live_docs_have_no_unresolved_internal_doc_links(self): |
| module = load_sync_module() |
|
|
| unresolved = module.find_unresolved_doc_links( |
| source_root=Path(__file__).resolve().parents[1], |
| ) |
|
|
| self.assertEqual(unresolved, []) |
|
|
| def test_check_unresolved_doc_links_raises_for_bad_docs(self): |
| module = load_sync_module() |
|
|
| with TemporaryDirectory() as temp_dir: |
| source_root = Path(temp_dir) / "docs" |
| (source_root / "zh").mkdir(parents=True) |
| (source_root / "zh" / "index.md").write_text( |
| "See [Missing](/missing).\n", |
| encoding="utf-8", |
| ) |
|
|
| with self.assertRaises(ValueError): |
| module.check_unresolved_doc_links(source_root) |
|
|
|
|
| if __name__ == "__main__": |
| unittest.main() |
|
|