feat: add web UI project registry and onboarding (#427)

Load projects from versioned webui/data/projects.registry.json with profile
mappings, workflow/schema paths, and read-only onboarding checklist UI.
Seeds Gitea-Tools; exposes /projects, /projects/{id}, and /api/projects.

Closes #427
This commit is contained in:
2026-07-07 14:44:27 -04:00
parent 30ded9b712
commit 16dcf65825
8 changed files with 529 additions and 7 deletions
+111
View File
@@ -0,0 +1,111 @@
"""Tests for web UI project registry (#427)."""
import json
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from webui.app import create_app
from webui.project_registry import (
default_registry_path,
load_registry,
project_to_dict,
)
class TestProjectRegistryLoader(unittest.TestCase):
def test_default_registry_loads_gitea_tools(self):
registry = load_registry()
self.assertEqual(registry.version, 1)
self.assertEqual(len(registry.projects), 1)
project = registry.projects[0]
self.assertEqual(project.id, "gitea-tools")
self.assertEqual(project.repo_name, "Gitea-Tools")
self.assertEqual(project.gitea_owner, "Scaled-Tech-Consulting")
self.assertEqual(project.remote_host, "https://gitea.prgs.cc")
self.assertEqual(project.profiles["author"], "prgs-author")
self.assertEqual(project.profiles["reviewer"], "prgs-reviewer")
self.assertEqual(project.profiles["reconciler"], "prgs-reconciler")
self.assertIn("skill", project.workflow_paths)
self.assertGreaterEqual(len(project.onboarding_checklist), 4)
def test_registry_rejects_credential_keys(self):
payload = {
"version": 1,
"projects": [
{
"id": "bad",
"repo_name": "Bad",
"gitea_owner": "Org",
"remote_host": "https://gitea.example.invalid",
"default_branch": "main",
"local_checkout_path": ".",
"profiles": {
"author": "a",
"reviewer": "r",
"reconciler": "c",
},
"workflow_paths": {"skill": "skills/x.md"},
"api_token": "secret",
}
],
}
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as handle:
json.dump(payload, handle)
path = Path(handle.name)
try:
with self.assertRaises(ValueError):
load_registry(path)
finally:
path.unlink(missing_ok=True)
def test_default_registry_path_points_at_packaged_data(self):
path = default_registry_path()
self.assertTrue(path.name == "projects.registry.json")
self.assertTrue(path.parent.name == "data")
class TestProjectRegistryRoutes(unittest.TestCase):
def setUp(self):
self.client = TestClient(create_app())
def test_projects_page_lists_gitea_tools(self):
response = self.client.get("/projects")
self.assertEqual(response.status_code, 200)
self.assertIn("Gitea-Tools", response.text)
self.assertIn("Scaled-Tech-Consulting", response.text)
self.assertIn("prgs-author", response.text)
self.assertNotIn("child issue", response.text.lower())
def test_project_detail_renders_checklist(self):
response = self.client.get("/projects/gitea-tools")
self.assertEqual(response.status_code, 200)
self.assertIn("Onboarding checklist", response.text)
self.assertIn("Configure execution profiles", response.text)
self.assertIn("branches/", response.text)
def test_project_detail_404(self):
response = self.client.get("/projects/unknown-repo")
self.assertEqual(response.status_code, 404)
def test_api_projects_json(self):
response = self.client.get("/api/projects")
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data["version"], 1)
self.assertEqual(len(data["projects"]), 1)
self.assertEqual(data["projects"][0]["id"], "gitea-tools")
self.assertIn("onboarding_checklist", data["projects"][0])
def test_project_to_dict_is_json_safe(self):
registry = load_registry()
encoded = json.dumps(project_to_dict(registry.projects[0]))
self.assertIn("gitea-tools", encoded)
if __name__ == "__main__":
unittest.main()
+6 -1
View File
@@ -30,12 +30,17 @@ class TestWebuiSkeleton(unittest.TestCase):
self.assertIn("Read-only MVP", response.text)
def test_route_stubs_render(self):
for path in ("/projects", "/prompts", "/runtime", "/audit"):
for path in ("/prompts", "/runtime", "/audit"):
with self.subTest(path=path):
response = self.client.get(path)
self.assertEqual(response.status_code, 200)
self.assertIn("child issue", response.text.lower())
def test_projects_is_implemented(self):
response = self.client.get("/projects")
self.assertEqual(response.status_code, 200)
self.assertIn("Gitea-Tools", response.text)
def test_extra_stub_routes(self):
for path in ("/worktrees", "/leases"):
with self.subTest(path=path):