__init__.py
python
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
1 day ago
| 1 | """Fixture helpers for hosted governance dashboard tests (§HGD.12).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import base64 |
| 6 | import json |
| 7 | import socket |
| 8 | import urllib.error |
| 9 | import urllib.request |
| 10 | from pathlib import Path |
| 11 | from typing import Any, Callable |
| 12 | from urllib.parse import parse_qs, urlparse |
| 13 | |
| 14 | from tools.hosted_dashboard.adapters.github import GitHubAdapters |
| 15 | from tools.hosted_dashboard.auth import generate_viewer_token |
| 16 | from tools.hosted_dashboard.config import HostedDashboardConfig, parse_hosted_dashboard_config |
| 17 | from tools.hosted_dashboard.handlers import DashboardService |
| 18 | from tools.hosted_dashboard.http_client import UpstreamClient, UpstreamResponse |
| 19 | from tools.hosted_dashboard.server import HostedServerConfig, HostedServerHandle, start_hosted_server |
| 20 | |
| 21 | MARKER_YAML = """\ |
| 22 | overseer_config_version: 1 |
| 23 | repo: |
| 24 | name: demo |
| 25 | root_relative_docs: docs |
| 26 | vcs: |
| 27 | regime: git-only |
| 28 | canonical: git |
| 29 | git: |
| 30 | remote: origin |
| 31 | main_branch: main |
| 32 | mirror_branch: null |
| 33 | feature_branch_pattern: "feat/*" |
| 34 | muse: |
| 35 | staging_remote: null |
| 36 | main_branch: null |
| 37 | docs: |
| 38 | handover: OVERSEER-HANDOVER.md |
| 39 | roadmap: ROADMAP.md |
| 40 | coordination: null |
| 41 | standing_decisions: STANDING-DECISIONS.md |
| 42 | handover_title: Handover |
| 43 | roadmap_title: Roadmap |
| 44 | thresholds: |
| 45 | realign_max_commits: 5 |
| 46 | drift_warn_only: true |
| 47 | freeze_contract: |
| 48 | enabled: true |
| 49 | reviewer: |
| 50 | mode: agent |
| 51 | model: thinking-high |
| 52 | provider: local |
| 53 | fallback: human |
| 54 | human_escalation: [security, irreversible, real_money, gates_tier3] |
| 55 | """ |
| 56 | |
| 57 | ROADMAP_MD = """\ |
| 58 | # Roadmap fixture |
| 59 | |
| 60 | | Phase | Model | Status | Notes | |
| 61 | | --- | --- | --- | --- | |
| 62 | | **Alpha** | Auto | **DONE** | shipped | |
| 63 | | **Beta** | Thinking | **WIP** | in progress | |
| 64 | | **Gamma** | Auto | **TODO** | pending | |
| 65 | """ |
| 66 | |
| 67 | HANDOVER_MD = """\ |
| 68 | # Handover fixture |
| 69 | |
| 70 | ## Pending gates |
| 71 | |
| 72 | - freeze-review for Gamma still open |
| 73 | - build-verification not yet run |
| 74 | |
| 75 | ## Snapshot |
| 76 | ok |
| 77 | """ |
| 78 | |
| 79 | |
| 80 | def free_port() -> int: |
| 81 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: |
| 82 | sock.bind(("127.0.0.1", 0)) |
| 83 | return int(sock.getsockname()[1]) |
| 84 | |
| 85 | |
| 86 | class FixtureUpstream: |
| 87 | """In-memory GitHub Contents-shaped fixture (no real network).""" |
| 88 | |
| 89 | def __init__(self) -> None: |
| 90 | self.files: dict[tuple[str, str, str, str], bytes] = {} |
| 91 | self.repos: dict[tuple[str, str], dict[str, Any]] = {} |
| 92 | self.org_repos: dict[str, list[dict[str, Any]]] = {} |
| 93 | self.calls: list[tuple[str, str]] = [] |
| 94 | self.force_status: dict[str, int] = {} |
| 95 | |
| 96 | def put_repo(self, owner: str, name: str, *, default_branch: str = "main", private: bool = False) -> None: |
| 97 | self.repos[(owner, name)] = { |
| 98 | "name": name, |
| 99 | "full_name": f"{owner}/{name}", |
| 100 | "default_branch": default_branch, |
| 101 | "private": private, |
| 102 | "owner": {"login": owner}, |
| 103 | } |
| 104 | self.org_repos.setdefault(owner, []) |
| 105 | entry = self.repos[(owner, name)] |
| 106 | if entry not in self.org_repos[owner]: |
| 107 | self.org_repos[owner].append(entry) |
| 108 | |
| 109 | def put_file(self, owner: str, repo: str, path: str, content: str | bytes, *, ref: str = "main") -> None: |
| 110 | raw = content.encode("utf-8") if isinstance(content, str) else content |
| 111 | self.files[(owner, repo, ref, path)] = raw |
| 112 | |
| 113 | def seed_kit_repo(self, owner: str = "acme", repo: str = "kit-demo") -> None: |
| 114 | self.put_repo(owner, repo) |
| 115 | self.put_file(owner, repo, ".overseer/config.yaml", MARKER_YAML) |
| 116 | self.put_file(owner, repo, "docs/ROADMAP.md", ROADMAP_MD) |
| 117 | self.put_file(owner, repo, "docs/OVERSEER-HANDOVER.md", HANDOVER_MD) |
| 118 | |
| 119 | def transport(self, method: str, url: str, headers: dict[str, str], timeout: float) -> UpstreamResponse: |
| 120 | self.calls.append((method, url)) |
| 121 | if method not in {"GET", "HEAD"}: |
| 122 | raise AssertionError(f"write verb attempted: {method}") |
| 123 | parsed = urlparse(url) |
| 124 | host = parsed.hostname or "" |
| 125 | if host not in {"api.github.com", "raw.githubusercontent.com"}: |
| 126 | # Host allowlist in client should have refused before transport; if not, 403 token path. |
| 127 | return UpstreamResponse(status=403, headers={}, body=b"host refused") |
| 128 | |
| 129 | path = parsed.path |
| 130 | qs = parse_qs(parsed.query) |
| 131 | |
| 132 | # Force status override by path prefix |
| 133 | for prefix, status in self.force_status.items(): |
| 134 | if prefix in url: |
| 135 | return UpstreamResponse(status=status, headers={}, body=b"forced") |
| 136 | |
| 137 | # /repos/{owner}/{repo} |
| 138 | parts = [p for p in path.split("/") if p] |
| 139 | if len(parts) == 3 and parts[0] == "repos": |
| 140 | owner, repo = parts[1], parts[2] |
| 141 | meta = self.repos.get((owner, repo)) |
| 142 | if meta is None: |
| 143 | return UpstreamResponse(status=404, headers={}, body=b"{}") |
| 144 | body = json.dumps(meta).encode("utf-8") |
| 145 | return UpstreamResponse(status=200, headers={"content-type": "application/json"}, body=body) |
| 146 | |
| 147 | # /repos/{o}/{r}/contents/{path} |
| 148 | if len(parts) >= 4 and parts[0] == "repos" and parts[3] == "contents": |
| 149 | owner, repo = parts[1], parts[2] |
| 150 | file_path = "/".join(parts[4:]) |
| 151 | ref = (qs.get("ref") or ["main"])[0] |
| 152 | raw = self.files.get((owner, repo, ref, file_path)) |
| 153 | if raw is None: |
| 154 | return UpstreamResponse(status=404, headers={}, body=b"{}") |
| 155 | accept = headers.get("Accept", "") |
| 156 | if "raw" in accept: |
| 157 | return UpstreamResponse(status=200, headers={}, body=raw if method == "GET" else b"") |
| 158 | payload = { |
| 159 | "encoding": "base64", |
| 160 | "content": base64.b64encode(raw).decode("ascii"), |
| 161 | "path": file_path, |
| 162 | } |
| 163 | return UpstreamResponse( |
| 164 | status=200, |
| 165 | headers={"content-type": "application/json"}, |
| 166 | body=json.dumps(payload).encode("utf-8") if method == "GET" else b"", |
| 167 | ) |
| 168 | |
| 169 | # /orgs/{owner}/repos or /users/{owner}/repos |
| 170 | if len(parts) == 3 and parts[0] in {"orgs", "users"} and parts[2] == "repos": |
| 171 | owner = parts[1] |
| 172 | data = self.org_repos.get(owner, []) |
| 173 | return UpstreamResponse( |
| 174 | status=200, |
| 175 | headers={"content-type": "application/json"}, |
| 176 | body=json.dumps(data).encode("utf-8"), |
| 177 | ) |
| 178 | |
| 179 | # check-runs |
| 180 | if len(parts) >= 5 and parts[3] == "commits" and parts[-1] == "check-runs": |
| 181 | payload = { |
| 182 | "check_runs": [ |
| 183 | {"name": "ci", "conclusion": "success"}, |
| 184 | ] |
| 185 | } |
| 186 | return UpstreamResponse( |
| 187 | status=200, |
| 188 | headers={"content-type": "application/json"}, |
| 189 | body=json.dumps(payload).encode("utf-8"), |
| 190 | ) |
| 191 | |
| 192 | return UpstreamResponse(status=404, headers={}, body=b"{}") |
| 193 | |
| 194 | |
| 195 | class HostedHttpClient: |
| 196 | """Minimal HTTP client for hosted dashboard tests.""" |
| 197 | |
| 198 | def __init__(self, base_url: str, viewer_token: str) -> None: |
| 199 | self.base_url = base_url.rstrip("/") |
| 200 | self.viewer_token = viewer_token |
| 201 | |
| 202 | def request( |
| 203 | self, |
| 204 | method: str, |
| 205 | path: str, |
| 206 | *, |
| 207 | headers: dict[str, str] | None = None, |
| 208 | origin: str | None = None, |
| 209 | auth: bool = True, |
| 210 | ) -> tuple[int, dict[str, Any] | str]: |
| 211 | url = f"{self.base_url}{path}" |
| 212 | req_headers = dict(headers or {}) |
| 213 | if auth: |
| 214 | req_headers.setdefault("Authorization", f"Bearer {self.viewer_token}") |
| 215 | if origin is not None: |
| 216 | req_headers["Origin"] = origin |
| 217 | request = urllib.request.Request(url, method=method, headers=req_headers) |
| 218 | try: |
| 219 | with urllib.request.urlopen(request, timeout=10) as response: |
| 220 | raw = response.read().decode("utf-8") |
| 221 | try: |
| 222 | return response.status, json.loads(raw) |
| 223 | except json.JSONDecodeError: |
| 224 | return response.status, raw |
| 225 | except urllib.error.HTTPError as exc: |
| 226 | raw = exc.read().decode("utf-8") |
| 227 | try: |
| 228 | return exc.code, json.loads(raw) |
| 229 | except json.JSONDecodeError: |
| 230 | return exc.code, raw |
| 231 | |
| 232 | def get(self, path: str, **kwargs: Any) -> tuple[int, dict[str, Any] | str]: |
| 233 | return self.request("GET", path, **kwargs) |
| 234 | |
| 235 | |
| 236 | def start_test_hosted( |
| 237 | *, |
| 238 | upstream: FixtureUpstream | None = None, |
| 239 | config: HostedDashboardConfig | None = None, |
| 240 | viewer_token: str | None = None, |
| 241 | port: int | None = None, |
| 242 | write_scope_refused: bool = False, |
| 243 | cors_origins: tuple[str, ...] | None = None, |
| 244 | ) -> tuple[HostedServerHandle, HostedHttpClient, FixtureUpstream]: |
| 245 | """Start hosted dashboard against fixture upstream.""" |
| 246 | fixture = upstream or FixtureUpstream() |
| 247 | if not fixture.repos: |
| 248 | fixture.seed_kit_repo() |
| 249 | |
| 250 | dash = config |
| 251 | if dash is None: |
| 252 | raw = { |
| 253 | "enabled": True, |
| 254 | "allow_non_loopback": False, |
| 255 | "cors_origins": list(cors_origins or ()), |
| 256 | "org_allowlist": ["acme/kit-demo"], |
| 257 | "sources": { |
| 258 | "github_contents": True, |
| 259 | "github_meta": True, |
| 260 | "github_checks_advisory": False, |
| 261 | "musehub_read": False, |
| 262 | }, |
| 263 | } |
| 264 | dash = parse_hosted_dashboard_config(raw) |
| 265 | |
| 266 | token = viewer_token or generate_viewer_token() |
| 267 | client = UpstreamClient(token="test-upstream", transport=fixture.transport) |
| 268 | adapters = GitHubAdapters( |
| 269 | client, |
| 270 | checks_advisory=dash.sources.github_checks_advisory, |
| 271 | enumeration_cap=dash.enumeration_cap, |
| 272 | max_doc_bytes=dash.max_doc_bytes, |
| 273 | ) |
| 274 | service = DashboardService(adapters, dash) |
| 275 | chosen = port or free_port() |
| 276 | server_config = HostedServerConfig( |
| 277 | bind="127.0.0.1", |
| 278 | port=chosen, |
| 279 | viewer_token=token, |
| 280 | upstream_token="test-upstream", |
| 281 | dashboard=dash, |
| 282 | service=service, |
| 283 | require_loopback_peer=True, |
| 284 | write_scope_refused=write_scope_refused, |
| 285 | ) |
| 286 | handle = start_hosted_server(server_config) |
| 287 | http = HostedHttpClient(handle.base_url, token) |
| 288 | return handle, http, fixture |
| 289 | |
| 290 | |
| 291 | def write_hosted_config(path: Path, block: dict[str, Any]) -> Path: |
| 292 | """Write a minimal overseer config with hosted_dashboard block for CLI tests.""" |
| 293 | from tests.support import FIXTURES |
| 294 | import yaml |
| 295 | import shutil |
| 296 | |
| 297 | base = yaml.safe_load((FIXTURES / "config-git-only.yaml").read_text(encoding="utf-8")) |
| 298 | base["hosted_dashboard"] = block |
| 299 | path.parent.mkdir(parents=True, exist_ok=True) |
| 300 | path.write_text(yaml.safe_dump(base, sort_keys=False), encoding="utf-8") |
| 301 | return path |
File History
1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
1 day ago