Introduce a single nav-config module (webui/nav.py) driving grouped navigation across the epic #631 Phase 1 information architecture: Health, Traffic, Runtime/Sessions, Projects, Inventory, Timeline, Policy (placeholder), and Insights (placeholder). The shell header now carries a read-only environment badge (local/remote from WEBUI_HOST), a mode: read-only badge, and a Docs link. Home summarizes the console purpose, lists the Phase 1 surfaces by group, and links the MVP legacy pages. Not-yet-implemented surfaces (/sessions, /inventory, /timeline, /policy, /insights) resolve to graceful read-only stub pages rather than 404s; their backing views land in later child issues of #631 (inventory surfaces backed by #636). Mutating methods on stub routes still fail closed with read-only-mvp. No privileged action controls are added. Adds tests/test_webui_shell.py covering nav groups, badge presence, environment classification, docs link, stub routes (200 + read-only), and home content. Updates docs/webui-local-dev.md with the new routes and a Phase 1 shell section. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
136 lines
4.4 KiB
Python
136 lines
4.4 KiB
Python
"""Tests for the Phase 1 operator console application shell (#638)."""
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from starlette.routing import Route
|
|
from starlette.testclient import TestClient
|
|
|
|
from webui import layout
|
|
from webui.app import create_app
|
|
from webui.nav import NAV_GROUPS, STUB_PAGES, nav_hrefs
|
|
|
|
|
|
class TestShellNav(unittest.TestCase):
|
|
def setUp(self):
|
|
self.client = TestClient(create_app())
|
|
|
|
def test_nav_group_labels_present(self):
|
|
text = self.client.get("/").text
|
|
for group in NAV_GROUPS:
|
|
with self.subTest(group=group.label):
|
|
self.assertIn(f">{group.label}<", text)
|
|
|
|
def test_phase1_group_labels_cover_expected_ia(self):
|
|
labels = {group.label for group in NAV_GROUPS}
|
|
for expected in (
|
|
"Health",
|
|
"Traffic",
|
|
"Runtime/Sessions",
|
|
"Projects",
|
|
"Inventory",
|
|
"Timeline",
|
|
"Policy",
|
|
"Insights",
|
|
):
|
|
with self.subTest(label=expected):
|
|
self.assertIn(expected, labels)
|
|
|
|
def test_every_nav_href_resolves_to_a_get_route(self):
|
|
app = create_app()
|
|
get_paths = {
|
|
route.path
|
|
for route in app.routes
|
|
if isinstance(route, Route) and "GET" in route.methods
|
|
}
|
|
for href in nav_hrefs():
|
|
with self.subTest(href=href):
|
|
self.assertIn(href, get_paths, f"nav href {href} has no GET route")
|
|
|
|
def test_legacy_hrefs_still_navigable(self):
|
|
text = self.client.get("/").text
|
|
for href in ("/queue", "/projects", "/prompts", "/runtime",
|
|
"/audit", "/worktrees", "/leases", "/actions"):
|
|
with self.subTest(href=href):
|
|
self.assertIn(f'href="{href}"', text)
|
|
|
|
|
|
class TestShellBadges(unittest.TestCase):
|
|
def setUp(self):
|
|
self.client = TestClient(create_app())
|
|
|
|
def test_mode_badge_present(self):
|
|
self.assertIn("mode: read-only", self.client.get("/").text)
|
|
|
|
def test_environment_badge_present(self):
|
|
self.assertIn("env:", self.client.get("/").text)
|
|
|
|
def test_default_environment_is_local(self):
|
|
self.assertEqual(layout.environment_label(), "local")
|
|
|
|
def test_remote_bind_reports_remote_environment(self):
|
|
import os
|
|
|
|
prior = os.environ.get("WEBUI_HOST")
|
|
os.environ["WEBUI_HOST"] = "10.0.0.5"
|
|
try:
|
|
self.assertEqual(layout.environment_label(), "remote")
|
|
finally:
|
|
if prior is None:
|
|
os.environ.pop("WEBUI_HOST", None)
|
|
else:
|
|
os.environ["WEBUI_HOST"] = prior
|
|
|
|
def test_docs_link_present(self):
|
|
text = self.client.get("/").text
|
|
self.assertIn(layout.DOCS_URL, text)
|
|
self.assertIn(">Docs<", text)
|
|
|
|
|
|
class TestShellStubs(unittest.TestCase):
|
|
def setUp(self):
|
|
self.client = TestClient(create_app())
|
|
|
|
def test_stub_routes_render_200(self):
|
|
for path, (title, _desc) in STUB_PAGES.items():
|
|
with self.subTest(path=path):
|
|
response = self.client.get(path)
|
|
self.assertEqual(response.status_code, 200, path)
|
|
self.assertIn(title, response.text)
|
|
self.assertIn("placeholder", response.text)
|
|
|
|
def test_stub_routes_are_read_only(self):
|
|
for path in STUB_PAGES:
|
|
with self.subTest(path=path):
|
|
response = self.client.post(path)
|
|
self.assertEqual(response.status_code, 405)
|
|
self.assertEqual(response.json()["error"], "read-only-mvp")
|
|
|
|
def test_stub_pages_carry_nav_and_badges(self):
|
|
response = self.client.get("/inventory")
|
|
self.assertIn("mode: read-only", response.text)
|
|
self.assertIn('href="/queue"', response.text)
|
|
|
|
|
|
class TestShellHome(unittest.TestCase):
|
|
def setUp(self):
|
|
self.client = TestClient(create_app())
|
|
|
|
def test_home_summarizes_console(self):
|
|
text = self.client.get("/").text
|
|
self.assertIn("Operator console", text)
|
|
self.assertIn("Phase 1", text)
|
|
|
|
def test_home_links_legacy_pages(self):
|
|
text = self.client.get("/").text
|
|
self.assertIn("MVP legacy pages", text)
|
|
for href in ("/queue", "/audit", "/leases"):
|
|
with self.subTest(href=href):
|
|
self.assertIn(f'href="{href}"', text)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|