Merge PR #40: Fix MCP integration tests and linting errors
This commit is contained in:
@@ -101,12 +101,8 @@ class BdClient:
|
|||||||
self.bd_path = bd_path if bd_path is not None else config.beads_path
|
self.bd_path = bd_path if bd_path is not None else config.beads_path
|
||||||
self.beads_db = beads_db if beads_db is not None else config.beads_db
|
self.beads_db = beads_db if beads_db is not None else config.beads_db
|
||||||
self.actor = actor if actor is not None else config.beads_actor
|
self.actor = actor if actor is not None else config.beads_actor
|
||||||
self.no_auto_flush = (
|
self.no_auto_flush = no_auto_flush if no_auto_flush is not None else config.beads_no_auto_flush
|
||||||
no_auto_flush if no_auto_flush is not None else config.beads_no_auto_flush
|
self.no_auto_import = no_auto_import if no_auto_import is not None else config.beads_no_auto_import
|
||||||
)
|
|
||||||
self.no_auto_import = (
|
|
||||||
no_auto_import if no_auto_import is not None else config.beads_no_auto_import
|
|
||||||
)
|
|
||||||
self.working_dir = working_dir if working_dir is not None else config.beads_working_dir
|
self.working_dir = working_dir if working_dir is not None else config.beads_working_dir
|
||||||
|
|
||||||
def _get_working_dir(self) -> str:
|
def _get_working_dir(self) -> str:
|
||||||
@@ -161,9 +157,7 @@ class BdClient:
|
|||||||
)
|
)
|
||||||
stdout, stderr = await process.communicate()
|
stdout, stderr = await process.communicate()
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
raise BdNotFoundError(
|
raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e
|
||||||
BdNotFoundError.installation_message(self.bd_path)
|
|
||||||
) from e
|
|
||||||
|
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise BdCommandError(
|
raise BdCommandError(
|
||||||
@@ -205,9 +199,7 @@ class BdClient:
|
|||||||
)
|
)
|
||||||
stdout, stderr = await process.communicate()
|
stdout, stderr = await process.communicate()
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
raise BdNotFoundError(
|
raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e
|
||||||
BdNotFoundError.installation_message(self.bd_path)
|
|
||||||
) from e
|
|
||||||
|
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise BdCommandError(
|
raise BdCommandError(
|
||||||
@@ -220,9 +212,7 @@ class BdClient:
|
|||||||
version_output = stdout.decode().strip()
|
version_output = stdout.decode().strip()
|
||||||
match = re.search(r"(\d+)\.(\d+)\.(\d+)", version_output)
|
match = re.search(r"(\d+)\.(\d+)\.(\d+)", version_output)
|
||||||
if not match:
|
if not match:
|
||||||
raise BdVersionError(
|
raise BdVersionError(f"Could not parse bd version from: {version_output}")
|
||||||
f"Could not parse bd version from: {version_output}"
|
|
||||||
)
|
|
||||||
|
|
||||||
version = tuple(int(x) for x in match.groups())
|
version = tuple(int(x) for x in match.groups())
|
||||||
|
|
||||||
@@ -418,9 +408,7 @@ class BdClient:
|
|||||||
)
|
)
|
||||||
_stdout, stderr = await process.communicate()
|
_stdout, stderr = await process.communicate()
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
raise BdNotFoundError(
|
raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e
|
||||||
BdNotFoundError.installation_message(self.bd_path)
|
|
||||||
) from e
|
|
||||||
|
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise BdCommandError(
|
raise BdCommandError(
|
||||||
@@ -446,9 +434,7 @@ class BdClient:
|
|||||||
)
|
)
|
||||||
stdout, stderr = await process.communicate()
|
stdout, stderr = await process.communicate()
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
raise BdNotFoundError(
|
raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e
|
||||||
BdNotFoundError.installation_message(self.bd_path)
|
|
||||||
) from e
|
|
||||||
|
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise BdCommandError(
|
raise BdCommandError(
|
||||||
@@ -513,9 +499,7 @@ class BdClient:
|
|||||||
)
|
)
|
||||||
stdout, stderr = await process.communicate()
|
stdout, stderr = await process.communicate()
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
raise BdNotFoundError(
|
raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e
|
||||||
BdNotFoundError.installation_message(self.bd_path)
|
|
||||||
) from e
|
|
||||||
|
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise BdCommandError(
|
raise BdCommandError(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from pydantic import field_validator
|
from pydantic import Field, field_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ class Config(BaseSettings):
|
|||||||
|
|
||||||
model_config = SettingsConfigDict(env_prefix="")
|
model_config = SettingsConfigDict(env_prefix="")
|
||||||
|
|
||||||
beads_path: str = _default_beads_path()
|
beads_path: str = Field(default_factory=_default_beads_path)
|
||||||
beads_db: str | None = None
|
beads_db: str | None = None
|
||||||
beads_actor: str | None = None
|
beads_actor: str | None = None
|
||||||
beads_no_auto_flush: bool = False
|
beads_no_auto_flush: bool = False
|
||||||
@@ -71,9 +71,7 @@ class Config(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not os.access(v, os.X_OK):
|
if not os.access(v, os.X_OK):
|
||||||
raise ValueError(
|
raise ValueError(f"bd executable at {v} is not executable.\nPlease check file permissions.")
|
||||||
f"bd executable at {v} is not executable.\nPlease check file permissions."
|
|
||||||
)
|
|
||||||
|
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@@ -97,8 +95,7 @@ class Config(BaseSettings):
|
|||||||
path = Path(v)
|
path = Path(v)
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"BEADS_DB points to non-existent file: {v}\n"
|
f"BEADS_DB points to non-existent file: {v}\n" + "Please verify the database path is correct."
|
||||||
+ "Please verify the database path is correct."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return v
|
return v
|
||||||
@@ -124,7 +121,7 @@ def load_config() -> Config:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
default_path = _default_beads_path()
|
default_path = _default_beads_path()
|
||||||
error_msg = (
|
error_msg = (
|
||||||
f"Beads MCP Server Configuration Error\n\n"
|
"Beads MCP Server Configuration Error\n\n"
|
||||||
+ f"{e}\n\n"
|
+ f"{e}\n\n"
|
||||||
+ "Common fix: Install the bd CLI first:\n"
|
+ "Common fix: Install the bd CLI first:\n"
|
||||||
+ " curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/install.sh | bash\n\n"
|
+ " curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/install.sh | bash\n\n"
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ async def debug_env() -> str:
|
|||||||
info.append(f"USER: {os.environ.get('USER', 'NOT SET')}\n")
|
info.append(f"USER: {os.environ.get('USER', 'NOT SET')}\n")
|
||||||
info.append("\n=== All Environment Variables ===\n")
|
info.append("\n=== All Environment Variables ===\n")
|
||||||
for key, value in sorted(os.environ.items()):
|
for key, value in sorted(os.environ.items()):
|
||||||
if not key.startswith('_'): # Skip internal vars
|
if not key.startswith("_"): # Skip internal vars
|
||||||
info.append(f"{key}={value}\n")
|
info.append(f"{key}={value}\n")
|
||||||
return "".join(info)
|
return "".join(info)
|
||||||
|
|
||||||
|
|||||||
@@ -68,13 +68,9 @@ async def beads_ready_work(
|
|||||||
|
|
||||||
|
|
||||||
async def beads_list_issues(
|
async def beads_list_issues(
|
||||||
status: Annotated[
|
status: Annotated[IssueStatus | None, "Filter by status (open, in_progress, blocked, closed)"] = None,
|
||||||
IssueStatus | None, "Filter by status (open, in_progress, blocked, closed)"
|
|
||||||
] = None,
|
|
||||||
priority: Annotated[int | None, "Filter by priority (0-4, 0=highest)"] = None,
|
priority: Annotated[int | None, "Filter by priority (0-4, 0=highest)"] = None,
|
||||||
issue_type: Annotated[
|
issue_type: Annotated[IssueType | None, "Filter by type (bug, feature, task, epic, chore)"] = None,
|
||||||
IssueType | None, "Filter by type (bug, feature, task, epic, chore)"
|
|
||||||
] = None,
|
|
||||||
assignee: Annotated[str | None, "Filter by assignee"] = None,
|
assignee: Annotated[str | None, "Filter by assignee"] = None,
|
||||||
limit: Annotated[int, "Maximum number of issues to return (1-1000)"] = 50,
|
limit: Annotated[int, "Maximum number of issues to return (1-1000)"] = 50,
|
||||||
) -> list[Issue]:
|
) -> list[Issue]:
|
||||||
@@ -110,9 +106,7 @@ async def beads_create_issue(
|
|||||||
acceptance: Annotated[str | None, "Acceptance criteria"] = None,
|
acceptance: Annotated[str | None, "Acceptance criteria"] = None,
|
||||||
external_ref: Annotated[str | None, "External reference (e.g., gh-9, jira-ABC)"] = None,
|
external_ref: Annotated[str | None, "External reference (e.g., gh-9, jira-ABC)"] = None,
|
||||||
priority: Annotated[int, "Priority (0-4, 0=highest)"] = 2,
|
priority: Annotated[int, "Priority (0-4, 0=highest)"] = 2,
|
||||||
issue_type: Annotated[
|
issue_type: Annotated[IssueType, "Type: bug, feature, task, epic, or chore"] = DEFAULT_ISSUE_TYPE,
|
||||||
IssueType, "Type: bug, feature, task, epic, or chore"
|
|
||||||
] = DEFAULT_ISSUE_TYPE,
|
|
||||||
assignee: Annotated[str | None, "Assignee username"] = None,
|
assignee: Annotated[str | None, "Assignee username"] = None,
|
||||||
labels: Annotated[list[str] | None, "List of labels"] = None,
|
labels: Annotated[list[str] | None, "List of labels"] = None,
|
||||||
id: Annotated[str | None, "Explicit issue ID (e.g., bd-42)"] = None,
|
id: Annotated[str | None, "Explicit issue ID (e.g., bd-42)"] = None,
|
||||||
@@ -243,9 +237,7 @@ async def beads_blocked() -> list[BlockedIssue]:
|
|||||||
|
|
||||||
|
|
||||||
async def beads_init(
|
async def beads_init(
|
||||||
prefix: Annotated[
|
prefix: Annotated[str | None, "Issue prefix (e.g., 'myproject' for myproject-1, myproject-2)"] = None,
|
||||||
str | None, "Issue prefix (e.g., 'myproject' for myproject-1, myproject-2)"
|
|
||||||
] = None,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Initialize bd in current directory.
|
"""Initialize bd in current directory.
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
"""Unit tests for BdClient."""
|
"""Unit tests for BdClient."""
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from beads_mcp.bd_client import BdClient, BdCommandError, BdNotFoundError
|
from beads_mcp.bd_client import BdClient, BdCommandError, BdNotFoundError
|
||||||
from beads_mcp.models import (
|
from beads_mcp.models import (
|
||||||
AddDependencyParams,
|
AddDependencyParams,
|
||||||
CloseIssueParams,
|
CloseIssueParams,
|
||||||
CreateIssueParams,
|
CreateIssueParams,
|
||||||
DependencyType,
|
|
||||||
IssueStatus,
|
|
||||||
IssueType,
|
|
||||||
ListIssuesParams,
|
ListIssuesParams,
|
||||||
ReadyWorkParams,
|
ReadyWorkParams,
|
||||||
ShowIssueParams,
|
ShowIssueParams,
|
||||||
|
|||||||
@@ -7,14 +7,11 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from beads_mcp.bd_client import BdClient, BdCommandError, BdNotFoundError
|
from beads_mcp.bd_client import BdClient, BdCommandError
|
||||||
from beads_mcp.models import (
|
from beads_mcp.models import (
|
||||||
AddDependencyParams,
|
AddDependencyParams,
|
||||||
CloseIssueParams,
|
CloseIssueParams,
|
||||||
CreateIssueParams,
|
CreateIssueParams,
|
||||||
DependencyType,
|
|
||||||
IssueStatus,
|
|
||||||
IssueType,
|
|
||||||
ListIssuesParams,
|
ListIssuesParams,
|
||||||
ReadyWorkParams,
|
ReadyWorkParams,
|
||||||
ShowIssueParams,
|
ShowIssueParams,
|
||||||
@@ -358,7 +355,6 @@ async def test_init_creates_beads_directory(bd_executable):
|
|||||||
This is a critical test for the bug where init was using --db flag
|
This is a critical test for the bug where init was using --db flag
|
||||||
and creating the database in the wrong location.
|
and creating the database in the wrong location.
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
from beads_mcp.bd_client import BdClient
|
from beads_mcp.bd_client import BdClient
|
||||||
from beads_mcp.models import InitParams
|
from beads_mcp.models import InitParams
|
||||||
|
|
||||||
@@ -370,30 +366,22 @@ async def test_init_creates_beads_directory(bd_executable):
|
|||||||
# Ensure .beads doesn't exist yet
|
# Ensure .beads doesn't exist yet
|
||||||
assert not beads_dir.exists()
|
assert not beads_dir.exists()
|
||||||
|
|
||||||
# Create client WITHOUT beads_db set (this was the bug!)
|
# Create client WITHOUT beads_db set and WITH working_dir set to temp_dir
|
||||||
client = BdClient(bd_path=bd_executable, beads_db=None)
|
client = BdClient(bd_path=bd_executable, beads_db=None, working_dir=temp_dir)
|
||||||
|
|
||||||
# Change to temp directory and run init
|
# Initialize with custom prefix (no need to chdir!)
|
||||||
original_cwd = os.getcwd()
|
params = InitParams(prefix="test")
|
||||||
try:
|
result = await client.init(params)
|
||||||
os.chdir(temp_dir)
|
|
||||||
|
|
||||||
# Initialize with custom prefix
|
# Verify .beads directory was created in temp directory
|
||||||
params = InitParams(prefix="test")
|
assert beads_dir.exists(), f".beads directory not created in {temp_dir}"
|
||||||
result = await client.init(params)
|
assert beads_dir.is_dir(), ".beads exists but is not a directory"
|
||||||
|
|
||||||
# Verify .beads directory was created in current directory
|
# Verify database file was created with correct prefix
|
||||||
assert beads_dir.exists(), f".beads directory not created in {temp_dir}"
|
db_files = list(beads_dir.glob("*.db"))
|
||||||
assert beads_dir.is_dir(), f".beads exists but is not a directory"
|
assert len(db_files) > 0, "No database file created in .beads/"
|
||||||
|
assert any("test" in str(db.name) for db in db_files), \
|
||||||
|
f"Database file doesn't contain prefix 'test': {[db.name for db in db_files]}"
|
||||||
|
|
||||||
# Verify database file was created with correct prefix
|
# Verify success message
|
||||||
db_files = list(beads_dir.glob("*.db"))
|
assert "initialized" in result.lower() or "created" in result.lower()
|
||||||
assert len(db_files) > 0, "No database file created in .beads/"
|
|
||||||
assert any("test" in str(db.name) for db in db_files), \
|
|
||||||
f"Database file doesn't contain prefix 'test': {[db.name for db in db_files]}"
|
|
||||||
|
|
||||||
# Verify success message
|
|
||||||
assert "initialized" in result.lower() or "created" in result.lower()
|
|
||||||
|
|
||||||
finally:
|
|
||||||
os.chdir(original_cwd)
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
"""Tests for beads_mcp.config module."""
|
"""Tests for beads_mcp.config module."""
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
@@ -19,7 +17,7 @@ class TestConfig:
|
|||||||
monkeypatch.delenv("BEADS_PATH", raising=False)
|
monkeypatch.delenv("BEADS_PATH", raising=False)
|
||||||
|
|
||||||
# Mock shutil.which to return a test path
|
# Mock shutil.which to return a test path
|
||||||
with patch("shutil.which", return_value="/usr/local/bin/bd"):
|
with patch("shutil.which", return_value="/usr/local/bin/bd"), patch("os.access", return_value=True):
|
||||||
config = Config()
|
config = Config()
|
||||||
assert config.beads_path == "/usr/local/bin/bd"
|
assert config.beads_path == "/usr/local/bin/bd"
|
||||||
|
|
||||||
@@ -39,11 +37,9 @@ class TestConfig:
|
|||||||
monkeypatch.setenv("BEADS_PATH", "bd")
|
monkeypatch.setenv("BEADS_PATH", "bd")
|
||||||
|
|
||||||
# Mock shutil.which to simulate finding bd in PATH
|
# Mock shutil.which to simulate finding bd in PATH
|
||||||
with patch("shutil.which", return_value="/usr/local/bin/bd"):
|
with patch("shutil.which", return_value="/usr/local/bin/bd"), patch("os.access", return_value=True):
|
||||||
# Mock os.access to say the file is executable
|
config = Config()
|
||||||
with patch("os.access", return_value=True):
|
assert config.beads_path == "/usr/local/bin/bd"
|
||||||
config = Config()
|
|
||||||
assert config.beads_path == "/usr/local/bin/bd"
|
|
||||||
|
|
||||||
def test_beads_path_not_found(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_beads_path_not_found(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
"""Test that invalid BEADS_PATH raises ValueError."""
|
"""Test that invalid BEADS_PATH raises ValueError."""
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ from unittest.mock import AsyncMock, patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from beads_mcp.bd_client import BdClient
|
from beads_mcp.models import BlockedIssue, Issue, Stats
|
||||||
from beads_mcp.models import BlockedIssue, Issue, IssueStatus, IssueType, Stats
|
|
||||||
from beads_mcp.tools import (
|
from beads_mcp.tools import (
|
||||||
beads_add_dependency,
|
beads_add_dependency,
|
||||||
beads_blocked,
|
beads_blocked,
|
||||||
|
|||||||
2
integrations/beads-mcp/uv.lock
generated
2
integrations/beads-mcp/uv.lock
generated
@@ -48,7 +48,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "beads-mcp"
|
name = "beads-mcp"
|
||||||
version = "0.9.4"
|
version = "0.9.6"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "fastmcp" },
|
{ name = "fastmcp" },
|
||||||
|
|||||||
Reference in New Issue
Block a user