Files
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

384 lines
14 KiB
Python

"""Self-tests + the real gate for ``scripts/check_docs_module_refs.py``.
Mirrors the sibling ``tests/unit/test_claude_md_freshness.py`` (CLAUDE.md map)
and the pure-detector idiom of ``tests/_guardrails/test_v080_deprecation_coverage.py``:
the detector core :func:`find_violations` is IO-free, so the synthetic
self-checks below feed crafted doc text + a dict-backed ``resolver`` while the
real-gate test (:func:`test_real_docs_have_no_violations`) drives the same code
over the live ``docs/`` tree.
The self-checks are non-vacuous and bidirectional: a planted dead LINK is
reported, a planted dead inline ref is reported, the correct (resolving) inline
ref is clean, ADR/refactor-history docs skip the inline check but NOT the link
check, an allowlisted dead inline ref is clean, and a bogus allowlist entry is
caught by the shrink-only guard.
"""
from __future__ import annotations
import os
import sys
import pytest
# Add project root to sys.path so we can import scripts (matches the sibling).
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
from scripts.check_docs_module_refs import ( # noqa: E402
_ALLOWLIST,
_is_historical_prose,
_is_module_shaped,
_unused_allowlist_entries,
collect_violations,
find_violations,
main,
)
pytestmark = pytest.mark.repo_lint
# A resolver stub: a ref/target resolves iff it is in ``existing``. The CLI uses
# a filesystem-backed resolver; the pure detector only needs this predicate.
def _stub_resolver(existing: set[str]):
return lambda ref_or_target: ref_or_target in existing
# --- find_violations: link check (every doc, no allowlist) --------------------
def test_dead_link_into_package_is_reported() -> None:
text = "See [the helper](../src/notebooklm/_gone.py) for details.\n"
violations = find_violations(
"docs/foo.md",
text,
resolver=_stub_resolver(set()),
is_live=True,
allowlist={},
)
assert [(v.kind, v.target) for v in violations] == [("link", "../src/notebooklm/_gone.py")]
def test_resolving_link_into_package_is_clean() -> None:
target = "../src/notebooklm/_runtime/lifecycle.py"
text = f"See [the helper]({target}).\n"
violations = find_violations(
"docs/foo.md",
text,
resolver=_stub_resolver({target}),
is_live=True,
allowlist={},
)
assert violations == []
def test_external_and_anchor_links_are_ignored() -> None:
text = (
"[home](https://example.com/src/notebooklm/x.py) [anchor](#section) [other](../README.md)\n"
)
violations = find_violations(
"docs/foo.md",
text,
resolver=_stub_resolver(set()),
is_live=True,
allowlist={},
)
# None of these are *relative paths into src/notebooklm/*, so none are checked.
assert violations == []
# --- find_violations: inline check (live docs only, allowlisted) --------------
def test_dead_inline_flat_ref_is_reported_and_subpackage_form_is_clean() -> None:
dead = "The runtime lives in `_runtime_lifecycle.py` today.\n"
flagged = find_violations(
"docs/architecture.md",
dead,
resolver=_stub_resolver(set()),
is_live=True,
allowlist={},
)
assert [(v.kind, v.target) for v in flagged] == [("inline", "_runtime_lifecycle.py")]
# The correct subpackage form resolves -> clean.
fixed = "The runtime lives in `_runtime/lifecycle.py` today.\n"
clean = find_violations(
"docs/architecture.md",
fixed,
resolver=_stub_resolver({"_runtime/lifecycle.py"}),
is_live=True,
allowlist={},
)
assert clean == []
def test_adr_doc_skips_inline_check_but_not_link_check() -> None:
# An ADR / refactor-history doc names a historical module in prose (inline)
# AND links to a dead target. The inline ref is NOT reported (is_live=False),
# but the dead LINK still is.
text = (
"Historically `_runtime_lifecycle.py` owned this "
"([src](../../src/notebooklm/_runtime_lifecycle.py)).\n"
)
violations = find_violations(
"docs/adr/0099-example.md",
text,
resolver=_stub_resolver(set()),
is_live=False,
allowlist={},
)
assert [(v.kind, v.target) for v in violations] == [
("link", "../../src/notebooklm/_runtime_lifecycle.py")
]
def test_allowlisted_dead_inline_ref_is_clean() -> None:
text = "The deleted `_core.py` shim explains the logger name.\n"
allowlist = {"docs/development.md:_core.py": "historical: deleted compat shim"}
violations = find_violations(
"docs/development.md",
text,
resolver=_stub_resolver(set()),
is_live=True,
allowlist=allowlist,
)
assert violations == []
# The same dead ref in a *different* doc (not keyed) is still reported.
elsewhere = find_violations(
"docs/architecture.md",
text,
resolver=_stub_resolver(set()),
is_live=True,
allowlist=allowlist,
)
assert [(v.kind, v.target) for v in elsewhere] == [("inline", "_core.py")]
def test_test_and_script_refs_are_not_module_shaped() -> None:
# The inline check excludes test_*.py / conftest.py and tests/ + scripts/.
assert not _is_module_shaped("test_client.py")
assert not _is_module_shaped("conftest.py")
assert not _is_module_shaped("tests/unit/test_x.py")
assert not _is_module_shaped("scripts/check_docs_module_refs.py")
# ...but real package modules (flat and subpackage) are, including the
# rpc/ + cli/ subpackages and the notebooklm_cli entry point (broadened
# per review so their inline refs are resolved, not silently skipped).
assert _is_module_shaped("_runtime_lifecycle.py")
assert _is_module_shaped("_runtime/lifecycle.py")
assert _is_module_shaped("client.py")
assert _is_module_shaped("types.py")
assert _is_module_shaped("rpc/types.py")
assert _is_module_shaped("cli/session_cmd.py")
assert _is_module_shaped("cli/services/generate.py")
assert _is_module_shaped("notebooklm_cli.py")
def test_historical_prose_docs_are_classified() -> None:
# ADRs, refactor-history, and the CHANGELOG are historical-prose (inline
# check skipped); ordinary live docs are not.
assert _is_historical_prose("docs/adr/0014-x.md")
assert _is_historical_prose("docs/refactor-history.md")
assert _is_historical_prose("CHANGELOG.md")
assert not _is_historical_prose("docs/architecture.md")
assert not _is_historical_prose("README.md")
def test_changelog_skips_inline_check_but_enforces_links(tmp_path) -> None:
# CHANGELOG entries name modules as they were at the time (e.g. cli/note.py
# pre-_cmd-rename); the inline check must skip them, but a dead LINK into the
# package is still reported.
_write(tmp_path / "src/notebooklm/__init__.py", "")
_write(tmp_path / "src/notebooklm/cli/note_cmd.py", "")
(tmp_path / "docs").mkdir() # main() requires a docs/ tree to exist
# Historical inline ref to a now-renamed module -> allowed.
_write(tmp_path / "CHANGELOG.md", "Fixed `cli/note.py` exit codes.\n")
assert main(["--repo-root", str(tmp_path)]) == 0
# ...but a dead link into the package in the CHANGELOG still fails.
_write(tmp_path / "CHANGELOG.md", "See [src](src/notebooklm/cli/note.py).\n")
assert main(["--repo-root", str(tmp_path)]) == 1
def test_inline_ref_to_tests_path_is_not_flagged() -> None:
# A live doc may legitimately cite a test file inline; it must not be
# checked against src/notebooklm/.
text = "Pinned in `tests/unit/test_row_adapters.py`.\n"
violations = find_violations(
"docs/architecture.md",
text,
resolver=_stub_resolver(set()),
is_live=True,
allowlist={},
)
assert violations == []
# --- Shrink-only allowlist guard ----------------------------------------------
def test_unused_allowlist_entry_for_missing_doc_is_flagged(tmp_path) -> None:
# An allowlist entry whose doc does not exist is dead weight under
# strict_missing (the real-repo guard); under the default it is skipped so
# main() stays repo-root-agnostic.
(tmp_path / "docs").mkdir()
entry = {"docs/ghost.md:_core.py": "stale entry, doc is gone"}
assert _unused_allowlist_entries(tmp_path, entry, strict_missing=True) == [
"docs/ghost.md:_core.py"
]
assert _unused_allowlist_entries(tmp_path, entry) == []
def test_unused_allowlist_entry_for_now_resolving_ref_is_flagged(tmp_path) -> None:
# If the ref now resolves under src/notebooklm/, the allowlist entry is
# obsolete — the gate should force its removal even though the doc still
# mentions it.
docs = tmp_path / "docs"
docs.mkdir()
(docs / "guide.md").write_text("We use `_now_real.py` here.\n", encoding="utf-8")
pkg = tmp_path / "src" / "notebooklm"
pkg.mkdir(parents=True)
(pkg / "_now_real.py").touch()
unused = _unused_allowlist_entries(
tmp_path, {"docs/guide.md:_now_real.py": "was missing, now exists"}
)
assert unused == ["docs/guide.md:_now_real.py"]
def test_unused_allowlist_entry_for_unmentioned_ref_is_flagged(tmp_path) -> None:
# The doc no longer mentions the ref at all -> entry is dead weight.
docs = tmp_path / "docs"
docs.mkdir()
(docs / "guide.md").write_text("No module here.\n", encoding="utf-8")
(tmp_path / "src" / "notebooklm").mkdir(parents=True)
unused = _unused_allowlist_entries(
tmp_path, {"docs/guide.md:_gone.py": "doc dropped the mention"}
)
assert unused == ["docs/guide.md:_gone.py"]
def test_genuinely_needed_allowlist_entry_is_not_flagged(tmp_path) -> None:
# Doc exists, mentions the ref inline, and the ref does NOT resolve -> kept.
docs = tmp_path / "docs"
docs.mkdir()
(docs / "guide.md").write_text("The deleted `_core.py` shim.\n", encoding="utf-8")
(tmp_path / "src" / "notebooklm").mkdir(parents=True)
unused = _unused_allowlist_entries(
tmp_path, {"docs/guide.md:_core.py": "historical: deleted compat shim"}
)
assert unused == []
# --- main(): end-to-end exit codes on a synthetic repo ------------------------
def _write(path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
def test_main_fails_on_dead_link(tmp_path, capsys) -> None:
_write(tmp_path / "src/notebooklm/__init__.py", "")
_write(
tmp_path / "docs/foo.md",
"[dead](../src/notebooklm/_gone.py)\n",
)
assert main(["--repo-root", str(tmp_path)]) == 1
assert "Broken links into src/notebooklm/" in capsys.readouterr().err
def test_main_fails_on_dead_inline_ref(tmp_path, capsys) -> None:
_write(tmp_path / "src/notebooklm/__init__.py", "")
_write(tmp_path / "docs/foo.md", "Runtime in `_runtime_lifecycle.py`.\n")
assert main(["--repo-root", str(tmp_path)]) == 1
assert "Dead inline module refs in live docs" in capsys.readouterr().err
def test_main_fails_on_stale_allowlist(tmp_path, capsys, monkeypatch) -> None:
# Completes main()'s exit-code coverage: the third error branch fires when an
# _ALLOWLIST entry is no longer justified (its doc exists but no longer
# mentions the ref). Patch the module-level allowlist with a bogus entry.
import scripts.check_docs_module_refs as mod
_write(tmp_path / "src/notebooklm/__init__.py", "")
_write(tmp_path / "docs/foo.md", "No module mention here.\n")
monkeypatch.setattr(mod, "_ALLOWLIST", {"docs/foo.md:_old.py": "stale: doc dropped it"})
assert mod.main(["--repo-root", str(tmp_path)]) == 1
assert "Stale _ALLOWLIST entries" in capsys.readouterr().err
def test_main_succeeds_when_all_refs_resolve(tmp_path) -> None:
_write(tmp_path / "src/notebooklm/__init__.py", "")
_write(tmp_path / "src/notebooklm/_runtime/lifecycle.py", "")
_write(
tmp_path / "docs/foo.md",
"Runtime in `_runtime/lifecycle.py` ([src](../src/notebooklm/_runtime/lifecycle.py)).\n",
)
assert main(["--repo-root", str(tmp_path)]) == 0
def test_main_returns_2_when_docs_dir_missing(tmp_path) -> None:
assert main(["--repo-root", str(tmp_path)]) == 2
def test_main_skips_inline_check_in_adr_but_enforces_links(tmp_path) -> None:
_write(tmp_path / "src/notebooklm/__init__.py", "")
# ADR names a dead flat module inline (allowed) but its link resolves.
_write(tmp_path / "src/notebooklm/_runtime/lifecycle.py", "")
_write(
tmp_path / "docs/adr/0099-x.md",
"Historically `_runtime_lifecycle.py` "
"([src](../../src/notebooklm/_runtime/lifecycle.py)).\n",
)
assert main(["--repo-root", str(tmp_path)]) == 0
# Now make the ADR's link dead -> must fail even though inline is skipped.
_write(
tmp_path / "docs/adr/0099-x.md",
"Historically `_runtime_lifecycle.py` "
"([src](../../src/notebooklm/_runtime_lifecycle.py)).\n",
)
assert main(["--repo-root", str(tmp_path)]) == 1
# --- The real gate ------------------------------------------------------------
def test_real_docs_have_no_violations() -> None:
"""The live ``docs/`` tree has zero dead links / dead inline refs.
This is the gate that forces the #1328 stale-ref burn-down to stay done.
"""
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
assert main(["--repo-root", repo_root]) == 0
def test_real_allowlist_is_not_stale() -> None:
"""Every ``_ALLOWLIST`` entry is still genuinely needed (shrink-only).
A live entry must point at a doc that still mentions the ref inline AND a ref
that still does not resolve under ``src/notebooklm/``. The moment a fix lands,
the entry must be removed; this catches a bogus/stale entry.
"""
from pathlib import Path
repo_root = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
unused = _unused_allowlist_entries(repo_root, _ALLOWLIST, strict_missing=True)
assert unused == [], (
"Stale _ALLOWLIST entries in scripts/check_docs_module_refs.py — the "
"allowlist is shrink-only. Remove these (the doc no longer needs the "
f"exemption): {unused}"
)
def test_collect_violations_over_real_tree_is_empty() -> None:
"""Direct call to the filesystem-backed collector returns no violations."""
from pathlib import Path
repo_root = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
assert collect_violations(repo_root) == []