import pytest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool from hqt.db.models import Base from hqt.errors import ServiceError from hqt.mcp.service import McpService from hqt.projects.service import ProjectService @pytest.fixture def factory(): # StaticPool + check_same_thread=False: every session created by the # factory shares the single in-memory connection, so per-operation # sessions all see the same database. engine = create_engine( "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) Base.metadata.create_all(engine) return sessionmaker(bind=engine, expire_on_commit=False) class TestProjectService: def test_create_and_get(self, factory, tmp_path): svc = ProjectService(factory) p = svc.create("test", str(tmp_path)) assert p.id is not None assert svc.get(p.id).name == "test" def test_list_excludes_archived(self, factory, tmp_path): a = tmp_path / "a" a.mkdir() b = tmp_path / "b" b.mkdir() svc = ProjectService(factory) svc.create("a", str(a)) p2 = svc.create("b", str(b)) svc.archive(p2.id) assert len(svc.list_all()) == 1 assert len(svc.list_all(include_archived=True)) == 2 def test_update_name_and_path(self, factory, tmp_path): old = tmp_path / "old" old.mkdir() new = tmp_path / "new" new.mkdir() svc = ProjectService(factory) p = svc.create("old", str(old)) updated = svc.update(p.id, "new", str(new)) assert updated.name == "new" assert updated.path == str(new.resolve()) assert svc.get(p.id).path == str(new.resolve()) def test_update_duplicate_path_raises(self, factory, tmp_path): a = tmp_path / "a" a.mkdir() b = tmp_path / "b" b.mkdir() svc = ProjectService(factory) svc.create("a", str(a)) p2 = svc.create("b", str(b)) with pytest.raises(ServiceError, match="already uses path"): svc.update(p2.id, "b", str(a)) # DB session must remain usable after rollback, values unchanged assert svc.get(p2.id).path == str(b.resolve()) assert svc.get(p2.id).name == "b" def test_update_unknown_id_raises(self, factory, tmp_path): svc = ProjectService(factory) with pytest.raises(ServiceError, match="not found"): svc.update(9999, "x", str(tmp_path)) def test_create_duplicate_path_raises_service_error(self, factory, tmp_path): dup = tmp_path / "dup" dup.mkdir() svc = ProjectService(factory) svc.create("a", str(dup)) with pytest.raises(ServiceError, match="already uses path"): svc.create("b", str(dup)) def test_create_rejects_nonexistent_path(self, factory, tmp_path): svc = ProjectService(factory) missing = tmp_path / "does-not-exist" with pytest.raises(ServiceError, match="does not exist or is not a directory"): svc.create("p", str(missing)) def test_create_rejects_file_path(self, factory, tmp_path): svc = ProjectService(factory) a_file = tmp_path / "afile" a_file.write_text("x") with pytest.raises(ServiceError, match="does not exist or is not a directory"): svc.create("p", str(a_file)) def test_create_stores_resolved_absolute_path(self, factory, tmp_path): svc = ProjectService(factory) p = svc.create("p", str(tmp_path)) assert p.path == str(tmp_path.resolve()) def test_create_expands_user_home(self, factory, tmp_path, monkeypatch): # Point ~ at tmp_path; "~" must expand to the existing tmp_path dir. monkeypatch.setenv("HOME", str(tmp_path)) svc = ProjectService(factory) p = svc.create("p", "~") assert p.path == str(tmp_path.resolve()) def test_update_rejects_nonexistent_path_and_leaves_row(self, factory, tmp_path): svc = ProjectService(factory) p = svc.create("p", str(tmp_path)) missing = tmp_path / "nope" with pytest.raises(ServiceError, match="does not exist or is not a directory"): svc.update(p.id, "p", str(missing)) assert svc.get(p.id).path == str(tmp_path.resolve()) class TestMcpService: def test_crud(self, factory): svc = McpService(factory) server = svc.create("test-mcp", "stdio", command="node", args=["server.js"]) assert server.id is not None assert svc.get("test-mcp") is not None assert len(svc.list_all()) == 1 svc.delete("test-mcp") assert svc.get("test-mcp") is None def test_bind_unbind(self, factory, tmp_path): psvc = ProjectService(factory) msvc = McpService(factory) project = psvc.create("proj", str(tmp_path)) server = msvc.create("s1", "stdio", command="cmd") msvc.bind_to_project(project.id, server.id) assert len(msvc.get_project_mcps(project.id)) == 1 msvc.unbind_from_project(project.id, server.id) assert len(msvc.get_project_mcps(project.id)) == 0