diff --git a/.gitignore b/.gitignore index 6a183f7..3aa60d8 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ Thumbs.db *.bak .pytest_cache/ pytest-cache-files-*/ +.gstack/ diff --git a/CURRENT_STATE.md b/CURRENT_STATE.md index b626882..0400255 100644 --- a/CURRENT_STATE.md +++ b/CURRENT_STATE.md @@ -2,7 +2,7 @@ Audit date: 2026-05-19 -This repo is a manifest-driven static atlas of plot pages, generated homepage content, generated README inventory text, a shared dashboard, and Python-based plot generators. The manifest is the inventory source of truth; data and metadata live beside each plot; generated outputs live under each plot's `output/` directory. +This repo is a manifest-driven static atlas of plot pages, generated homepage content, generated README inventory text, a shared dashboard, a browser smoke harness, and Python-based plot generators. The manifest is the inventory source of truth; data and metadata live beside each plot; generated outputs live under each plot's `output/` directory. ## Confirmed Working Pieces @@ -12,13 +12,14 @@ This repo is a manifest-driven static atlas of plot pages, generated homepage co - The repo-level validator exists at `scripts/validate_repo.py`. - The test suite includes bootstrap smoke checks for the build and validator entrypoints. - The shared accessibility and link-check scripts are present. +- The browser smoke harness exists at `scripts/browser_smoke.py`. ## Commands Status below reflects the current local verification pass. - `python -m pip install -r requirements.txt` - passed -- `python build_all.py` - passed +- `uv run --with numpy --with pandas --with matplotlib --with plotly --with scipy python build_all.py` - passed and refreshed generated outputs - `python scripts/generate_homepage.py` - passed - `python scripts/generate_readme_links.py` - passed - `python scripts/generate_sitemap.py` - passed @@ -26,6 +27,8 @@ Status below reflects the current local verification pass. - `python scripts/validate_repo.py --check` - passed - `python scripts/check_links.py` - passed - `python scripts/check_accessibility_static.py` - passed +- `python scripts/browser_smoke.py` - passed for homepage, dashboard, and AI Compute Timeline +- Browser QA (desktop + mobile screenshots in `.gstack/qa-reports/screenshots/`) passed on homepage, dashboard, and AI Compute Timeline with no console errors - `python -m pytest tests -q` - passed ## Important Files and Directories @@ -59,14 +62,15 @@ Status below reflects the current local verification pass. ## Known Risks -- The dashboard loads D3 from a CDN. +- The dashboard loads D3 from a CDN, so the browser smoke harness is a preflight rather than an offline-safe guarantee. - Some plot rows remain speculative or projection-based and should not be reworded into facts without source review. - Generated outputs need to be rebuilt after data or source edits to stay fresh. - The repo depends on the Python packages listed in `requirements.txt`. ## Immediate Next Moves -1. Follow `docs/agentic-overhaul/two-prompt-buildout-plan.md` for the next feature branch. -2. Run `python build_all.py` when changing data or plot generators. -3. Run `python scripts/validate_repo.py --check` after any substantive change. -4. Keep `CURRENT_STATE.md` and `docs/agentic-overhaul/2026-05-audit.md` up to date when the repo shape changes. +1. Use `python scripts/browser_smoke.py --html /tmp/browser-smoke/index.html` before browser QA for the homepage, dashboard, and AI Compute Timeline. +2. Follow `docs/agentic-overhaul/two-prompt-buildout-plan.md` for the next feature branch. +3. Run `python build_all.py` when changing data or plot generators. +4. Run `python scripts/validate_repo.py --check` after any substantive change. +5. Keep `CURRENT_STATE.md` and `docs/agentic-overhaul/2026-05-audit.md` up to date when the repo shape changes. diff --git a/docs/agentic-overhaul/2026-05-audit.md b/docs/agentic-overhaul/2026-05-audit.md index 8007427..d26d266 100644 --- a/docs/agentic-overhaul/2026-05-audit.md +++ b/docs/agentic-overhaul/2026-05-audit.md @@ -37,7 +37,7 @@ Scores are subjective and reflect this audit pass. - Decide whether to prune or summarize more of the archival task briefs under `.agent-tasks/`. - Tighten provenance notes for the most speculative plot rows where source review is still needed. -- Consider a browser-level smoke test for the dashboard and one representative plot page. +- Browser smoke harness now covers the dashboard, homepage, and one representative plot page; keep the target list aligned with the manifest as pages change. ### P2 diff --git a/docs/agentic-overhaul/two-prompt-buildout-plan.md b/docs/agentic-overhaul/two-prompt-buildout-plan.md index 47cb5dc..34a4f7b 100644 --- a/docs/agentic-overhaul/two-prompt-buildout-plan.md +++ b/docs/agentic-overhaul/two-prompt-buildout-plan.md @@ -2,6 +2,8 @@ This repo is already in the self-orienting, self-validating state. The remaining work should be done in small, independently mergeable features that fit a two-prompt loop: +Completed in this branch: item 1, the browser smoke harness for homepage + dashboard + one representative plot. The next unfinished item is order 2, offline-safe dashboard loading. + 1. Build prompt: create a feature branch, implement one feature, add or update tests, update docs, commit, and push. 2. QA prompt: open the branch in the browser, take screenshots, verify behavior, update docs if needed, commit, push, and merge. diff --git a/requirements.txt b/requirements.txt index c933634..72bd896 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,8 @@ matplotlib>=3.5.0 plotly>=5.0.0 scipy>=1.7.0 +# Test dependency for the repo validation workflow and local checks +pytest>=9.0.0 + # Optional: for static image export from Plotly # kaleido>=0.2.0 diff --git a/scripts/browser_smoke.py b/scripts/browser_smoke.py new file mode 100644 index 0000000..fb19a85 --- /dev/null +++ b/scripts/browser_smoke.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""Browser smoke harness for the homepage, dashboard, and one representative plot. + +The script is intentionally stdlib-only so the repo has a non-UI pass/fail signal +without depending on Playwright or another browser automation runtime. + +It validates the canonical smoke targets, prints a concise terminal report, and +can optionally write a self-contained HTML receipt for browser QA. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from html import escape +from pathlib import Path + +from manifest_utils import ROOT + +SMOKE_VIEWPORTS = [ + {"name": "desktop", "width": 1440, "height": 1200}, + {"name": "mobile", "width": 390, "height": 844}, +] + +SMOKE_TARGETS = [ + { + "name": "homepage", + "title": "Plots homepage", + "path": "index.html", + "checks": [ + 'data-page="home"', + "plots_manifest.json", + "published entries", + ], + }, + { + "name": "dashboard", + "title": "Unified Dashboard", + "path": "dashboard/index.html", + "checks": [ + "dashboard.css", + "dashboard.js", + "dashboard-container", + ], + }, + { + "name": "representative-plot", + "title": "AI Compute Timeline", + "path": "ai-compute-timeline/index.html", + "checks": [ + "output/ai_compute_timeline_interactive.html", + "output/ai_compute_timeline_highres.png", + "output/ai_compute_timeline.svg", + ], + }, +] + + +def _relative_url(root: Path, rel_path: str) -> str: + return (root / rel_path).as_uri() + + +def evaluate_target(root: Path, target: dict) -> dict: + path = root / target["path"] + text = path.read_text(encoding="utf-8", errors="ignore") if path.is_file() else "" + missing = [needle for needle in target["checks"] if needle not in text] + result = { + "name": target["name"], + "title": target["title"], + "path": target["path"], + "url": _relative_url(root, target["path"]), + "file_exists": path.is_file(), + "missing_checks": missing, + "status": "pass" if path.is_file() and not missing else "fail", + "viewports": SMOKE_VIEWPORTS, + } + return result + + +def run_smoke_checks(root: Path = ROOT) -> list[dict]: + return [evaluate_target(root, target) for target in SMOKE_TARGETS] + + +def render_text_report(results: list[dict], root: Path) -> str: + lines = [ + "Browser smoke harness", + f"Root: {root}", + "", + ] + for result in results: + lines.append(f"[{result['status'].upper()}] {result['title']} -> {result['path']}") + if result["missing_checks"]: + for check in result["missing_checks"]: + lines.append(f" - missing: {check}") + viewport_summary = ", ".join( + f"{viewport['name']} {viewport['width']}x{viewport['height']}" + for viewport in result["viewports"] + ) + lines.append(f" - browser viewports: {viewport_summary}") + lines.append("") + lines.append("Pass/fail: " + ("PASS" if all(result["status"] == "pass" for result in results) else "FAIL")) + return "\n".join(lines) + + +def render_html_report(results: list[dict], root: Path) -> str: + rows = [] + for result in results: + missing = ", ".join(result["missing_checks"]) if result["missing_checks"] else "—" + viewports = ", ".join( + f"{vp['name']} {vp['width']}×{vp['height']}" for vp in result["viewports"] + ) + rows.append( + f""" + {escape(result['title'])} + {escape(result['path'])} + {escape(result['status'].upper())} + {escape(viewports)} + {escape(missing)} + """ + ) + + overall = "PASS" if all(result["status"] == "pass" for result in results) else "FAIL" + return f""" + + + + + Browser Smoke Receipt + + + +

Browser smoke receipt {overall}

+

Root: {escape(str(root))}

+

This receipt validates the canonical browser-smoke targets before opening them in a real browser session. Capture desktop and mobile screenshots for the three linked pages after this preflight passes.

+ + + + + + {''.join(rows)} + +
TargetPathStatusViewportsMissing checks
+ + +""" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Validate the browser smoke targets.") + parser.add_argument("--root", type=Path, default=ROOT, help="Repository root to validate.") + parser.add_argument("--html", type=Path, help="Optional path for a browser-openable HTML receipt.") + parser.add_argument("--json", dest="json_path", type=Path, help="Optional path for a JSON receipt.") + args = parser.parse_args(argv) + + results = run_smoke_checks(args.root) + ok = all(result["status"] == "pass" for result in results) + + print(render_text_report(results, args.root)) + + if args.html: + args.html.parent.mkdir(parents=True, exist_ok=True) + args.html.write_text(render_html_report(results, args.root), encoding="utf-8") + print(f"HTML receipt: {args.html}") + + if args.json_path: + args.json_path.parent.mkdir(parents=True, exist_ok=True) + args.json_path.write_text( + json.dumps({"root": str(args.root), "results": results}, indent=2), + encoding="utf-8", + ) + print(f"JSON receipt: {args.json_path}") + + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validate_repo.py b/scripts/validate_repo.py index be0c9b6..e71a0e2 100644 --- a/scripts/validate_repo.py +++ b/scripts/validate_repo.py @@ -27,6 +27,7 @@ "plots_manifest.json", "build_all.py", "requirements.txt", + "scripts/browser_smoke.py", "scripts/validate_repo.py", "docs/agentic-overhaul/2026-05-audit.md", ".github/workflows/validate.yml", diff --git a/tests/test_browser_smoke.py b/tests/test_browser_smoke.py new file mode 100644 index 0000000..72d2cf3 --- /dev/null +++ b/tests/test_browser_smoke.py @@ -0,0 +1,56 @@ +"""Tests for the browser smoke harness.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + + +def _load_browser_smoke(): + sys.path.insert(0, "scripts") + import browser_smoke + + return browser_smoke + + +class TestBrowserSmokeTargets: + def test_targets_are_canonical(self): + browser_smoke = _load_browser_smoke() + + names = [target["name"] for target in browser_smoke.SMOKE_TARGETS] + assert names == ["homepage", "dashboard", "representative-plot"] + + paths = [target["path"] for target in browser_smoke.SMOKE_TARGETS] + assert paths == [ + "index.html", + "dashboard/index.html", + "ai-compute-timeline/index.html", + ] + + def test_viewports_are_desktop_and_mobile(self): + browser_smoke = _load_browser_smoke() + + viewport_names = [viewport["name"] for viewport in browser_smoke.SMOKE_VIEWPORTS] + assert viewport_names == ["desktop", "mobile"] + + def test_run_smoke_checks_pass_for_repo_root(self, repo_root: Path): + browser_smoke = _load_browser_smoke() + + results = browser_smoke.run_smoke_checks(repo_root) + assert len(results) == 3 + assert all(result["status"] == "pass" for result in results) + assert all(result["file_exists"] for result in results) + + def test_render_html_report_mentions_targets(self, repo_root: Path): + browser_smoke = _load_browser_smoke() + + results = browser_smoke.run_smoke_checks(repo_root) + html = browser_smoke.render_html_report(results, repo_root) + + assert "Browser smoke receipt" in html + assert "homepage" in html + assert "dashboard/index.html" in html + assert "ai-compute-timeline/index.html" in html + assert "PASS" in html