Files
ietf-draft-analyzer/scripts/test_all_routes.py
Christian Nennemann d1a20fa02e feat: add blog draft generator and fix broken routes
Blog drafting section (dev-only):
- BlogDraftGenerator gathers project data (gaps, proposals, stats) as
  context and calls Claude to produce Medium-style blog posts
- DB schema: blog_drafts table with title, content, tags, cost tracking
- Web UI: list, generate (async with live preview), detail (rendered +
  source toggle), edit, and export routes
- 6 writing styles: deep-dive, overview, opinion, listicle, comparison,
  series-post
- Nav link added to sidebar under Proposals

Bug fixes found via route testing (scripts/test_all_routes.py):
- /authors/<id>: Draft.status → Draft.states (correct attribute name)
- /false-positives: add missing `import re` in ratings.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 04:39:16 +01:00

226 lines
8.7 KiB
Python

#!/usr/bin/env python3
"""Test ALL web UI routes and report any returning 500 errors.
Runs against the Flask test client with dev=True so admin routes are accessible.
Queries SQLite DB for real IDs needed by dynamic routes.
"""
import os
import sys
import sqlite3
import traceback
# Must run from src/ directory
src_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'src')
sys.path.insert(0, src_dir)
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'drafts.db')
def get_test_ids():
"""Query the database for real IDs to use in dynamic routes."""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
ids = {}
# Get a draft name
row = conn.execute("SELECT name FROM drafts LIMIT 1").fetchone()
ids["draft_name"] = row["name"] if row else "draft-nonexistent"
# Get a person_id from authors
row = conn.execute("SELECT person_id FROM authors WHERE person_id IS NOT NULL LIMIT 1").fetchone()
ids["person_id"] = row["person_id"] if row else 1
# Get a gap_id
row = conn.execute("SELECT id FROM gaps LIMIT 1").fetchone()
ids["gap_id"] = row["id"] if row else 1
# Get an idea_id
row = conn.execute("SELECT id FROM ideas LIMIT 1").fetchone()
ids["idea_id"] = row["id"] if row else 1
# Get a proposal_id (may not exist)
try:
row = conn.execute("SELECT id FROM proposals LIMIT 1").fetchone()
ids["proposal_id"] = row["id"] if row else None
except Exception:
ids["proposal_id"] = None
# Get a blog_draft_id (may not exist)
try:
row = conn.execute("SELECT id FROM blog_drafts LIMIT 1").fetchone()
ids["blog_draft_id"] = row["id"] if row else None
except Exception:
ids["blog_draft_id"] = None
# Get two draft names for compare
rows = conn.execute("SELECT name FROM drafts LIMIT 2").fetchall()
ids["two_drafts"] = ",".join(r["name"] for r in rows) if len(rows) >= 2 else ""
conn.close()
return ids
def main():
ids = get_test_ids()
print(f"Test IDs: draft={ids['draft_name']}, person={ids['person_id']}, "
f"gap={ids['gap_id']}, idea={ids['idea_id']}, "
f"proposal={ids['proposal_id']}, blog={ids['blog_draft_id']}")
print()
from webui.app import create_app
app = create_app(dev=True)
client = app.test_client()
# Define all routes to test: (method, url, description)
routes = [
# === Pages (public) ===
("GET", "/", "overview"),
("GET", "/drafts", "drafts list"),
("GET", f"/drafts/{ids['draft_name']}", "draft detail"),
("GET", "/ideas", "ideas"),
("GET", "/ratings", "ratings"),
("GET", "/timeline", "timeline"),
("GET", "/idea-clusters", "idea clusters"),
("GET", "/architecture", "architecture"),
("GET", f"/authors/{ids['person_id']}", "author detail"),
("GET", "/authors", "authors"),
("GET", "/citations", "citations"),
("GET", "/about", "about"),
("GET", "/impressum", "impressum"),
("GET", "/datenschutz", "datenschutz"),
("GET", "/search", "search (empty)"),
("GET", "/search?q=agent", "search (query)"),
("GET", "/ask", "ask (empty)"),
# === API (public) ===
("GET", "/api/drafts", "api drafts"),
("GET", "/api/stats", "api stats"),
("GET", "/api/authors/network", "api author network"),
("GET", "/api/citations", "api citations"),
("GET", "/api/search?q=agent", "api search"),
("GET", "/api/ideas", "api ideas"),
("GET", "/api/ratings", "api ratings"),
("GET", "/api/timeline", "api timeline"),
("GET", "/api/idea-clusters", "api idea clusters"),
("GET", "/api/categories", "api categories"),
("GET", f"/api/drafts/{ids['draft_name']}", "api draft detail"),
("GET", "/api/architecture", "api architecture"),
# === Admin pages ===
("GET", "/gaps", "gaps"),
("GET", "/gaps/demo", "gaps demo"),
("GET", f"/gaps/{ids['gap_id']}", "gap detail"),
("GET", "/api/gaps", "api gaps"),
("GET", f"/api/gaps/{ids['gap_id']}", "api gap detail"),
("GET", "/monitor", "monitor"),
("GET", "/api/monitor", "api monitor"),
("GET", "/admin/analytics", "analytics"),
("GET", "/landscape", "landscape"),
("GET", "/api/landscape", "api landscape"),
("GET", "/similarity", "similarity"),
("GET", "/api/similarity", "api similarity"),
("GET", "/compare", "compare (empty)"),
("GET", f"/compare?drafts={ids['two_drafts']}", "compare (with drafts)"),
("GET", "/sources", "sources"),
("GET", "/false-positives", "false positives"),
("GET", "/api/sources", "api sources"),
("GET", "/api/false-positives", "api false positives"),
("GET", "/api/citations/influence", "api citation influence"),
("GET", "/api/citations/bcp", "api bcp analysis"),
("GET", "/idea-analysis", "idea analysis"),
("GET", "/api/idea-analysis", "api idea analysis"),
("GET", f"/ideas/{ids['idea_id']}", "idea detail"),
("GET", "/trends", "trends"),
("GET", "/complexity", "complexity"),
("GET", "/api/trends", "api trends"),
("GET", "/api/complexity", "api complexity"),
("GET", "/proposals", "proposals"),
("GET", "/proposals/new", "proposal new (GET)"),
("GET", "/api/proposals", "api proposals"),
("GET", "/proposals/intake", "proposal intake (GET)"),
("GET", "/blog", "blog drafts"),
("GET", "/blog/generate", "blog generate (GET)"),
("GET", "/export/obsidian", "obsidian export"),
]
# Add conditional routes that need existing records
if ids["proposal_id"]:
routes.extend([
("GET", f"/proposals/{ids['proposal_id']}", "proposal detail"),
("GET", f"/proposals/{ids['proposal_id']}/edit", "proposal edit (GET)"),
("GET", f"/api/proposals/{ids['proposal_id']}", "api proposal detail"),
])
if ids["blog_draft_id"]:
routes.extend([
("GET", f"/blog/{ids['blog_draft_id']}", "blog detail"),
("GET", f"/blog/{ids['blog_draft_id']}/edit", "blog edit (GET)"),
("GET", f"/blog/{ids['blog_draft_id']}/export", "blog export"),
])
# Run tests
results = {"ok": [], "error_500": [], "error_other": [], "exception": []}
for method, url, desc in routes:
try:
if method == "GET":
resp = client.get(url)
elif method == "POST":
resp = client.post(url, json={})
else:
continue
status = resp.status_code
if status == 500:
# Try to extract error from response
error_text = resp.data.decode("utf-8", errors="replace")[:500]
results["error_500"].append((desc, url, status, error_text))
print(f" FAIL 500 {desc:30s} {url}")
elif status >= 400:
results["error_other"].append((desc, url, status))
print(f" WARN {status} {desc:30s} {url}")
else:
results["ok"].append((desc, url, status))
print(f" OK {status} {desc:30s} {url}")
except Exception as e:
tb = traceback.format_exc()
results["exception"].append((desc, url, str(e), tb))
print(f" EXCP {desc:30s} {url} -> {e}")
# Summary
print("\n" + "=" * 70)
print(f"SUMMARY: {len(results['ok'])} OK, {len(results['error_500'])} x 500, "
f"{len(results['error_other'])} x 4xx, {len(results['exception'])} exceptions")
print("=" * 70)
if results["error_500"]:
print("\n--- 500 ERRORS (BROKEN ROUTES) ---")
for desc, url, status, error_text in results["error_500"]:
print(f"\n Route: {desc}")
print(f" URL: {url}")
print(f" Error preview:")
# Show just enough of the error
for line in error_text.split("\n")[:10]:
print(f" {line}")
if results["exception"]:
print("\n--- EXCEPTIONS (APP CRASHED) ---")
for desc, url, err, tb in results["exception"]:
print(f"\n Route: {desc}")
print(f" URL: {url}")
print(f" Exception: {err}")
# Show last few lines of traceback
tb_lines = tb.strip().split("\n")
for line in tb_lines[-5:]:
print(f" {line}")
if results["error_other"]:
print("\n--- 4xx RESPONSES (may be expected) ---")
for desc, url, status in results["error_other"]:
print(f" {status} {desc:30s} {url}")
if __name__ == "__main__":
main()