Files
ietf-draft-analyzer/tests/test_obsidian_export.py
Christian Nennemann 1ec1f69bee v0.3.0: Publication-ready release with blog site, paper update, and polish
Release prep:
- Version bump to 0.3.0 (pyproject.toml, cli.py)
- Rewrite README.md with current stats (475 drafts, 713 authors, 501 ideas)
- Add CONTRIBUTING.md with dev setup and code conventions

Blog site:
- Add scripts/build-site.py (markdown → HTML with clean CSS, dark mode, nav)
- Generate static site in docs/blog/ (10 pages)
- Ready for GitHub Pages deployment

Academic paper (paper/main.tex):
- Update all counts: 474→475 drafts, 557→710 authors, 1907→462 ideas, 11→12 gaps
- Add false-positive filtering methodology (113 excluded, 361 relevant)
- Add cross-org convergence analysis (132 ideas, 33% rate)
- Add GDPR compliance gap to gap table
- Add LLM-as-judge caveats to rating methodology and limitations
- Add FIPA, IEEE P3394, W3C WoT to related work with bibliography entries
- Fix safety ratio to show monthly variation (1.5:1 to 21:1)

Pipeline:
- Fetch 1 new draft (475 total), 3 new authors (713 total)
- Fix 16 ruff lint errors across test files
- All 106 tests pass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 17:54:43 +01:00

200 lines
6.5 KiB
Python

"""Tests for the Obsidian vault export.
If this test breaks, the export is out of sync with the data model.
Fix obsidian_export.py to match whatever changed.
"""
from __future__ import annotations
import io
import sys
import zipfile
from pathlib import Path
_project_root = Path(__file__).resolve().parent.parent
if str(_project_root / "src") not in sys.path:
sys.path.insert(0, str(_project_root / "src"))
from webui.obsidian_export import build_obsidian_vault
def test_vault_structure(seeded_db):
"""Vault ZIP should contain expected folders and key files."""
data = build_obsidian_vault(seeded_db)
assert len(data) > 0
z = zipfile.ZipFile(io.BytesIO(data))
names = z.namelist()
# Key structural files must exist
assert "IETF-AI-Agent-Drafts/Dashboard.md" in names
assert "IETF-AI-Agent-Drafts/Authors/index.md" in names
assert "IETF-AI-Agent-Drafts/Categories/index.md" in names
assert "IETF-AI-Agent-Drafts/.obsidian/graph.json" in names
# Should have analysis notes
analysis = [n for n in names if "/Analysis/" in n]
assert len(analysis) >= 3 # Score Distribution, Top Rated, Ideas Overview
def test_vault_has_all_drafts(seeded_db):
"""Every draft in the DB should have a corresponding note in the vault."""
data = build_obsidian_vault(seeded_db)
z = zipfile.ZipFile(io.BytesIO(data))
draft_files = [n for n in z.namelist() if "/Drafts/" in n]
# seeded_db has 5 drafts
assert len(draft_files) == 5
# Check each draft name appears
draft_names = {Path(f).stem for f in draft_files}
assert "draft-alpha-agent-comm" in draft_names
assert "draft-gamma-agent-id" in draft_names
def test_draft_note_has_frontmatter(seeded_db):
"""Draft notes must have YAML frontmatter with score and categories."""
data = build_obsidian_vault(seeded_db)
z = zipfile.ZipFile(io.BytesIO(data))
content = z.read("IETF-AI-Agent-Drafts/Drafts/draft-alpha-agent-comm.md").decode()
# YAML frontmatter
assert content.startswith("---")
assert "score:" in content
assert "novelty:" in content
assert "maturity:" in content
assert "categories:" in content
assert "tags:" in content
# No floating-point noise (e.g., 3.4000000000000004)
import re
long_floats = re.findall(r"\d+\.\d{4,}", content)
assert len(long_floats) == 0, f"Unformatted floats found: {long_floats}"
def test_draft_note_has_wikilinks(seeded_db):
"""Draft notes should link to authors and categories with [[wikilinks]]."""
data = build_obsidian_vault(seeded_db)
z = zipfile.ZipFile(io.BytesIO(data))
content = z.read("IETF-AI-Agent-Drafts/Drafts/draft-alpha-agent-comm.md").decode()
# Should link to authors
assert "[[Alice Researcher]]" in content
assert "[[Bob Engineer]]" in content
# Should link to categories
assert "[[A2A protocols]]" in content
def test_draft_note_has_ideas(seeded_db):
"""Draft notes should include extracted ideas."""
data = build_obsidian_vault(seeded_db)
z = zipfile.ZipFile(io.BytesIO(data))
content = z.read("IETF-AI-Agent-Drafts/Drafts/draft-alpha-agent-comm.md").decode()
assert "Extracted Ideas" in content
assert "Agent Handshake" in content
assert "Capability Negotiation" in content
def test_draft_note_has_rating_bars(seeded_db):
"""Draft notes should include visual score bars."""
data = build_obsidian_vault(seeded_db)
z = zipfile.ZipFile(io.BytesIO(data))
content = z.read("IETF-AI-Agent-Drafts/Drafts/draft-alpha-agent-comm.md").decode()
# Score bars use block chars
assert "\u2588" in content # filled block
assert "\u2591" in content # empty block
assert "/5.0" in content
def test_author_notes(seeded_db):
"""Author notes should list their drafts with wikilinks."""
data = build_obsidian_vault(seeded_db)
z = zipfile.ZipFile(io.BytesIO(data))
content = z.read("IETF-AI-Agent-Drafts/Authors/Alice Researcher.md").decode()
assert content.startswith("---")
assert "affiliation:" in content
assert "ExampleCorp" in content
assert "[[draft-alpha-agent-comm" in content
assert "[[draft-gamma-agent-id" in content
def test_category_notes(seeded_db):
"""Category notes should list drafts with scores."""
data = build_obsidian_vault(seeded_db)
z = zipfile.ZipFile(io.BytesIO(data))
cat_files = [n for n in z.namelist() if "/Categories/" in n and "index" not in n]
# seeded_db has 5 distinct categories
assert len(cat_files) >= 4
# Check one category note
content = z.read("IETF-AI-Agent-Drafts/Categories/A2A protocols.md").decode()
assert "[[draft-alpha-agent-comm" in content
assert "draft_count:" in content
def test_dashboard_has_mermaid(seeded_db):
"""Dashboard should contain Mermaid chart blocks."""
data = build_obsidian_vault(seeded_db)
z = zipfile.ZipFile(io.BytesIO(data))
content = z.read("IETF-AI-Agent-Drafts/Dashboard.md").decode()
assert "```mermaid" in content
assert "pie title" in content
assert "Key Stats" in content
assert "Total Drafts" in content
def test_vault_has_glossary(seeded_db):
"""Vault should contain a Glossary with scoring dimensions explained."""
data = build_obsidian_vault(seeded_db)
z = zipfile.ZipFile(io.BytesIO(data))
assert "IETF-AI-Agent-Drafts/Analysis/Glossary.md" in z.namelist()
content = z.read("IETF-AI-Agent-Drafts/Analysis/Glossary.md").decode()
# All five dimensions must be explained
for dim in ("Novelty", "Maturity", "Overlap", "Momentum", "Relevance"):
assert dim in content, f"Glossary missing dimension: {dim}"
assert "Composite Score" in content
assert "Internet-Draft" in content
def test_top_rated_uses_full_names(seeded_db):
"""Top Rated table should use full dimension names, not abbreviations."""
data = build_obsidian_vault(seeded_db)
z = zipfile.ZipFile(io.BytesIO(data))
content = z.read("IETF-AI-Agent-Drafts/Analysis/Top Rated.md").decode()
assert "Novelty" in content
assert "Maturity" in content
assert "| Nov |" not in content # no abbreviations
def test_vault_is_valid_zip(seeded_db):
"""The output should be a valid ZIP that can be extracted."""
data = build_obsidian_vault(seeded_db)
z = zipfile.ZipFile(io.BytesIO(data))
# Should not raise
bad = z.testzip()
assert bad is None, f"Corrupt file in ZIP: {bad}"
# All files should be decodable as UTF-8
for name in z.namelist():
if name.endswith(".md"):
z.read(name).decode("utf-8")