headroom/tests/test_cli_dashboard.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

47 lines
1.5 KiB
Python
Raw Permalink Normal View History

feat(cli): add `headroom dashboard` and surface the dashboard URL (#1277) (#1292) ## Description The savings dashboard is served at `GET /dashboard` (`headroom/proxy/server.py`) but was effectively undiscoverable: there was no `headroom dashboard` command, the `wrap` startup banner only printed `Proxy ready on http://127.0.0.1:PORT` (never the dashboard URL), and the docs buried it — so users on current releases didn't know it existed (#1277). This makes it discoverable from the CLI, the wrap banner, and the docs. Closes #1277 ## Type of Change - [x] New feature (non-breaking change that adds functionality) ## Changes Made - `headroom/cli/proxy.py`: new `headroom dashboard` command — prints `http://127.0.0.1:<port>/dashboard` and opens it in a browser (stdlib `webbrowser`); `--no-open` just prints, `--port`/`HEADROOM_PORT` honored. Headless failures are swallowed (URL already printed). - `headroom/cli/wrap.py`: print the dashboard URL alongside "Proxy ready" so every `wrap` surfaces it. - `docs/content/docs/installation.mdx` + `README.md`: document `headroom dashboard`. - `docs/content/docs/mcp.mdx`: document the Codex MCP `command: "headroom"` PATH pitfall (#768) — a project-venv (`uv add`) install isn't on the host's PATH; install globally with `uv tool install` / pipx, or use an absolute path. - `tests/test_cli_dashboard.py`: new tests. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] New tests added for new functionality ### Test Output ```text $ python -m pytest tests/test_cli_dashboard.py -q 3 passed $ python -m ruff check headroom/cli/proxy.py headroom/cli/wrap.py tests/test_cli_dashboard.py All checks passed! ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.13, branch fix/1277-dashboard-discoverability off headroomlabs-ai/main - Exact command / steps: built the CLI and invoked the new command via the real entry-point import (`from headroom.cli.main import main; main(['dashboard','--no-open','--port','8787'], standalone_mode=False)`) and checked it is registered (`'dashboard' in main.commands`). - Observed result: prints ` Dashboard: http://127.0.0.1:8787/dashboard`, `'dashboard' in main.commands` → `True`, exit 0. The three new tests pass (prints URL + no browser on `--no-open`; opens the URL by default; a raising `webbrowser.open` does not crash the command). - Not tested: did not load the rendered `/dashboard` HTML against a live proxy in CI — the change only adds a launcher/printer for the existing route; the route itself is unchanged. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 02:05:38 +02:00
"""Tests for the `headroom dashboard` command (#1277)."""
from __future__ import annotations
import webbrowser
from click.testing import CliRunner
from headroom.cli.main import main
def test_dashboard_no_open_prints_url(monkeypatch):
"""--no-open prints the dashboard URL and never opens a browser."""
opened: list[str] = []
monkeypatch.setattr(webbrowser, "open", lambda u, *a, **k: opened.append(u) or True)
result = CliRunner().invoke(main, ["dashboard", "--no-open", "--port", "9999"])
assert result.exit_code == 0, result.output
assert "http://127.0.0.1:9999/dashboard" in result.output
assert opened == [] # browser must not be launched
def test_dashboard_opens_browser_by_default(monkeypatch):
"""Without --no-open the command opens the URL in a browser."""
opened: list[str] = []
monkeypatch.setattr(webbrowser, "open", lambda u, *a, **k: opened.append(u) or True)
result = CliRunner().invoke(main, ["dashboard", "--port", "1234"])
assert result.exit_code == 0, result.output
assert opened == ["http://127.0.0.1:1234/dashboard"]
def test_dashboard_browser_failure_is_swallowed(monkeypatch):
"""A headless box where webbrowser.open raises must not crash the command."""
def _boom(*_a, **_k):
raise RuntimeError("no display")
monkeypatch.setattr(webbrowser, "open", _boom)
result = CliRunner().invoke(main, ["dashboard"])
assert result.exit_code == 0, result.output
assert "/dashboard" in result.output