From 7a1d950cc2b86771287f24cd0e726fc201b7e18e Mon Sep 17 00:00:00 2001 From: 370025263 <2385455860@qq.com> Date: Thu, 2 Jul 2026 16:46:19 +0800 Subject: [PATCH] fix(api): handle first-use status and skill search Return an empty skill-search result before creating an embedding client when the skill index has not been built yet. Allow /api/v1/status to report an uninitialized skill directory with git_branch=null instead of returning 500. Signed-off-by: 370025263 <2385455860@qq.com> --- src/xskill/api/app.py | 10 +++- src/xskill/core.py | 2 + tests/test_first_use_status_search.py | 69 +++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 tests/test_first_use_status_search.py diff --git a/src/xskill/api/app.py b/src/xskill/api/app.py index ce35d960..7f305d5a 100644 --- a/src/xskill/api/app.py +++ b/src/xskill/api/app.py @@ -26,6 +26,7 @@ from fastapi import APIRouter, FastAPI, HTTPException from fastapi.responses import FileResponse from pydantic import BaseModel, Field +from dulwich.errors import NotGitRepository from xskill import __version__ from xskill.config import load_config, get_skill_dir @@ -183,7 +184,7 @@ class HealthResponse(BaseModel): class StatusResponse(BaseModel): skill_dir: str skill_count: int - git_branch: str + git_branch: Optional[str] = None class InitRequest(BaseModel): @@ -453,6 +454,8 @@ async def api_import_skill(req: ImportSkillRequest): async def api_search_skills(req: SkillSearchRequest): """Search existing skills by semantic similarity.""" try: + if not (_skill_dir / ".skill_index.pkl").exists(): + return [] embedding_client = create_embed_client(_config) return search_skill_index( skill_dir=_skill_dir, @@ -734,7 +737,10 @@ async def api_status(): """Return system status: skill dir, skill count, git branch.""" try: skills = list_skills(_skill_dir) - branch = current_branch(str(_skill_dir)) + try: + branch = current_branch(str(_skill_dir)) + except NotGitRepository: + branch = None return StatusResponse( skill_dir=str(_skill_dir), skill_count=len(skills), diff --git a/src/xskill/core.py b/src/xskill/core.py index 09046c74..af48313b 100644 --- a/src/xskill/core.py +++ b/src/xskill/core.py @@ -89,6 +89,8 @@ def search_trajectories(self, query: str, top_k: int = 5, def search_skills(self, query: str, top_k: int = 5) -> list[SkillHit]: """跨 skill_repo 搜索 skill。""" from xskill.skill.repo import search_skill_index + if not (self.skill_repo.root / ".skill_index.pkl").exists(): + return [] items = search_skill_index( skill_dir=self.skill_repo.root, query=query, diff --git a/tests/test_first_use_status_search.py b/tests/test_first_use_status_search.py new file mode 100644 index 00000000..3b456b35 --- /dev/null +++ b/tests/test_first_use_status_search.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from fastapi.testclient import TestClient + + +def _configure_server(monkeypatch, tmp_path): + from xskill.api import app as srv + + skill_dir = tmp_path / "skill" + skill_dir.mkdir() + monkeypatch.setattr(srv, "_config", { + "llm": {}, + "embedding": {}, + "watcher": {"poll_interval": 30}, + }) + monkeypatch.setattr(srv, "_skill_dir", skill_dir) + return srv, skill_dir + + +def test_status_handles_uninitialized_skill_dir(monkeypatch, tmp_path): + srv, skill_dir = _configure_server(monkeypatch, tmp_path) + + app = srv.create_app(home_root=tmp_path) + client = TestClient(app, raise_server_exceptions=False) + + resp = client.get("/api/v1/status") + + assert resp.status_code == 200 + assert resp.json() == { + "skill_dir": str(skill_dir), + "skill_count": 0, + "git_branch": None, + } + + +def test_api_skill_search_missing_index_skips_embedding_client(monkeypatch, tmp_path): + srv, _skill_dir = _configure_server(monkeypatch, tmp_path) + monkeypatch.setattr( + srv, + "create_embed_client", + lambda _config: (_ for _ in ()).throw(AssertionError("unexpected embed client")), + ) + + app = srv.create_app(home_root=tmp_path) + client = TestClient(app, raise_server_exceptions=False) + + resp = client.post("/api/v1/skills/search", json={"query": "heartbeat", "top_k": 2}) + + assert resp.status_code == 200 + assert resp.json() == [] + + +def test_sdk_skill_search_missing_index_skips_embedding_client(monkeypatch, tmp_path): + from xskill import core + from xskill.utils import llm + + skill_dir = tmp_path / "skill" + skill_dir.mkdir() + monkeypatch.setattr(core, "load_config", lambda config_path=None: {"embedding": {}}) + monkeypatch.setattr(core, "get_skill_dir", lambda: skill_dir) + monkeypatch.setattr( + llm, + "create_embed_client", + lambda _config: (_ for _ in ()).throw(AssertionError("unexpected embed client")), + ) + + xskill = core.XSkill() + + assert xskill.search_skills("heartbeat", top_k=2) == []