tests/test_docs_consistency.py (stdlib only, reads files, no process or network modules): - README anchor links resolve (GitHub slug rule pinned), code fences balanced - every README "## " section and the key features are linked from the TOC - newest changelog entry is the 11/4/26 one - every route registered with routes.get/post is in ProjectStructure.md (method + path) - the file map lists every root .py, web/ file, resources/ file, tests/e2e, tests/js, examples, tests.yml and names no .py that does not exist - every ultimate_grid.* event sent by the code is in Server Event Types (+ sweep phase payload) - html_generator.js_files ends with logic_standalone.js and is listed in the docs - no stale convertStateToConfigs / network_utils.py mentions - CONTRIBUTING.md names every CI job id and the CI flags tests.yml: comment-only edit, the "# expect:" counts now match the measured 499 passed / 16 skipped (stub leg). Note: the docs checks are expected to be red at this commit; the next commit (docs) makes them green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
202 lines
8.1 KiB
Python
202 lines
8.1 KiB
Python
"""Docs vs code consistency checks (stdlib only, reads files, nothing else).
|
|
|
|
These tests fail when the documentation drifts from the code: a route, an event,
|
|
a file or a CI step that exists in the repo but is missing from the docs (or the
|
|
other way round), a broken README anchor, or a stale name that was removed.
|
|
When one fails after you add a route / file / event, update the docs it names.
|
|
"""
|
|
import re
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _read(rel):
|
|
return (ROOT / rel).read_text(encoding="utf-8").replace("\r\n", "\n")
|
|
|
|
|
|
def _headings(text):
|
|
"""(level, title) for every markdown heading outside fenced code blocks."""
|
|
out = []
|
|
fenced = False
|
|
for line in text.split("\n"):
|
|
if line.lstrip().startswith("```"):
|
|
fenced = not fenced
|
|
continue
|
|
if fenced:
|
|
continue
|
|
m = re.match(r"^(#{1,6})\s+(.+?)\s*$", line)
|
|
if m:
|
|
out.append((len(m.group(1)), m.group(2)))
|
|
return out
|
|
|
|
|
|
def _slug(title):
|
|
"""GitHub heading anchor rule (without the duplicate suffix)."""
|
|
t = title.replace("**", "").replace("`", "")
|
|
t = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", t)
|
|
t = t.lower()
|
|
t = re.sub(r"[^\w\- ]", "", t)
|
|
return t.replace(" ", "-")
|
|
|
|
|
|
def _slugs(text):
|
|
seen = {}
|
|
out = []
|
|
for _level, title in _headings(text):
|
|
s = _slug(title)
|
|
n = seen.get(s, 0)
|
|
seen[s] = n + 1
|
|
out.append(s if n == 0 else f"{s}-{n}")
|
|
return out
|
|
|
|
|
|
def _fenced_block_after(text, heading_fragment):
|
|
"""First fenced code block that follows the heading containing heading_fragment."""
|
|
start = text.index(heading_fragment)
|
|
m = re.search(r"```[^\n]*\n(.*?)\n```", text[start:], re.S)
|
|
assert m, f"no code block after {heading_fragment!r}"
|
|
return m.group(1)
|
|
|
|
|
|
def _section(text, heading_fragment):
|
|
"""Text from the heading containing heading_fragment up to the next '---' rule."""
|
|
start = text.index(heading_fragment)
|
|
end = text.find("\n---\n", start)
|
|
return text[start:end if end != -1 else len(text)]
|
|
|
|
|
|
def _root_py():
|
|
return sorted(p for p in ROOT.glob("*.py"))
|
|
|
|
|
|
# ---------------------------------------------------------------- slug rule
|
|
|
|
def test_slug_rule_is_pinned():
|
|
assert _slug("🌟 Key Features") == "-key-features"
|
|
assert _slug("Job Order & Preview Pass (Run Settings)") == "job-order--preview-pass-run-settings"
|
|
assert _slug("Distribution API (registered via `distribution_routes.py`)") == \
|
|
"distribution-api-registered-via-distribution_routespy"
|
|
assert _slug("[Link](#x) **Bold**") == "link-bold"
|
|
|
|
|
|
# ---------------------------------------------------------------- README
|
|
|
|
def test_readme_anchor_links_resolve():
|
|
text = _read("README.md")
|
|
assert text.count("```") % 2 == 0, "unbalanced code fences in README.md"
|
|
slugs = set(_slugs(text))
|
|
# Raw HTML anchors (name= / id=) also count as targets.
|
|
slugs |= set(re.findall(r'(?:name|id)="([^"]+)"', text))
|
|
scan = re.sub(r"```.*?```", "", text, flags=re.S)
|
|
missing = sorted({a for a in re.findall(r"\]\(#([^)\s]+)\)", scan) if a not in slugs})
|
|
assert not missing, f"README anchors with no heading: {missing}"
|
|
|
|
|
|
def test_readme_toc_covers_sections():
|
|
text = _read("README.md")
|
|
toc = _section(text, "## 📑 Table of Contents")
|
|
linked = set(re.findall(r"\]\(#([^)\s]+)\)", toc))
|
|
titles = _headings(text)
|
|
skip = {titles[0][1], "📑 Table of Contents"}
|
|
missing = [t for lvl, t in titles if lvl == 2 and t not in skip and _slug(t) not in linked]
|
|
assert not missing, f"README '## ' sections absent from the TOC: {missing}"
|
|
required = [
|
|
"Workflow Sweep", "Pairwise Review", "Standalone Dashboard", "Image Metrics",
|
|
"Job Order", "Skip Layer Guidance", "MOSS", "Run API", "Development / Tests",
|
|
]
|
|
by_title = {t: _slug(t) for _lvl, t in titles}
|
|
for frag in required:
|
|
hits = [s for t, s in by_title.items() if frag in t]
|
|
assert hits, f"README has no heading containing {frag!r}"
|
|
assert any(s in linked for s in hits), f"TOC does not link {frag!r}"
|
|
|
|
|
|
def test_changelog_has_11_4():
|
|
text = _read("README.md")
|
|
start = text.index("## 📝 Changelog")
|
|
first = re.search(r"^### (Update .*)$", text[start:], re.M)
|
|
assert first and first.group(1).startswith("Update 11/4/26"), \
|
|
f"newest changelog entry is {first.group(1) if first else None!r}"
|
|
|
|
|
|
# ---------------------------------------------------------------- ProjectStructure
|
|
|
|
def test_routes_documented():
|
|
doc = _read("ProjectStructure.md").split("\n")
|
|
pat = re.compile(r'routes\.(get|post|put|delete)\(\s*"([^"]+)"')
|
|
missing = []
|
|
for p in _root_py():
|
|
if p.name == "conftest.py":
|
|
continue
|
|
for method, path in pat.findall(p.read_text(encoding="utf-8")):
|
|
needle = "`" + path + "`"
|
|
if not any(needle in line and method.upper() in line for line in doc):
|
|
missing.append(f"{method.upper()} {path} ({p.name})")
|
|
assert not missing, "routes missing from ProjectStructure.md API Routes: " + ", ".join(missing)
|
|
|
|
|
|
def test_file_map_complete():
|
|
text = _read("ProjectStructure.md")
|
|
block = _fenced_block_after(text, "Project File Structure")
|
|
names = [p.name for p in _root_py()]
|
|
names += [p.name for p in (ROOT / "web").rglob("*") if p.suffix in (".js", ".md")]
|
|
names += [p.name for p in (ROOT / "resources").glob("*") if p.is_file()]
|
|
names += ["tests/e2e/", "tests/js/", "examples/", "tests.yml", "pytest.ini"]
|
|
missing = sorted({n for n in names if n not in block})
|
|
assert not missing, f"files absent from the ProjectStructure.md file map: {missing}"
|
|
on_disk = {p.name for p in ROOT.rglob("*.py") if ".git" not in p.parts}
|
|
ghosts = sorted({n for n in re.findall(r"[\w\-]+\.py\b", block) if n not in on_disk})
|
|
assert not ghosts, f"file map names .py files that do not exist: {ghosts}"
|
|
|
|
|
|
def test_events_documented():
|
|
pat = re.compile(r'send_sync\(\s*"(ultimate_grid\.\w+)"')
|
|
sent = set()
|
|
for p in _root_py():
|
|
sent |= set(pat.findall(p.read_text(encoding="utf-8")))
|
|
assert sent, "no events found in the code"
|
|
text = _read("ProjectStructure.md")
|
|
sec = _section(text, "### **Server Event Types**")
|
|
missing = sorted(e for e in sent if "`" + e + "`" not in sec)
|
|
assert not missing, f"events missing from Server Event Types: {missing}"
|
|
assert "phase" in sec and '"sweep"' in sec, "progress payload must document phase and the sweep payload"
|
|
|
|
|
|
def test_js_files_order():
|
|
src = _read("html_generator.py")
|
|
m = re.search(r"js_files\s*=\s*\[(.*?)\n\s*\]", src, re.S)
|
|
assert m
|
|
files = re.findall(r'^\s*"([\w\-]+\.js)"', m.group(1), re.M)
|
|
assert files and files[-1] == "logic_standalone.js"
|
|
for f in files:
|
|
assert (ROOT / "resources" / f).is_file(), f
|
|
assert files.index("logic_ratings.js") < files.index("logic_ui.js")
|
|
assert files.index("logic_pipeline.js") < files.index("logic_compare.js")
|
|
for name in ("ProjectStructure.md", "AI-CONTEXT.md"):
|
|
doc = _read(name)
|
|
pos = [doc.find(f) for f in files]
|
|
assert all(p != -1 for p in pos), f"{name} must list every js_files entry"
|
|
|
|
|
|
def test_no_stale_names():
|
|
for name in ("AI-CONTEXT.md", "ProjectStructure.md"):
|
|
for no, line in enumerate(_read(name).split("\n"), 1):
|
|
for stale in ("convertStateToConfigs", "network_utils.py"):
|
|
if stale in line and "no longer" not in line and "removed" not in line:
|
|
raise AssertionError(f"{name}:{no} mentions {stale}")
|
|
|
|
|
|
# ---------------------------------------------------------------- CONTRIBUTING vs CI
|
|
|
|
def test_contributing_matches_ci():
|
|
ci = _read(".github/workflows/tests.yml")
|
|
jobs = ci[ci.index("\njobs:"):]
|
|
ids = re.findall(r"^ ([\w\-]+):\s*$", jobs, re.M)
|
|
assert ids, "no job ids found in tests.yml"
|
|
contrib = _read("CONTRIBUTING.md")
|
|
missing = [i for i in ids if i not in contrib]
|
|
assert not missing, f"CI jobs not named in CONTRIBUTING.md: {missing}"
|
|
for flag in ("--ignore=tests/e2e", "--browser chromium", "node --test tests/js/", "--input-type=module"):
|
|
assert flag in contrib, f"CONTRIBUTING.md does not mention {flag!r}"
|