diff --git a/tests/unit/build_scripts/test_check_links.py b/tests/unit/build_scripts/test_check_links.py
new file mode 100644
index 000000000..658a6dc4d
--- /dev/null
+++ b/tests/unit/build_scripts/test_check_links.py
@@ -0,0 +1,84 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT license.
+
+import tempfile
+from pathlib import Path
+
+import pytest
+
+from build_scripts.check_links import extract_urls, resolve_relative_url, strip_fragment
+
+
+class TestStripFragment:
+ def test_removes_fragment(self) -> None:
+ assert strip_fragment("https://example.com/page#section") == "https://example.com/page"
+
+ def test_no_fragment_unchanged(self) -> None:
+ assert strip_fragment("https://example.com/page") == "https://example.com/page"
+
+ def test_empty_fragment(self) -> None:
+ assert strip_fragment("https://example.com/page#") == "https://example.com/page"
+
+ def test_preserves_query_string(self) -> None:
+ result = strip_fragment("https://example.com/page?q=1#section")
+ assert "q=1" in result
+ assert "section" not in result
+
+
+class TestResolveRelativeUrl:
+ def test_http_url_unchanged(self) -> None:
+ url = "https://example.com"
+ assert resolve_relative_url("/some/file.md", url) == url
+
+ def test_mailto_unchanged(self) -> None:
+ url = "mailto:test@example.com"
+ assert resolve_relative_url("/some/file.md", url) == url
+
+ def test_relative_url_resolved(self, tmp_path: Path) -> None:
+ base = str(tmp_path / "docs" / "file.md")
+ target = str(tmp_path / "docs" / "other.md")
+ Path(target).parent.mkdir(parents=True, exist_ok=True)
+ Path(target).write_text("# Other")
+ result = resolve_relative_url(base, "other.md")
+ assert "other" in result
+
+ def test_relative_url_with_md_extension(self, tmp_path: Path) -> None:
+ base = str(tmp_path / "docs" / "file.md")
+ target = tmp_path / "docs" / "other.md"
+ target.parent.mkdir(parents=True, exist_ok=True)
+ target.write_text("# Other")
+ result = resolve_relative_url(base, "other")
+ assert result.endswith(".md")
+
+
+class TestExtractUrls:
+ def test_extracts_markdown_links(self, tmp_path: Path) -> None:
+ f = tmp_path / "test.md"
+ f.write_text("[Click here](https://example.com)")
+ urls = extract_urls(str(f))
+ assert "https://example.com" in urls
+
+ def test_extracts_href_links(self, tmp_path: Path) -> None:
+ f = tmp_path / "test.html"
+ f.write_text('link')
+ urls = extract_urls(str(f))
+ assert "https://example.com" in urls
+
+ def test_extracts_src_links(self, tmp_path: Path) -> None:
+ f = tmp_path / "test.html"
+ f.write_text('
')
+ urls = extract_urls(str(f))
+ assert "https://example.com/image.png" in urls
+
+ def test_empty_file_returns_no_urls(self, tmp_path: Path) -> None:
+ f = tmp_path / "empty.md"
+ f.write_text("")
+ urls = extract_urls(str(f))
+ assert urls == []
+
+ def test_strips_fragments_from_extracted_urls(self, tmp_path: Path) -> None:
+ f = tmp_path / "test.md"
+ f.write_text("[link](https://example.com/page#section)")
+ urls = extract_urls(str(f))
+ assert "https://example.com/page" in urls
+ assert not any("#section" in u for u in urls)
diff --git a/tests/unit/build_scripts/test_generate_rss.py b/tests/unit/build_scripts/test_generate_rss.py
new file mode 100644
index 000000000..88f9d63e1
--- /dev/null
+++ b/tests/unit/build_scripts/test_generate_rss.py
@@ -0,0 +1,80 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT license.
+
+import ast
+from collections.abc import Callable
+from pathlib import Path
+from typing import Any
+
+
+def _load_generate_rss_functions() -> tuple[Callable[[str], str], Callable[[Path], tuple[str, str]]]:
+ """Load generate_rss helpers without executing the script body."""
+ script_path = Path(__file__).resolve().parents[3] / "build_scripts" / "generate_rss.py"
+ source = script_path.read_text(encoding="utf-8")
+ parsed_module = ast.parse(source, filename=str(script_path))
+ target_functions = {"extract_date_from_filename", "parse_blog_markdown"}
+ selected_nodes: list[ast.stmt] = []
+ for node in parsed_module.body:
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
+ selected_nodes.append(node)
+ continue
+ if isinstance(node, ast.FunctionDef) and node.name in target_functions:
+ selected_nodes.append(node)
+ safe_module = ast.Module(body=selected_nodes, type_ignores=[])
+ namespace: dict[str, Any] = {}
+ exec(compile(safe_module, filename=str(script_path), mode="exec"), namespace)
+ return namespace["extract_date_from_filename"], namespace["parse_blog_markdown"]
+
+
+extract_date_from_filename, parse_blog_markdown = _load_generate_rss_functions()
+
+
+class TestExtractDateFromFilename:
+ def test_standard_date(self) -> None:
+ assert extract_date_from_filename("2024_12_3.md") == "2024-12-03"
+
+ def test_double_digit_day_and_month(self) -> None:
+ assert extract_date_from_filename("2023_11_25.md") == "2023-11-25"
+
+ def test_single_digit_month(self) -> None:
+ assert extract_date_from_filename("2024_1_15.md") == "2024-01-15"
+
+ def test_returns_empty_for_invalid_filename(self) -> None:
+ assert extract_date_from_filename("no_date_here.md") == ""
+
+ def test_returns_empty_for_non_numeric(self) -> None:
+ assert extract_date_from_filename("intro.md") == ""
+
+
+class TestParseBlogMarkdown:
+ def test_extracts_title(self, tmp_path: Path) -> None:
+ f = tmp_path / "2024_01_01.md"
+ f.write_text("# My Blog Title\n\nSome description here.")
+ title, _ = parse_blog_markdown(f)
+ assert title == "My Blog Title"
+
+ def test_extracts_description(self, tmp_path: Path) -> None:
+ f = tmp_path / "2024_01_01.md"
+ f.write_text("# Title\n\nThis is the description paragraph.")
+ _, desc = parse_blog_markdown(f)
+ assert "This is the description paragraph." in desc
+
+ def test_skips_small_tag_in_description(self, tmp_path: Path) -> None:
+ f = tmp_path / "2024_01_01.md"
+ f.write_text("# Title\n\ndate info\n\nReal description here.")
+ _, desc = parse_blog_markdown(f)
+ assert "small" not in desc
+ assert "Real description here." in desc
+
+ def test_empty_title_when_no_heading(self, tmp_path: Path) -> None:
+ f = tmp_path / "2024_01_01.md"
+ f.write_text("No heading here.\n\nJust paragraphs.")
+ title, _ = parse_blog_markdown(f)
+ assert title == ""
+
+ def test_multiline_description_joined(self, tmp_path: Path) -> None:
+ f = tmp_path / "2024_01_01.md"
+ f.write_text("# Title\n\nLine one.\nLine two.")
+ _, desc = parse_blog_markdown(f)
+ assert "Line one." in desc
+ assert "Line two." in desc
diff --git a/tests/unit/build_scripts/test_prepare_package.py b/tests/unit/build_scripts/test_prepare_package.py
new file mode 100644
index 000000000..53f5026c7
--- /dev/null
+++ b/tests/unit/build_scripts/test_prepare_package.py
@@ -0,0 +1,84 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT license.
+
+import subprocess
+from pathlib import Path
+from unittest.mock import MagicMock, patch
+
+from build_scripts.prepare_package import build_frontend, copy_frontend_to_package
+
+
+class TestBuildFrontend:
+ def test_returns_false_when_npm_not_found(self, tmp_path: Path) -> None:
+ with patch("build_scripts.prepare_package.shutil.which", return_value=None):
+ result = build_frontend(tmp_path)
+ assert result is False
+
+ def test_returns_false_when_package_json_missing(self, tmp_path: Path) -> None:
+ with patch("build_scripts.prepare_package.shutil.which", return_value="/usr/bin/npm"):
+ with patch("build_scripts.prepare_package.subprocess.run", return_value=MagicMock(stdout="10.0.0\n")):
+ result = build_frontend(tmp_path)
+ assert result is False
+
+ def test_returns_false_when_npm_install_fails(self, tmp_path: Path) -> None:
+ (tmp_path / "package.json").write_text("{}")
+ responses = [
+ MagicMock(stdout="10.0.0\n"),
+ subprocess.CalledProcessError(1, "npm install", output="error"),
+ ]
+ with patch("build_scripts.prepare_package.shutil.which", return_value="/usr/bin/npm"):
+ with patch("build_scripts.prepare_package.subprocess.run", side_effect=responses):
+ result = build_frontend(tmp_path)
+ assert result is False
+
+ def test_returns_false_when_npm_build_fails(self, tmp_path: Path) -> None:
+ (tmp_path / "package.json").write_text("{}")
+ responses = [
+ MagicMock(stdout="10.0.0\n"),
+ MagicMock(),
+ subprocess.CalledProcessError(1, "npm run build", output="error"),
+ ]
+ with patch("build_scripts.prepare_package.shutil.which", return_value="/usr/bin/npm"):
+ with patch("build_scripts.prepare_package.subprocess.run", side_effect=responses):
+ result = build_frontend(tmp_path)
+ assert result is False
+
+ def test_returns_true_when_build_succeeds(self, tmp_path: Path) -> None:
+ (tmp_path / "package.json").write_text("{}")
+ with patch("build_scripts.prepare_package.shutil.which", return_value="/usr/bin/npm"):
+ with patch("build_scripts.prepare_package.subprocess.run", return_value=MagicMock(stdout="10.0.0\n")):
+ result = build_frontend(tmp_path)
+ assert result is True
+
+
+class TestCopyFrontendToPackage:
+ def test_returns_false_when_dist_missing(self, tmp_path: Path) -> None:
+ result = copy_frontend_to_package(tmp_path / "dist", tmp_path / "out")
+ assert result is False
+
+ def test_returns_false_when_index_html_missing(self, tmp_path: Path) -> None:
+ dist = tmp_path / "dist"
+ dist.mkdir()
+ (dist / "main.js").write_text("console.log('hi')")
+ result = copy_frontend_to_package(dist, tmp_path / "out")
+ assert result is False
+
+ def test_returns_true_when_copy_succeeds(self, tmp_path: Path) -> None:
+ dist = tmp_path / "dist"
+ dist.mkdir()
+ (dist / "index.html").write_text("")
+ out = tmp_path / "out"
+ result = copy_frontend_to_package(dist, out)
+ assert result is True
+ assert (out / "index.html").exists()
+
+ def test_removes_existing_output_dir(self, tmp_path: Path) -> None:
+ dist = tmp_path / "dist"
+ dist.mkdir()
+ (dist / "index.html").write_text("")
+ out = tmp_path / "out"
+ out.mkdir()
+ (out / "old_file.txt").write_text("old")
+ copy_frontend_to_package(dist, out)
+ assert not (out / "old_file.txt").exists()
+ assert (out / "index.html").exists()
diff --git a/tests/unit/build_scripts/test_validate_docs.py b/tests/unit/build_scripts/test_validate_docs.py
new file mode 100644
index 000000000..bea434a49
--- /dev/null
+++ b/tests/unit/build_scripts/test_validate_docs.py
@@ -0,0 +1,86 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT license.
+
+import tempfile
+from pathlib import Path
+
+import pytest
+
+from build_scripts.validate_docs import find_orphaned_files, parse_toc_files, validate_toc_files
+
+
+class TestParseTocFiles:
+ def test_extracts_single_file(self) -> None:
+ toc = [{"file": "intro"}]
+ result = parse_toc_files(toc)
+ assert "intro" in result
+
+ def test_extracts_nested_children(self) -> None:
+ toc = [{"file": "parent", "children": [{"file": "child"}]}]
+ result = parse_toc_files(toc)
+ assert "parent" in result
+ assert "child" in result
+
+ def test_ignores_entries_without_file(self) -> None:
+ toc = [{"title": "No file here"}]
+ result = parse_toc_files(toc)
+ assert len(result) == 0
+
+ def test_empty_toc(self) -> None:
+ result = parse_toc_files([])
+ assert result == set()
+
+ def test_normalizes_backslashes(self) -> None:
+ toc = [{"file": "setup\\install"}]
+ result = parse_toc_files(toc)
+ assert "setup/install" in result
+
+
+class TestValidateTocFiles:
+ def test_no_errors_when_files_exist(self, tmp_path: Path) -> None:
+ (tmp_path / "intro.md").write_text("# Intro")
+ errors = validate_toc_files({"intro.md"}, tmp_path)
+ assert errors == []
+
+ def test_error_when_file_missing(self, tmp_path: Path) -> None:
+ errors = validate_toc_files({"missing.md"}, tmp_path)
+ assert len(errors) == 1
+ assert "missing.md" in errors[0]
+
+ def test_skips_api_generated_files(self, tmp_path: Path) -> None:
+ errors = validate_toc_files({"api/some_module"}, tmp_path)
+ assert errors == []
+
+ def test_multiple_missing_files(self, tmp_path: Path) -> None:
+ errors = validate_toc_files({"a.md", "b.md"}, tmp_path)
+ assert len(errors) == 2
+
+
+class TestFindOrphanedFiles:
+ def test_no_orphans_when_all_referenced(self, tmp_path: Path) -> None:
+ (tmp_path / "intro.md").write_text("# Intro")
+ orphaned = find_orphaned_files({"intro.md"}, tmp_path)
+ assert orphaned == []
+
+ def test_detects_orphaned_markdown(self, tmp_path: Path) -> None:
+ (tmp_path / "orphan.md").write_text("# Orphan")
+ orphaned = find_orphaned_files(set(), tmp_path)
+ assert any("orphan.md" in o for o in orphaned)
+
+ def test_skips_build_directory(self, tmp_path: Path) -> None:
+ build_dir = tmp_path / "_build"
+ build_dir.mkdir()
+ (build_dir / "generated.md").write_text("# Generated")
+ orphaned = find_orphaned_files(set(), tmp_path)
+ assert not any("_build" in o for o in orphaned)
+
+ def test_skips_myst_yml(self, tmp_path: Path) -> None:
+ (tmp_path / "myst.yml").write_text("project:")
+ orphaned = find_orphaned_files(set(), tmp_path)
+ assert not any("myst.yml" in o for o in orphaned)
+
+ def test_skips_py_companion_files(self, tmp_path: Path) -> None:
+ (tmp_path / "notebook.ipynb").write_text("{}")
+ (tmp_path / "notebook.py").write_text("# companion")
+ orphaned = find_orphaned_files(set(), tmp_path)
+ assert not any("notebook.py" in o for o in orphaned)