Skip to content

Commit 2c9f01e

Browse files
committed
feat: Fix singleton implementation for LockManager and ClientManager
1 parent 045e2d6 commit 2c9f01e

File tree

5 files changed

+58
-14
lines changed

5 files changed

+58
-14
lines changed

src/vectorcode/cli_utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -619,13 +619,13 @@ class LockManager:
619619
"""
620620

621621
__locks: dict[str, AsyncFileLock]
622-
__singleton: "LockManager"
622+
singleton: Optional["LockManager"] = None
623623

624624
def __new__(cls) -> "LockManager":
625-
if not hasattr(cls, "__singleton") or cls.__singleton is None:
626-
cls.__singleton = super().__new__(cls)
627-
cls.__singleton.__locks = {}
628-
return cls.__singleton
625+
if cls.singleton is None:
626+
cls.singleton = super().__new__(cls)
627+
cls.singleton.__locks = {}
628+
return cls.singleton
629629

630630
def get_lock(self, path: str | os.PathLike) -> AsyncFileLock:
631631
path = str(expand_path(str(path), True))

src/vectorcode/common.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -248,14 +248,14 @@ class _ClientModel:
248248

249249

250250
class ClientManager:
251-
__singleton: Optional["ClientManager"] = None
251+
singleton: Optional["ClientManager"] = None
252252
__clients: dict[str, _ClientModel]
253253

254254
def __new__(cls) -> "ClientManager":
255-
if not hasattr(cls, "__singleton") or cls.__singleton is None:
256-
cls.__singleton = super().__new__(cls)
257-
cls.__singleton.__clients = {}
258-
return cls.__singleton
255+
if cls.singleton is None:
256+
cls.singleton = super().__new__(cls)
257+
cls.singleton.__clients = {}
258+
return cls.singleton
259259

260260
@contextlib.asynccontextmanager
261261
async def get_client(self, configs: Config, need_lock: bool = True):

src/vectorcode/mcp_main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ async def query_tool(
188188
async with ClientManager().get_client(config) as client:
189189
collection = await get_collection(client, config, False)
190190

191-
if collection is None:
191+
if collection is None: # pragma: nocover
192192
raise McpError(
193193
ErrorData(
194194
code=1,

tests/test_common.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,15 +509,19 @@ async def test_wait_for_server_timeout():
509509

510510
@pytest.mark.asyncio
511511
async def test_client_manager_get_client():
512-
config = Config(db_url="https://test_host:1234", db_path="test_db")
512+
config = Config(
513+
db_url="https://test_host:1234", db_path="test_db", project_root="test_proj"
514+
)
513515
config1 = Config(
514516
db_url="http://test_host1:1234",
515517
db_path="test_db",
518+
project_root="test_proj1",
516519
db_settings={"anonymized_telemetry": True},
517520
)
518521
config1_alt = Config(
519522
db_url="http://test_host1:1234",
520523
db_path="test_db",
524+
project_root="test_proj1",
521525
db_settings={"anonymized_telemetry": True, "other_setting": "value"},
522526
)
523527
# Patch chromadb.AsyncHttpClient to avoid actual network calls
@@ -580,6 +584,42 @@ async def test_client_manager_get_client():
580584
assert id(client1_alt) == id(client1)
581585

582586

587+
@pytest.mark.asyncio
588+
async def test_client_manager_list_server_processes():
589+
async def _try_server(url):
590+
return "127.0.0.1" in url or "localhost" in url
591+
592+
async def _start_server(cfg):
593+
return AsyncMock()
594+
595+
with (
596+
tempfile.TemporaryDirectory() as temp_dir,
597+
patch("vectorcode.common.start_server", side_effect=_start_server),
598+
patch("vectorcode.common.try_server", side_effect=_try_server),
599+
):
600+
db_path = os.path.join(temp_dir, "db")
601+
os.makedirs(db_path, exist_ok=True)
602+
603+
ClientManager._create_client = AsyncMock()
604+
async with ClientManager().get_client(
605+
Config(
606+
db_url="http://test_host:8001",
607+
project_root="proj1",
608+
db_path=db_path,
609+
)
610+
):
611+
print(ClientManager().get_processes())
612+
async with ClientManager().get_client(
613+
Config(
614+
db_url="http://test_host:8002",
615+
project_root="proj2",
616+
db_path=db_path,
617+
)
618+
):
619+
pass
620+
assert len(ClientManager().get_processes()) == 2
621+
622+
583623
@pytest.mark.asyncio
584624
async def test_client_manager_kill_servers():
585625
manager = ClientManager()
@@ -596,5 +636,6 @@ async def _try_server(url):
596636
manager._create_client = AsyncMock(return_value=AsyncMock())
597637
async with manager.get_client(Config(db_url="http://test_host:1081")):
598638
pass
639+
assert len(manager.get_processes()) == 1
599640
await manager.kill_servers()
600641
mock_process.terminate.assert_called_once()

tests/test_mcp.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ async def test_query_tool_invalid_project_root():
8787
@pytest.mark.asyncio
8888
async def test_query_tool_success():
8989
with (
90+
tempfile.TemporaryDirectory() as temp_dir,
9091
patch("os.path.isdir", return_value=True),
9192
patch("vectorcode.mcp_main.get_project_config") as mock_get_project_config,
9293
patch("vectorcode.mcp_main.get_collection") as mock_get_collection,
@@ -101,7 +102,9 @@ async def test_query_tool_success():
101102
):
102103
from vectorcode.mcp_main import ClientManager
103104

104-
mock_config = Config(chunk_size=100, overlap_ratio=0.1, reranker=None)
105+
mock_config = Config(
106+
chunk_size=100, overlap_ratio=0.1, reranker=None, project_root=temp_dir
107+
)
105108
mock_load_config_file.return_value = mock_config
106109
mock_get_project_config.return_value = mock_config
107110
mock_client = AsyncMock()
@@ -126,7 +129,7 @@ async def test_query_tool_success():
126129
mock_open.return_value = mock_file_handle
127130

128131
result = await query_tool(
129-
n_query=2, query_messages=["keyword1"], project_root="/valid/path"
132+
n_query=2, query_messages=["keyword1"], project_root=temp_dir
130133
)
131134

132135
assert len(result) == 2

0 commit comments

Comments
 (0)