From fb02862d4451921c7961b48bb5e4091296f53b5e Mon Sep 17 00:00:00 2001 From: who96 <825265100@qq.com> Date: Thu, 26 Mar 2026 09:18:17 +0800 Subject: [PATCH] Refresh local registry for local skill search --- openspace/mcp_server.py | 8 ++--- tests/test_issue3_startup.py | 69 ++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/openspace/mcp_server.py b/openspace/mcp_server.py index d3b8e9e..e81ef12 100644 --- a/openspace/mcp_server.py +++ b/openspace/mcp_server.py @@ -119,7 +119,6 @@ mcp = FastMCP("OpenSpace", **_fastmcp_kwargs) _openspace_instance = None _openspace_lock = asyncio.Lock() _standalone_store = None -_local_skill_registry = None # Internal state: tracks bot skill directories already registered this session. _registered_skill_dirs: set = set() @@ -205,11 +204,9 @@ def _get_local_skill_registry(): This avoids initializing the full OpenSpace engine when callers only want to inspect local skills. It mirrors the skill directory discovery order used by the full engine, but skips LLM / provider startup. + The registry is rebuilt per call so later local searches can see + newly added skills without requiring a process restart. """ - global _local_skill_registry - if _local_skill_registry is not None: - return _local_skill_registry - from openspace.config import get_config from openspace.skill_engine import SkillRegistry @@ -253,7 +250,6 @@ def _get_local_skill_registry(): registry = SkillRegistry(skill_dirs=skill_paths) registry.discover() - _local_skill_registry = registry return registry diff --git a/tests/test_issue3_startup.py b/tests/test_issue3_startup.py index 386a412..7e1e044 100644 --- a/tests/test_issue3_startup.py +++ b/tests/test_issue3_startup.py @@ -47,3 +47,72 @@ Find me when querying demo local skill. item["name"] == "Demo Local Skill" for item in payload["results"] ) + + +@pytest.mark.asyncio +async def test_local_search_skills_refreshes_registry_between_calls(monkeypatch, tmp_path): + skill_root = tmp_path / "skills" + first_skill_dir = skill_root / "first-local-skill" + first_skill_dir.mkdir(parents=True) + (first_skill_dir / "SKILL.md").write_text( + """--- +name: First Local Skill +description: First local skill for cache regression coverage +--- + +First local skill content. +""", + encoding="utf-8", + ) + + async def forbidden_get_openspace(): + pytest.fail("_get_openspace should not run for source='local'") + + monkeypatch.setenv("OPENSPACE_HOST_SKILL_DIRS", str(skill_root)) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_BASE_URL", raising=False) + monkeypatch.setattr(mcp_server, "_get_openspace", forbidden_get_openspace) + monkeypatch.setattr( + "openspace.cloud.embedding.generate_embedding", + lambda text, api_key=None: None, + ) + + first_response = await mcp_server.search_skills( + query="first local skill", + source="local", + limit=5, + auto_import=False, + ) + + first_payload = json.loads(first_response) + assert any( + item["name"] == "First Local Skill" + for item in first_payload["results"] + ) + + second_skill_dir = skill_root / "second-local-skill" + second_skill_dir.mkdir(parents=True) + (second_skill_dir / "SKILL.md").write_text( + """--- +name: Second Local Skill +description: Second local skill created after the first search +--- + +Second local skill content. +""", + encoding="utf-8", + ) + + second_response = await mcp_server.search_skills( + query="second local skill", + source="local", + limit=5, + auto_import=False, + ) + + second_payload = json.loads(second_response) + assert any( + item["name"] == "Second Local Skill" + for item in second_payload["results"] + )