From 82c405fd4436675a2fed20fc687705e7e4b09611 Mon Sep 17 00:00:00 2001 From: Guan Tong Date: Sun, 2 Aug 2026 02:17:43 +0800 Subject: [PATCH] feat: expose manager playground route --- Makefile | 4 +- src/agent_manager/api/web.py | 4 +- src/agent_manager/cli.py | 15 ++++++- tests/agent_manager/test_cli.py | 77 +++++++++++++++++++++++++++++++++ tests/agent_manager/test_web.py | 8 +++- 5 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 tests/agent_manager/test_cli.py diff --git a/Makefile b/Makefile index fd9f9a7a..522b0323 100644 --- a/Makefile +++ b/Makefile @@ -83,9 +83,9 @@ up: validate-image $(EXAMPLE)/.env ## Run the example in engine mode: API on htt $(COMPOSE) --profile engine up -d @echo "engine: http://localhost:8090/health" -up-ui: validate-image $(EXAMPLE)/.env ## Run the example in manager mode: chat UI on http://localhost:8100/demo +up-ui: validate-image $(EXAMPLE)/.env ## Run the example in manager mode: playground on http://localhost:8100/playground $(COMPOSE) --profile ui up -d - @echo "chat UI: http://localhost:8100/demo" + @echo "playground: http://localhost:8100/playground" down: ## Stop the example stack. $(COMPOSE) --profile engine --profile ui down diff --git a/src/agent_manager/api/web.py b/src/agent_manager/api/web.py index b5b6f5cc..6e8aea83 100644 --- a/src/agent_manager/api/web.py +++ b/src/agent_manager/api/web.py @@ -29,8 +29,8 @@ def mount_web(app: FastAPI, settings: Settings) -> None: def widget_js() -> FileResponse: return FileResponse(STATIC_DIR / "widget.js", media_type="application/javascript") - @app.get("/demo") - def demo() -> FileResponse: + @app.get("/playground") + def playground() -> FileResponse: return FileResponse(STATIC_DIR / "demo.html", media_type="text/html") @app.get("/widget-demo.html") diff --git a/src/agent_manager/cli.py b/src/agent_manager/cli.py index 13094bc8..88e3f582 100644 --- a/src/agent_manager/cli.py +++ b/src/agent_manager/cli.py @@ -12,6 +12,16 @@ from dotenv import load_dotenv +def _playground_url(host: str, port: int) -> str: + if host in {"0.0.0.0", "::"}: + display_host = "localhost" + elif ":" in host and not host.startswith("["): + display_host = f"[{host}]" + else: + display_host = host + return f"http://{display_host}:{port}/playground" + + @click.command() @click.option("--config", required=True, help="Path to agents.yml") @click.option("--host", default=None, help="Host to bind to (overrides settings)") @@ -35,7 +45,10 @@ def main(config: str, host: str | None, port: int | None, env: str | None, migra upgrade_database() app = create_app(config, settings) - uvicorn.run(app, host=host or settings.host, port=port or settings.port) + bind_host = host or settings.host + bind_port = port or settings.port + click.echo(f"Playground: {_playground_url(bind_host, bind_port)}") + uvicorn.run(app, host=bind_host, port=bind_port) if __name__ == "__main__": diff --git a/tests/agent_manager/test_cli.py b/tests/agent_manager/test_cli.py new file mode 100644 index 00000000..b7865ad8 --- /dev/null +++ b/tests/agent_manager/test_cli.py @@ -0,0 +1,77 @@ +"""Tests for the ``agent-manager`` startup message and bind options.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest +from click.testing import CliRunner + +from agent_manager.cli import main + + +@pytest.fixture +def captured_run(monkeypatch: pytest.MonkeyPatch) -> dict[str, object]: + import uvicorn + + captured: dict[str, object] = {} + + def fake_create_app(config: str, settings: object) -> object: + captured["config"] = config + captured["settings"] = settings + return object() + + def fake_run(app: object, host: str, port: int) -> None: + captured["app"] = app + captured["host"] = host + captured["port"] = port + + monkeypatch.setattr("agent_manager.cli.load_dotenv", lambda path, override: None) + monkeypatch.setattr("agent_manager.api.create_app", fake_create_app) + monkeypatch.setattr( + "agent_manager.config.Settings", + lambda: SimpleNamespace(host="0.0.0.0", port=8100), + ) + monkeypatch.setattr(uvicorn, "run", fake_run) + return captured + + +def test_startup_prints_clickable_playground_url(captured_run: dict[str, object]) -> None: + result = CliRunner().invoke(main, ["--config", "agents.yml", "--no-migrate"]) + + assert result.exit_code == 0, result.output + assert "Playground: http://localhost:8100/playground" in result.output + assert captured_run["host"] == "0.0.0.0" + assert captured_run["port"] == 8100 + + +def test_startup_url_uses_host_and_port_overrides(captured_run: dict[str, object]) -> None: + result = CliRunner().invoke( + main, + [ + "--config", + "agents.yml", + "--host", + "127.0.0.1", + "--port", + "8200", + "--no-migrate", + ], + ) + + assert result.exit_code == 0, result.output + assert "Playground: http://127.0.0.1:8200/playground" in result.output + assert captured_run["host"] == "127.0.0.1" + assert captured_run["port"] == 8200 + + +def test_startup_url_formats_ipv6_host(captured_run: dict[str, object]) -> None: + result = CliRunner().invoke( + main, + ["--config", "agents.yml", "--host", "::1", "--no-migrate"], + ) + + assert result.exit_code == 0, result.output + assert "Playground: http://[::1]:8100/playground" in result.output + assert captured_run["host"] == "::1" + assert captured_run["port"] == 8100 diff --git a/tests/agent_manager/test_web.py b/tests/agent_manager/test_web.py index d44aaa45..02699c6e 100644 --- a/tests/agent_manager/test_web.py +++ b/tests/agent_manager/test_web.py @@ -24,13 +24,17 @@ def test_widget_js_served_as_javascript(client: TestClient) -> None: assert "agent-chat" in r.text -def test_demo_page_served_as_html(client: TestClient) -> None: - r = client.get("/demo") +def test_playground_page_served_as_html(client: TestClient) -> None: + r = client.get("/playground") assert r.status_code == 200 assert "text/html" in r.headers["content-type"] assert " None: + assert client.get("/demo").status_code == 404 + + @pytest.mark.parametrize( "path", [