From ab2766272222cde2f51ba6bb5f7b9cd5cda0075f Mon Sep 17 00:00:00 2001 From: Fred Porter Date: Thu, 17 Sep 2026 07:40:58 +0800 Subject: [PATCH 1/3] fix(capabilities): remove consolidated uFlow/uKnowledge repos from requirements uFlow and uKnowledge are consolidated internal subsystems of uCore (under app.flow and app.knowledge) rather than standalone git checkouts under ~/Code. Remove legacy repo requirements for workflow.run, knowledge.search, and developer.guided, and add preflight/readiness integration tests. --- backend/tests/test_capabilities_preflight.py | 81 ++++++++++++++++++++ config/capability_requirements.json | 6 +- 2 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 backend/tests/test_capabilities_preflight.py diff --git a/backend/tests/test_capabilities_preflight.py b/backend/tests/test_capabilities_preflight.py new file mode 100644 index 0000000..ea942c9 --- /dev/null +++ b/backend/tests/test_capabilities_preflight.py @@ -0,0 +1,81 @@ +"""Integration tests for Capabilities Preflight and Readiness API endpoints.""" +from __future__ import annotations + +from aiohttp import web +from aiohttp.test_utils import AioHTTPTestCase + +from app.api.extensions_api import ( + handle_capabilities_readiness, + handle_capability_preflight, +) +from app.extensions.registry import registry + + +class CapabilitiesPreflightAPITest(AioHTTPTestCase): + async def get_application(self): + app = web.Application() + registry.discover() + registry.register_routes(app) + app.router.add_get( + "/api/capabilities/{capability}/preflight", + handle_capability_preflight, + ) + app.router.add_get( + "/api/capabilities/readiness", + handle_capabilities_readiness, + ) + return app + + async def test_workflow_run_preflight_ready(self): + resp = await self.client.get("/api/capabilities/workflow.run/preflight") + assert resp.status == 200 + data = await resp.json() + assert data["capability"] == "workflow.run" + assert data["ready"] is True + assert data["repair_required"] is False + assert data["repair"] == [] + + async def test_knowledge_search_preflight_ready(self): + resp = await self.client.get("/api/capabilities/knowledge.search/preflight") + assert resp.status == 200 + data = await resp.json() + assert data["capability"] == "knowledge.search" + assert data["ready"] is True + assert data["repair_required"] is False + assert data["repair"] == [] + + async def test_developer_guided_preflight_ready(self): + resp = await self.client.get("/api/capabilities/developer.guided/preflight") + assert resp.status == 200 + data = await resp.json() + assert data["capability"] == "developer.guided" + assert data["ready"] is True + assert data["repair_required"] is False + assert data["repair"] == [] + + async def test_capabilities_readiness_batch_ready(self): + resp = await self.client.get( + "/api/capabilities/readiness?capabilities=workflow.run,knowledge.search,developer.guided" + ) + assert resp.status == 200 + data = await resp.json() + assert data["ready"] is True + assert data["count"] == 3 + caps = {c["capability"]: c for c in data["capabilities"]} + assert "workflow.run" in caps + assert caps["workflow.run"]["ready"] is True + assert "knowledge.search" in caps + assert caps["knowledge.search"]["ready"] is True + assert "developer.guided" in caps + assert caps["developer.guided"]["ready"] is True + + async def test_unknown_capability_preflight_missing(self): + resp = await self.client.get("/api/capabilities/nonexistent.capability/preflight") + assert resp.status == 412 + data = await resp.json() + assert data["ready"] is False + assert data["repair_required"] is True + assert any( + item.get("kind") == "config" and item.get("id") == "capability_requirements" + for item in data["repair"] + ) diff --git a/config/capability_requirements.json b/config/capability_requirements.json index 24a5180..a7bda5c 100644 --- a/config/capability_requirements.json +++ b/config/capability_requirements.json @@ -2,14 +2,14 @@ "workflow.run": { "extensions": ["uflow"], "tools": ["git"], - "repos": ["uFlow"], + "repos": [], "variables": [], "secrets": [] }, "knowledge.search": { "extensions": ["uknowledge"], "tools": [], - "repos": ["uKnowledge"], + "repos": [], "variables": [], "secrets": [] }, @@ -23,7 +23,7 @@ "developer.guided": { "extensions": [], "tools": ["git", "node", "python"], - "repos": ["uCore", "uFlow", "uKnowledge", "uCode"], + "repos": ["uCore", "uCode"], "variables": [], "secrets": [] }, From 8d0ab302a08b90e1d29fb6ad789e8dc7a31535b2 Mon Sep 17 00:00:00 2001 From: Fred Porter Date: Thu, 17 Sep 2026 07:43:42 +0800 Subject: [PATCH 2/3] test(capabilities): isolate developer.guided preflight test prerequisites --- backend/tests/test_capabilities_preflight.py | 38 +++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/backend/tests/test_capabilities_preflight.py b/backend/tests/test_capabilities_preflight.py index ea942c9..12ccb0b 100644 --- a/backend/tests/test_capabilities_preflight.py +++ b/backend/tests/test_capabilities_preflight.py @@ -1,6 +1,10 @@ """Integration tests for Capabilities Preflight and Readiness API endpoints.""" from __future__ import annotations +import tempfile +from pathlib import Path +from unittest.mock import patch + from aiohttp import web from aiohttp.test_utils import AioHTTPTestCase @@ -8,6 +12,7 @@ handle_capabilities_readiness, handle_capability_preflight, ) +from app.core.settings import settings from app.extensions.registry import registry @@ -44,30 +49,45 @@ async def test_knowledge_search_preflight_ready(self): assert data["repair_required"] is False assert data["repair"] == [] - async def test_developer_guided_preflight_ready(self): + async def test_developer_guided_preflight_excludes_uflow_and_uknowledge(self): resp = await self.client.get("/api/capabilities/developer.guided/preflight") - assert resp.status == 200 data = await resp.json() assert data["capability"] == "developer.guided" - assert data["ready"] is True - assert data["repair_required"] is False - assert data["repair"] == [] + repair_ids = [item.get("id") for item in data.get("repair", [])] + assert "uFlow" not in repair_ids + assert "uKnowledge" not in repair_ids + + async def test_developer_guided_preflight_ready_when_prereqs_met(self): + async def fake_check_tool(tool_id: str): + return {"installed": True, "ok": True} + + with tempfile.TemporaryDirectory() as td: + root = Path(td) + (root / "uCore").mkdir() + (root / "uCode").mkdir() + with patch.object(settings, "udos_root", root), \ + patch("app.api.extensions_api.check_tool", side_effect=fake_check_tool): + resp = await self.client.get("/api/capabilities/developer.guided/preflight") + assert resp.status == 200 + data = await resp.json() + assert data["capability"] == "developer.guided" + assert data["ready"] is True + assert data["repair_required"] is False + assert data["repair"] == [] async def test_capabilities_readiness_batch_ready(self): resp = await self.client.get( - "/api/capabilities/readiness?capabilities=workflow.run,knowledge.search,developer.guided" + "/api/capabilities/readiness?capabilities=workflow.run,knowledge.search" ) assert resp.status == 200 data = await resp.json() assert data["ready"] is True - assert data["count"] == 3 + assert data["count"] == 2 caps = {c["capability"]: c for c in data["capabilities"]} assert "workflow.run" in caps assert caps["workflow.run"]["ready"] is True assert "knowledge.search" in caps assert caps["knowledge.search"]["ready"] is True - assert "developer.guided" in caps - assert caps["developer.guided"]["ready"] is True async def test_unknown_capability_preflight_missing(self): resp = await self.client.get("/api/capabilities/nonexistent.capability/preflight") From ab1281a9534cacf5416aa8c9f4231c5ab02749fa Mon Sep 17 00:00:00 2001 From: Fred Porter Date: Thu, 17 Sep 2026 07:46:13 +0800 Subject: [PATCH 3/3] fix(capabilities): auto-filter consolidated repos from user overrides and add schema test --- backend/app/api/extensions_api.py | 7 ++++ backend/tests/test_capabilities_preflight.py | 40 ++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/backend/app/api/extensions_api.py b/backend/app/api/extensions_api.py index 972843f..edf7bce 100644 --- a/backend/app/api/extensions_api.py +++ b/backend/app/api/extensions_api.py @@ -32,6 +32,9 @@ def _load_user_vars() -> dict[str, Any]: return data if isinstance(data, dict) else {} +CONSOLIDATED_REPOS: frozenset[str] = frozenset({"uFlow", "uKnowledge"}) + + def _load_capability_requirements() -> tuple[dict[str, Any], str]: override = settings.config_dir / "capability_requirements.json" source = override if override.exists() else _DEFAULT_CAPABILITY_REQ_FILE @@ -43,6 +46,10 @@ def _load_capability_requirements() -> tuple[dict[str, Any], str]: return {}, str(source) if not isinstance(data, dict): return {}, str(source) + # Automatically migrate/clean up consolidated repos from requirements (including user overrides) + for spec in data.values(): + if isinstance(spec, dict) and isinstance(spec.get("repos"), list): + spec["repos"] = [r for r in spec["repos"] if str(r) not in CONSOLIDATED_REPOS] return data, str(source) diff --git a/backend/tests/test_capabilities_preflight.py b/backend/tests/test_capabilities_preflight.py index 12ccb0b..ac2944c 100644 --- a/backend/tests/test_capabilities_preflight.py +++ b/backend/tests/test_capabilities_preflight.py @@ -31,6 +31,16 @@ async def get_application(self): ) return app + async def test_capability_requirements_consolidated_repos_contract(self): + from app.api.extensions_api import _load_capability_requirements, CONSOLIDATED_REPOS + reqs, _ = _load_capability_requirements() + assert reqs["workflow.run"]["repos"] == [] + assert reqs["knowledge.search"]["repos"] == [] + assert reqs["developer.guided"]["repos"] == ["uCore", "uCode"] + for cap, spec in reqs.items(): + repos = spec.get("repos", []) + assert not (CONSOLIDATED_REPOS & set(repos)), f"{cap} must not require consolidated repos: {repos}" + async def test_workflow_run_preflight_ready(self): resp = await self.client.get("/api/capabilities/workflow.run/preflight") assert resp.status == 200 @@ -99,3 +109,33 @@ async def test_unknown_capability_preflight_missing(self): item.get("kind") == "config" and item.get("id") == "capability_requirements" for item in data["repair"] ) + + async def test_legacy_user_override_filters_consolidated_repos(self): + import json + + with tempfile.TemporaryDirectory() as td: + cfg_dir = Path(td) + override_file = cfg_dir / "capability_requirements.json" + # Simulate legacy override file containing obsolete uFlow / uKnowledge repo requirements + override_file.write_text( + json.dumps( + { + "workflow.run": { + "extensions": ["uflow"], + "tools": ["git"], + "repos": ["uFlow"], + "variables": [], + "secrets": [], + } + } + ), + encoding="utf-8", + ) + with patch.object(settings, "config_dir", cfg_dir): + resp = await self.client.get("/api/capabilities/workflow.run/preflight") + assert resp.status == 200 + data = await resp.json() + assert data["ready"] is True + assert data["repair_required"] is False + assert data["repair"] == [] +