Files
stack/tests/docs/test_docs_build.py
kert f34600749a
All checks were successful
CI / lint (push) Successful in 35s
CI / notebooks-smoke (push) Successful in 1m25s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / llm (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / notebooks (push) Successful in 58s
Infra CI / zotero (push) Successful in 18s
Infra CI / docs (push) Successful in 17s
Infra CI / api (push) Successful in 1m9s
Infra CI / llm (push) Successful in 45s
Infra CI / mc (push) Successful in 13s
Deploy / report (push) Successful in 13s
CI / test (push) Successful in 14m14s
fix(docs): self-consistent, hermetic library export (closes #623)
The exporter read tags in a second query after the minutes-long item
hydration; a tag edit landing in between (P37 controller tag ops during
#618's suite run) produced a library.json whose item tags were missing
from the tag list, failing test_tags_in_items_match_tag_list on every
run until regeneration. The tag list is now derived from the serialized
items themselves (self-consistent by construction) and the output file
is swapped in atomically.

Tests no longer assert on live data/bib.sqlite: TestExportLibrary
exports a fixture store via new STACK_BIB_DB/STACK_LIBRARY_JSON
overrides — the live-store contract is the docs build's own concern.

Co-designed with a peer session that landed the derived-tags exporter.
2026-08-14 15:10:48 -04:00

174 lines
5.6 KiB
Python

"""Integration tests for the docs build pipeline.
TestExportLibrary is hermetic (#623): it exports a small fixture store
via STACK_BIB_DB/STACK_LIBRARY_JSON overrides instead of asserting on
the live, constantly-mutating data/bib.sqlite.
"""
from __future__ import annotations
import json
import os
import re
import subprocess
import sys
from pathlib import Path
import pytest
_ROOT = Path(__file__).resolve().parents[2]
EXPORT_SCRIPT = _ROOT / "docs/scripts/export_library.py"
DOCS_DIR = _ROOT / "docs/docs"
INTRO_MD = DOCS_DIR / "intro.md"
class TestExportLibrary:
"""Tests for docs/scripts/export_library.py."""
@pytest.fixture
def exported(self, tmp_path):
"""Export a 2-item fixture store; return the parsed payload."""
from bib.item import Item
from bib.store import Store
db = tmp_path / "bib.sqlite"
store = Store(str(db), storage_dir=tmp_path / "storage")
store.create(
Item(
item_type="rule",
title="CY2027 PFS proposed rule",
url="https://example.gov/nprm",
abstract="A" * 400,
tags=["module:pfs", "year:2027"],
)
)
store.create(
Item(
item_type="source",
title="Comment letter",
url="https://example.gov/comment",
tags=["module:pfs", "docket:CMS-2026-2377"],
)
)
store.close()
out = tmp_path / "library.json"
result = subprocess.run(
[sys.executable, str(EXPORT_SCRIPT)],
capture_output=True,
text=True,
cwd=_ROOT,
env={
**os.environ,
"STACK_BIB_DB": str(db),
"STACK_LIBRARY_JSON": str(out),
},
)
assert result.returncode == 0, result.stderr
return json.loads(out.read_text())
def test_export_writes_fixture_items(self, exported):
assert len(exported["items"]) == 2
titles = {i["title"] for i in exported["items"]}
assert titles == {"CY2027 PFS proposed rule", "Comment letter"}
def test_item_has_required_fields(self, exported):
required = {
"key",
"item_type",
"title",
"url",
"abstract",
"tags",
"collections",
"creators",
}
for item in exported["items"]:
missing = required - set(item.keys())
assert not missing, f"Item {item.get('key', '?')} missing fields: {missing}"
def test_abstracts_not_truncated(self, exported):
by_title = {i["title"]: i for i in exported["items"]}
assert by_title["CY2027 PFS proposed rule"]["abstract"] == "A" * 400
def test_tags_match_item_tags(self, exported):
"""The tag list is exactly the tags on serialized items, with
counts — self-consistent by construction, so a tag edit landing
mid-export can no longer orphan item tags (#623)."""
assert exported["tags"] == [
{"name": "docket:CMS-2026-2377", "count": 1},
{"name": "module:pfs", "count": 2},
{"name": "year:2027", "count": 1},
]
def test_missing_db_writes_empty(self, tmp_path):
out = tmp_path / "library.json"
result = subprocess.run(
[sys.executable, str(EXPORT_SCRIPT)],
capture_output=True,
text=True,
cwd=_ROOT,
env={
**os.environ,
"STACK_BIB_DB": str(tmp_path / "nope.sqlite"),
"STACK_LIBRARY_JSON": str(out),
},
)
assert result.returncode == 0, result.stderr
assert json.loads(out.read_text()) == {
"items": [],
"collections": [],
"tags": [],
}
class TestIntroLinks:
"""Verify intro.md links resolve to real pages."""
def test_intro_exists(self):
assert INTRO_MD.exists(), "intro.md not found"
def test_no_broken_relative_links(self):
"""All relative links in intro.md should point to existing files."""
if not INTRO_MD.exists():
import pytest
pytest.skip("intro.md not found")
content = INTRO_MD.read_text()
# Find markdown links like [text](path)
links = re.findall(r"\[.*?\]\(([^)]+)\)", content)
for link in links:
if link.startswith("http") or link.startswith("#"):
continue
# Relative link — check it resolves
target = DOCS_DIR / link.lstrip("/")
# Could be a directory with index.md or a .md file
exists = (
target.exists()
or target.with_suffix(".md").exists()
or (target / "index.md").exists()
or target.parent.exists() # directory exists
)
assert exists, f"Broken link in intro.md: {link}"
class TestExtractDocs:
"""Verify the API doc extraction produces output."""
def test_api_docs_directory_exists(self):
api_dir = DOCS_DIR / "api"
if not api_dir.exists():
import pytest
pytest.skip("API docs not generated yet")
assert api_dir.is_dir()
def test_api_has_packages(self):
api_dir = DOCS_DIR / "api"
if not api_dir.exists():
pytest.skip("API docs not generated yet")
packages = [d.name for d in api_dir.iterdir() if d.is_dir()]
if not packages:
pytest.skip("API docs not generated yet (griffe not run)")
assert len(packages) > 0