test_hosted_dashboard_e2e.py
python
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
3 days ago
| 1 | """End-to-end tests for hosted governance dashboard (§HGD.12).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import os |
| 6 | import signal |
| 7 | import subprocess |
| 8 | import sys |
| 9 | import time |
| 10 | from pathlib import Path |
| 11 | |
| 12 | from cli.kit_root import kit_root |
| 13 | from tests.fixtures.hosted_dashboard import ( |
| 14 | HANDOVER_MD, |
| 15 | MARKER_YAML, |
| 16 | ROADMAP_MD, |
| 17 | start_test_hosted, |
| 18 | write_hosted_config, |
| 19 | ) |
| 20 | from tools.hosted_dashboard.server import STATIC_ROOT |
| 21 | |
| 22 | |
| 23 | def test_ui_honesty_banner_present() -> None: |
| 24 | html = (STATIC_ROOT / "index.html").read_text(encoding="utf-8") |
| 25 | assert "Authoritative workflow remains" in html |
| 26 | assert "local" in html.lower() |
| 27 | js = (STATIC_ROOT / "assets" / "dashboard.js").read_text(encoding="utf-8") |
| 28 | assert "localStorage" not in js |
| 29 | assert "sessionStorage" not in js |
| 30 | |
| 31 | |
| 32 | def test_authenticated_flow_org_docs_gates() -> None: |
| 33 | handle, client, _ = start_test_hosted() |
| 34 | try: |
| 35 | status, health = client.get("/api/health", auth=False) |
| 36 | assert status == 200 |
| 37 | |
| 38 | status, summary = client.get("/api/org/summary") |
| 39 | assert status == 200 |
| 40 | assert summary["result"]["repos"] |
| 41 | |
| 42 | for action in ("roadmap", "handover", "gates", "config-marker"): |
| 43 | status, payload = client.get(f"/api/repos/acme/kit-demo/{action}") |
| 44 | assert status == 200 |
| 45 | assert payload["ok"] is True |
| 46 | |
| 47 | # Honesty banner in static UI bytes |
| 48 | status, html = client.get("/", auth=False) |
| 49 | assert status == 200 |
| 50 | assert isinstance(html, str) |
| 51 | assert "Authoritative workflow remains" in html |
| 52 | finally: |
| 53 | handle.shutdown() |
| 54 | |
| 55 | |
| 56 | def test_cli_preview_sigint_exit_zero_no_vcs_mutation(tmp_path: Path, monkeypatch) -> None: |
| 57 | """Start ok hosted-dashboard briefly; SIGINT → exit 0; no .git/.muse mutation.""" |
| 58 | config_path = tmp_path / ".overseer" / "config.yaml" |
| 59 | write_hosted_config( |
| 60 | config_path, |
| 61 | { |
| 62 | "enabled": True, |
| 63 | "org_allowlist": ["acme/kit-demo"], |
| 64 | "sources": { |
| 65 | "github_contents": True, |
| 66 | "github_meta": True, |
| 67 | "github_checks_advisory": False, |
| 68 | "musehub_read": False, |
| 69 | }, |
| 70 | }, |
| 71 | ) |
| 72 | git_dir = tmp_path / ".git" |
| 73 | muse_dir = tmp_path / ".muse" |
| 74 | git_dir.mkdir() |
| 75 | muse_dir.mkdir() |
| 76 | sentinel = git_dir / "HEAD" |
| 77 | sentinel.write_text("ref: refs/heads/main\n", encoding="utf-8") |
| 78 | before = sentinel.read_bytes() |
| 79 | muse_before = {p.name: p.stat().st_mtime_ns for p in muse_dir.iterdir()} if any(muse_dir.iterdir()) else {} |
| 80 | |
| 81 | env = os.environ.copy() |
| 82 | env["OVERSEER_HOSTED_DASHBOARD_VIEWER_TOKEN"] = "a" * 32 |
| 83 | env["PYTHONPATH"] = str(kit_root()) |
| 84 | # Use a free port via ephemeral; pick high port and hope available, or skip listen refuse. |
| 85 | from tests.fixtures.hosted_dashboard import free_port |
| 86 | |
| 87 | port = free_port() |
| 88 | proc = subprocess.Popen( |
| 89 | [ |
| 90 | sys.executable, |
| 91 | "-m", |
| 92 | "cli.main", |
| 93 | "hosted-dashboard", |
| 94 | "--port", |
| 95 | str(port), |
| 96 | "--bind", |
| 97 | "127.0.0.1", |
| 98 | "--config", |
| 99 | str(config_path), |
| 100 | ], |
| 101 | cwd=str(tmp_path), |
| 102 | env=env, |
| 103 | stdout=subprocess.PIPE, |
| 104 | stderr=subprocess.PIPE, |
| 105 | ) |
| 106 | try: |
| 107 | time.sleep(0.6) |
| 108 | proc.send_signal(signal.SIGINT) |
| 109 | code = proc.wait(timeout=5) |
| 110 | # KeyboardInterrupt path returns 0; some platforms may deliver as non-zero — accept 0/-2/130. |
| 111 | assert code in {0, -2, 130} or code == 0 |
| 112 | finally: |
| 113 | if proc.poll() is None: |
| 114 | proc.kill() |
| 115 | proc.wait(timeout=3) |
| 116 | |
| 117 | assert sentinel.read_bytes() == before |
| 118 | assert muse_before == ({p.name: p.stat().st_mtime_ns for p in muse_dir.iterdir()} if any(muse_dir.iterdir()) else {}) |
File History
1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
3 days ago