feat: Phase 2 — remove builtins layer, ServiceRegistry is installed-only
Unit Tests / test (push) Successful in 11m31s

Builtins (email/calendar/files) are no longer baked into the API image.
ServiceRegistry now only knows about installed store services. When nothing
is installed, Caddy and DNS get no service routes — no hardcoded fallback.

Changes:
- service_registry.py: remove _BUILTINS_DIR, _builtin_ids, _builtin_manifest,
  _load_manifest; get() and list_all() now delegate entirely to installed services
- caddy_manager.py: remove _build_core_service_routes(); remove hardcoded
  fallback pairs from _http01_service_pairs(); empty registry → api block only
- network_manager.py: _get_service_subdomains() returns [] when no registry
- api/services/builtins/: deleted (email, calendar, files manifests)
- Tests updated throughout: removed builtin-dependent assertions, added
  installed-service fixtures, updated fallback expectations to api-only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 08:53:44 -04:00
parent 18b50d08c1
commit 0bfe95320b
12 changed files with 419 additions and 766 deletions
+38 -34
View File
@@ -30,7 +30,7 @@ def _mgr_with_registry(registry=None):
def _mock_registry():
"""Return a mock ServiceRegistry that reproduces the 3 builtin service routes."""
"""Return a mock ServiceRegistry that reproduces 3 store service routes."""
reg = MagicMock()
reg.get_caddy_routes.return_value = [
{
@@ -76,33 +76,39 @@ def _nm(registry=None):
class TestBuildRegistryServiceRoutes(unittest.TestCase):
def test_returns_hardcoded_when_no_registry(self):
"""service_registry=None produces the same output as _build_core_service_routes."""
def test_returns_api_only_when_no_registry(self):
"""service_registry=None produces only the @api block."""
mgr = _mgr_with_registry(registry=None)
domain = 'alpha.pic.ngo'
result = mgr._build_registry_service_routes(domain)
expected = CaddyManager._build_core_service_routes(domain)
self.assertEqual(result, expected)
self.assertIn('@api host api.alpha.pic.ngo', result)
self.assertIn('reverse_proxy cell-api:3000', result)
self.assertNotIn('@calendar', result)
self.assertNotIn('@mail', result)
def test_returns_hardcoded_when_registry_empty(self):
"""An empty route list from the registry falls back to hardcoded."""
def test_returns_api_only_when_registry_empty(self):
"""An empty route list from the registry produces only the @api block."""
reg = MagicMock()
reg.get_caddy_routes.return_value = []
mgr = _mgr_with_registry(registry=reg)
domain = 'alpha.pic.ngo'
result = mgr._build_registry_service_routes(domain)
expected = CaddyManager._build_core_service_routes(domain)
self.assertEqual(result, expected)
self.assertIn('@api host api.alpha.pic.ngo', result)
self.assertIn('reverse_proxy cell-api:3000', result)
self.assertNotIn('@calendar', result)
self.assertNotIn('@mail', result)
def test_registry_error_falls_back(self):
"""When get_caddy_routes raises, output equals _build_core_service_routes."""
def test_returns_api_only_on_registry_error(self):
"""When get_caddy_routes raises, only the @api block is produced."""
reg = MagicMock()
reg.get_caddy_routes.side_effect = Exception('registry unavailable')
mgr = _mgr_with_registry(registry=reg)
domain = 'alpha.pic.ngo'
result = mgr._build_registry_service_routes(domain)
expected = CaddyManager._build_core_service_routes(domain)
self.assertEqual(result, expected)
self.assertIn('@api host api.alpha.pic.ngo', result)
self.assertIn('reverse_proxy cell-api:3000', result)
self.assertNotIn('@calendar', result)
self.assertNotIn('@mail', result)
def test_single_service_no_extras(self):
"""One service with no extra_subdomains produces one matcher + handle + api block."""
@@ -234,27 +240,25 @@ class TestHttp01ServicePairs(unittest.TestCase):
self.assertEqual(webdav_entry, 'cell-webdav:80')
self.assertNotEqual(webdav_entry, 'cell-filegator:8080')
def test_fallback_when_no_registry(self):
"""Without a registry the hardcoded pairs are returned, including api."""
def test_only_api_when_no_registry(self):
"""Without a registry only the api pair is returned."""
mgr = _mgr_with_registry(registry=None)
pairs = mgr._http01_service_pairs()
subdomains = [s for s, _ in pairs]
self.assertIn('calendar', subdomains)
self.assertIn('mail', subdomains)
self.assertIn('webmail', subdomains)
self.assertIn('files', subdomains)
self.assertIn('webdav', subdomains)
self.assertIn('api', subdomains)
self.assertNotIn('calendar', subdomains)
self.assertNotIn('mail', subdomains)
self.assertNotIn('files', subdomains)
def test_fallback_when_registry_error(self):
"""When get_caddy_routes raises, falls back to hardcoded pairs."""
def test_only_api_on_registry_error(self):
"""When get_caddy_routes raises, only the api pair is present."""
reg = MagicMock()
reg.get_caddy_routes.side_effect = RuntimeError('boom')
mgr = _mgr_with_registry(registry=reg)
pairs = mgr._http01_service_pairs()
subdomains = [s for s, _ in pairs]
self.assertIn('calendar', subdomains)
self.assertIn('api', subdomains)
self.assertNotIn('calendar', subdomains)
# ---------------------------------------------------------------------------
@@ -326,14 +330,14 @@ class TestCaddyfileWithRegistry(unittest.TestCase):
self.assertIn('reverse_proxy cell-filegator:8080', out)
self.assertIn('reverse_proxy cell-webdav:80', out)
def test_pic_ngo_fallback_when_registry_empty(self):
"""pic_ngo falls back to hardcoded routes when registry returns empty list."""
def test_pic_ngo_api_only_when_registry_empty(self):
"""pic_ngo emits only the api block when registry returns empty list."""
reg = MagicMock()
reg.get_caddy_routes.return_value = []
out = self._generate('pic_ngo', cell_name='alpha', registry=reg)
# Hardcoded routes should appear
self.assertIn('@calendar host calendar.alpha.pic.ngo', out)
self.assertIn('@mail host mail.alpha.pic.ngo webmail.alpha.pic.ngo', out)
self.assertIn('@api host api.alpha.pic.ngo', out)
self.assertNotIn('@calendar', out)
self.assertNotIn('@mail', out)
# ---------------------------------------------------------------------------
@@ -354,11 +358,11 @@ class TestNetworkManagerGetServiceSubdomains(unittest.TestCase):
self.managers.append(nm)
return nm
def test_no_registry_returns_hardcoded(self):
"""Without a registry the hardcoded service subdomain list is returned."""
def test_no_registry_returns_empty(self):
"""Without a registry an empty list is returned."""
nm = self._make(registry=None)
subs = nm._get_service_subdomains()
self.assertCountEqual(subs, ['calendar', 'files', 'mail', 'webmail', 'webdav'])
self.assertEqual(subs, [])
def test_registry_returns_all_subdomains(self):
"""Primary + extra_subdomains from all routes are returned."""
@@ -369,13 +373,13 @@ class TestNetworkManagerGetServiceSubdomains(unittest.TestCase):
for expected in ('calendar', 'mail', 'webmail', 'files', 'webdav'):
self.assertIn(expected, subs)
def test_registry_error_falls_back(self):
"""When get_caddy_routes raises, hardcoded list is returned."""
def test_registry_error_returns_empty(self):
"""When get_caddy_routes raises, an empty list is returned."""
reg = MagicMock()
reg.get_caddy_routes.side_effect = Exception('broken registry')
nm = self._make(registry=reg)
subs = nm._get_service_subdomains()
self.assertCountEqual(subs, ['calendar', 'files', 'mail', 'webmail', 'webdav'])
self.assertEqual(subs, [])
def test_registry_extra_subdomains_included(self):
"""extra_subdomains from each route are included in the returned list."""