Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions backend/app/api/extensions_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)


Expand Down
141 changes: 141 additions & 0 deletions backend/tests/test_capabilities_preflight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""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

from app.api.extensions_api import (
handle_capabilities_readiness,
handle_capability_preflight,
)
from app.core.settings import settings
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_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
data = await resp.json()
assert data["capability"] == "workflow.run"
assert data["ready"] is True
assert data["repair_required"] is False
assert data["repair"] == []
Comment thread
fredporter marked this conversation as resolved.

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_excludes_uflow_and_uknowledge(self):
resp = await self.client.get("/api/capabilities/developer.guided/preflight")
data = await resp.json()
assert data["capability"] == "developer.guided"
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"
)
assert resp.status == 200
data = await resp.json()
assert data["ready"] is True
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

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"]
)

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"] == []

6 changes: 3 additions & 3 deletions config/capability_requirements.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
},
Expand All @@ -23,7 +23,7 @@
"developer.guided": {
"extensions": [],
"tools": ["git", "node", "python"],
"repos": ["uCore", "uFlow", "uKnowledge", "uCode"],
"repos": ["uCore", "uCode"],
"variables": [],
"secrets": []
},
Expand Down
Loading