Merge PR #40: Fix MCP integration tests and linting errors

This commit is contained in:
Steve Yegge
2025-10-15 11:45:20 -07:00
9 changed files with 41 additions and 88 deletions

View File

@@ -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(

View File

@@ -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"

View File

@@ -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)

View File

@@ -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.

View File

@@ -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,

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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,

View File

@@ -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" },