Files
AstrBot/docs/tests/test_sync_docs_to_wiki.py
Soulter fc33b3eb68 docs: transfer AstrBotDevs/AstrBot-docs to AstrBotDevs/AstrBot (#5960)
* docs: transfer AstrBotDevs/AstrBot-docs to AstrBotDevs/AstrBot
* refactor: reorder imports and improve type hints in sync_docs_to_wiki.py and upload_doc_images_to_r2.py
* feat: add GitHub Actions workflow to sync wiki with documentation

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
Co-authored-by: anka-afk <110004162+anka-afk@users.noreply.github.com>
Co-authored-by: zouyonghe <62183434+zouyonghe@users.noreply.github.com>
Co-authored-by: shuiping233 <49360196+shuiping233@users.noreply.github.com>
Co-authored-by: LIghtJUNction <106986785+LIghtJUNction@users.noreply.github.com>
Co-authored-by: Sjshi763 <179909421+Sjshi763@users.noreply.github.com>
Co-authored-by: xiewoc <70128845+xiewoc@users.noreply.github.com>
Co-authored-by: QingFeng-awa <151742581+QingFeng-awa@users.noreply.github.com>
Co-authored-by: PaloMiku <96452465+PaloMiku@users.noreply.github.com>
Co-authored-by: shangxueink <138397030+shangxueink@users.noreply.github.com>
Co-authored-by: IGCrystal-A <244300990+IGCrystal-A@users.noreply.github.com>
Co-authored-by: RC-CHN <67079377+RC-CHN@users.noreply.github.com>
Co-authored-by: MC090610 <113341105+MC090610@users.noreply.github.com>
Co-authored-by: Waterwzy <196913419+Waterwzy@users.noreply.github.com>
Co-authored-by: Lanhuace-Wan <186303160+Lanhuace-Wan@users.noreply.github.com>
Co-authored-by: LiAlH4qwq <61769640+LiAlH4qwq@users.noreply.github.com>
Co-authored-by: HSOS6 <209910899+HSOS6@users.noreply.github.com>
Co-authored-by: th-dd <162813557+th-dd@users.noreply.github.com>
Co-authored-by: miaoxutao123 <81676466+miaoxutao123@users.noreply.github.com>
Co-authored-by: nuomicici <143102889+nuomicici@users.noreply.github.com>
Co-authored-by: nasyt233 <210103278+nasyt233@users.noreply.github.com>
Co-authored-by: jlugjb <7426462+jlugjb@users.noreply.github.com>
Co-authored-by: Raven95676 <176760093+Raven95676@users.noreply.github.com>
Co-authored-by: Futureppo <180109455+Futureppo@users.noreply.github.com>
Co-authored-by: MliKiowa <61873808+MliKiowa@users.noreply.github.com>
Co-authored-by: Fridemn <150212937+Fridemn@users.noreply.github.com>
Co-authored-by: BakaCookie520 <138355736+BakaCookie520@users.noreply.github.com>
Co-authored-by: YumeYuka <125112916+YumeYuka@users.noreply.github.com>
Co-authored-by: xming521 <32786500+xming521@users.noreply.github.com>
Co-authored-by: ywh555hhh <121592812+ywh555hhh@users.noreply.github.com>
Co-authored-by: stevessr <89645372+stevessr@users.noreply.github.com>
Co-authored-by: roeseth <41995115+roeseth@users.noreply.github.com>
Co-authored-by: ikun-1145141 <265925499+ikun-1145141@users.noreply.github.com>
Co-authored-by: evpeople <54983536+evpeople@users.noreply.github.com>
Co-authored-by: Yue-bin <60509781+Yue-bin@users.noreply.github.com>
Co-authored-by: W1ndys <109416673+W1ndys@users.noreply.github.com>
Co-authored-by: TheFurina <218887821+TheFurina@users.noreply.github.com>
Co-authored-by: Seayon <12275933+Seayon@users.noreply.github.com>
Co-authored-by: OnlyblackTea <38585636+OnlyblackTea@users.noreply.github.com>
Co-authored-by: ocetars <74854972+ocetars@users.noreply.github.com>
Co-authored-by: railgun19457 <117180744+railgun19457@users.noreply.github.com>
Co-authored-by: JunieXD <107397009+JunieXD@users.noreply.github.com>
Co-authored-by: advent259141 <197440256+advent259141@users.noreply.github.com>
Co-authored-by: Doge2077 <91442300+Doge2077@users.noreply.github.com>
Co-authored-by: Bocity <23430545+Bocity@users.noreply.github.com>
Co-authored-by: Aurora-xk <192227833+Aurora-xk@users.noreply.github.com>
2026-03-09 23:38:21 +08:00

492 lines
18 KiB
Python

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 = "![Diagram](../images/diagram.png)\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()