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
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.
174 lines
5.6 KiB
Python
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
|