feat(webui): test coverage and CI gate (#436)
Add consolidated MVP route/read-only tests, path-filter helpers, and scripts/test-webui plus scripts/ci-webui-check for Jenkins path-filtered runs on PRs touching web UI surfaces. Closes #436
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
"""Tests for web UI CI path-filter gate (#436)."""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from webui.ci_paths import should_run_webui_ci, touches_webui_surface
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class TestCiPathMatching(unittest.TestCase):
|
||||
def test_positive_paths(self):
|
||||
for path in (
|
||||
"webui/app.py",
|
||||
"tests/test_webui_skeleton.py",
|
||||
"docs/webui-local-dev.md",
|
||||
"scripts/test-webui",
|
||||
):
|
||||
with self.subTest(path=path):
|
||||
self.assertTrue(touches_webui_surface(path))
|
||||
|
||||
def test_negative_paths(self):
|
||||
for path in ("mcp_server.py", "tests/test_mcp_server.py", "README.md"):
|
||||
with self.subTest(path=path):
|
||||
self.assertFalse(touches_webui_surface(path))
|
||||
|
||||
def test_should_run_aggregate(self):
|
||||
self.assertTrue(should_run_webui_ci(["README.md", "webui/layout.py"]))
|
||||
self.assertFalse(should_run_webui_ci(["mcp_server.py", "gitea_auth.py"]))
|
||||
|
||||
|
||||
class TestCiRunnerScript(unittest.TestCase):
|
||||
def test_test_webui_smoke(self):
|
||||
script = _REPO_ROOT / "scripts" / "test-webui"
|
||||
result = subprocess.run(
|
||||
["bash", str(script), "-q"],
|
||||
cwd=_REPO_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
env={
|
||||
**os.environ,
|
||||
"WEBUI_TEST_PATTERN": "test_webui_skeleton.py",
|
||||
},
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr or result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,156 @@
|
||||
"""Consolidated MVP coverage for the internal web UI (#436)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
from starlette.routing import Route
|
||||
|
||||
from webui.app import create_app
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
# HTML pages and JSON exports registered on master MVP.
|
||||
MVP_GET_ROUTES: tuple[tuple[str, str | tuple[str, ...]], ...] = (
|
||||
("/", "Operator console"),
|
||||
("/health", "mcp-control-plane-webui"),
|
||||
("/queue", "Live queue"),
|
||||
("/api/queue", ("prs", "issues")),
|
||||
("/projects", "Gitea-Tools"),
|
||||
("/api/projects", "projects"),
|
||||
("/prompts", "Prompt library"),
|
||||
("/api/prompts", "prompts"),
|
||||
("/runtime", "Runtime"),
|
||||
("/audit", "Audit"),
|
||||
("/worktrees", "Worktrees"),
|
||||
("/leases", "Leases"),
|
||||
)
|
||||
|
||||
MUTATION_METHODS = ("POST", "PUT", "PATCH", "DELETE")
|
||||
|
||||
|
||||
def _import_optional(module_name: str):
|
||||
try:
|
||||
return importlib.import_module(module_name)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
class TestWebuiServerStartup(unittest.TestCase):
|
||||
def test_create_app_imports_cleanly(self):
|
||||
app = create_app()
|
||||
self.assertIsNotNone(app)
|
||||
|
||||
def test_main_module_imports(self):
|
||||
import webui.__main__ as main_mod
|
||||
|
||||
self.assertTrue(callable(main_mod.main))
|
||||
|
||||
|
||||
class TestWebuiRouteRendering(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = TestClient(create_app())
|
||||
|
||||
def test_all_mvp_get_routes_render(self):
|
||||
for path, needle in MVP_GET_ROUTES:
|
||||
with self.subTest(path=path):
|
||||
response = self.client.get(path)
|
||||
self.assertEqual(response.status_code, 200, path)
|
||||
if path == "/health":
|
||||
assert isinstance(needle, str)
|
||||
self.assertEqual(response.json()["service"], needle)
|
||||
elif path.startswith("/api/"):
|
||||
data = response.json()
|
||||
if isinstance(needle, tuple):
|
||||
for key in needle:
|
||||
self.assertIn(key, data)
|
||||
else:
|
||||
self.assertIn(needle, data)
|
||||
else:
|
||||
assert isinstance(needle, str)
|
||||
self.assertIn(needle, response.text)
|
||||
|
||||
def test_registered_routes_include_mvp_paths(self):
|
||||
app = create_app()
|
||||
get_paths = {
|
||||
route.path
|
||||
for route in app.routes
|
||||
if isinstance(route, Route) and "GET" in route.methods
|
||||
}
|
||||
for path, _ in MVP_GET_ROUTES:
|
||||
with self.subTest(path=path):
|
||||
self.assertIn(path, get_paths)
|
||||
|
||||
|
||||
class TestWebuiReadOnlyGuards(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = TestClient(create_app())
|
||||
|
||||
def test_mutation_methods_rejected_on_core_paths(self):
|
||||
paths = ("/health", "/queue", "/projects", "/prompts", "/api/queue")
|
||||
for path in paths:
|
||||
for method in MUTATION_METHODS:
|
||||
with self.subTest(path=path, method=method):
|
||||
response = self.client.request(method, path)
|
||||
self.assertEqual(response.status_code, 405)
|
||||
self.assertEqual(response.json()["error"], "read-only-mvp")
|
||||
|
||||
|
||||
class TestWebuiOptionalModules(unittest.TestCase):
|
||||
"""Exercise child-issue modules when present on the branch under test."""
|
||||
|
||||
def test_audit_validator_basics_when_present(self):
|
||||
mod = _import_optional("webui.audit_validator")
|
||||
if mod is None:
|
||||
self.skipTest("webui.audit_validator not merged on this branch")
|
||||
report = "## Controller Handoff\n- Task: review PR #1\n"
|
||||
self.assertEqual(mod.infer_task_kind(report), "review_pr")
|
||||
result = mod.audit_report(report)
|
||||
self.assertIn("grade", result)
|
||||
|
||||
def test_gated_actions_fail_closed_when_present(self):
|
||||
mod = _import_optional("webui.gated_actions")
|
||||
if mod is None:
|
||||
self.skipTest("webui.gated_actions not merged on this branch")
|
||||
registry = mod.build_action_registry()
|
||||
for action in registry.actions:
|
||||
with self.subTest(action=action.action_id):
|
||||
self.assertFalse(action.enabled)
|
||||
result = mod.attempt_action("merge_pr", pr_number=1)
|
||||
self.assertFalse(result["success"])
|
||||
|
||||
def test_deployment_boundary_when_present(self):
|
||||
mod = _import_optional("webui.deployment_boundary")
|
||||
if mod is None:
|
||||
self.skipTest("webui.deployment_boundary not merged on this branch")
|
||||
assessment = mod.assess_bind_host("0.0.0.0")
|
||||
self.assertEqual(assessment.disposition, "refuse")
|
||||
|
||||
|
||||
class TestWebuiTestInventory(unittest.TestCase):
|
||||
def test_webui_test_modules_exist(self):
|
||||
modules = sorted(_REPO_ROOT.glob("tests/test_webui_*.py"))
|
||||
names = [path.name for path in modules]
|
||||
self.assertIn("test_webui_skeleton.py", names)
|
||||
self.assertIn("test_webui_project_registry.py", names)
|
||||
self.assertIn("test_webui_prompt_library.py", names)
|
||||
self.assertIn("test_webui_queue_dashboard.py", names)
|
||||
self.assertGreaterEqual(len(names), 5)
|
||||
|
||||
|
||||
class TestWebuiCiScripts(unittest.TestCase):
|
||||
def test_runner_scripts_exist(self):
|
||||
for name in ("test-webui", "ci-webui-check"):
|
||||
script = _REPO_ROOT / "scripts" / name
|
||||
self.assertTrue(script.is_file(), name)
|
||||
self.assertTrue(os.access(script, os.X_OK), name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user