fc33b3eb68
* 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>
492 lines
18 KiB
Python
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 = "\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()
|