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>
200 lines
6.5 KiB
Python
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")
|