test(mcp): Fix two failing integration tests and linting errors

1. Fix `test_default_beads_path_auto_detection`
    - Changed beads_path to use `Field(default_factory=_default_beads_path)` so the default is evaluated at instance
    creation time, not class definition time
    - Updated test to mock both `shutil.which` and `os.access`
2. Fix `test_init_creates_beads_directory`
    - Fixed test to pass `working_dir=temp_dir` to `BdClient` instead of using `os.chdir()`
    - The `_get_working_dir()` method checks `PWD` env var first, which isn't updated by `os.chdir()`
3. Fix minor linting errors reported by `ruff` tool
4. Update `beads` version to `0.9.6` in `uv.lock` file

MCP Server test coverage is now excellent, at 92% overall maintaining our high-standards of production level quality.

```
Name                         Stmts   Miss  Cover
------------------------------------------------
src/beads_mcp/__init__.py        1      0   100%
src/beads_mcp/__main__.py        3      3     0%
src/beads_mcp/bd_client.py     214     14    93%
src/beads_mcp/config.py         51      2    96%
src/beads_mcp/models.py         92      1    99%
src/beads_mcp/server.py         58     16    72%
src/beads_mcp/tools.py          59      0   100%
------------------------------------------------
TOTAL                          478     36    92%
```
This commit is contained in:
Baishampayan Ghose
2025-10-15 16:48:39 +05:30
parent 953858b853
commit 4353592fe6
7 changed files with 38 additions and 71 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.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.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_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.working_dir = working_dir if working_dir is not None else config.beads_working_dir
def _get_working_dir(self) -> str:
@@ -161,9 +157,7 @@ class BdClient:
)
stdout, stderr = await process.communicate()
except FileNotFoundError as e:
raise BdNotFoundError(
BdNotFoundError.installation_message(self.bd_path)
) from e
raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e
if process.returncode != 0:
raise BdCommandError(
@@ -205,9 +199,7 @@ class BdClient:
)
stdout, stderr = await process.communicate()
except FileNotFoundError as e:
raise BdNotFoundError(
BdNotFoundError.installation_message(self.bd_path)
) from e
raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e
if process.returncode != 0:
raise BdCommandError(
@@ -220,9 +212,7 @@ class BdClient:
version_output = stdout.decode().strip()
match = re.search(r"(\d+)\.(\d+)\.(\d+)", version_output)
if not match:
raise BdVersionError(
f"Could not parse bd version from: {version_output}"
)
raise BdVersionError(f"Could not parse bd version from: {version_output}")
version = tuple(int(x) for x in match.groups())
@@ -418,9 +408,7 @@ class BdClient:
)
_stdout, stderr = await process.communicate()
except FileNotFoundError as e:
raise BdNotFoundError(
BdNotFoundError.installation_message(self.bd_path)
) from e
raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e
if process.returncode != 0:
raise BdCommandError(
@@ -446,9 +434,7 @@ class BdClient:
)
stdout, stderr = await process.communicate()
except FileNotFoundError as e:
raise BdNotFoundError(
BdNotFoundError.installation_message(self.bd_path)
) from e
raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e
if process.returncode != 0:
raise BdCommandError(
@@ -513,9 +499,7 @@ class BdClient:
)
stdout, stderr = await process.communicate()
except FileNotFoundError as e:
raise BdNotFoundError(
BdNotFoundError.installation_message(self.bd_path)
) from e
raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e
if process.returncode != 0:
raise BdCommandError(

View File

@@ -5,7 +5,7 @@ import shutil
import sys
from pathlib import Path
from pydantic import field_validator
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -31,7 +31,7 @@ class Config(BaseSettings):
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_actor: str | None = None
beads_no_auto_flush: bool = False
@@ -71,9 +71,7 @@ class Config(BaseSettings):
)
if not os.access(v, os.X_OK):
raise ValueError(
f"bd executable at {v} is not executable.\nPlease check file permissions."
)
raise ValueError(f"bd executable at {v} is not executable.\nPlease check file permissions.")
return v
@@ -97,8 +95,7 @@ class Config(BaseSettings):
path = Path(v)
if not path.exists():
raise ValueError(
f"BEADS_DB points to non-existent file: {v}\n"
+ "Please verify the database path is correct."
f"BEADS_DB points to non-existent file: {v}\n" + "Please verify the database path is correct."
)
return v
@@ -124,7 +121,7 @@ def load_config() -> Config:
except Exception as e:
default_path = _default_beads_path()
error_msg = (
f"Beads MCP Server Configuration Error\n\n"
"Beads MCP Server Configuration Error\n\n"
+ f"{e}\n\n"
+ "Common fix: Install the bd CLI first:\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("\n=== All Environment Variables ===\n")
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")
return "".join(info)

View File

@@ -68,13 +68,9 @@ async def beads_ready_work(
async def beads_list_issues(
status: Annotated[
IssueStatus | None, "Filter by status (open, in_progress, blocked, closed)"
] = None,
status: Annotated[IssueStatus | None, "Filter by status (open, in_progress, blocked, closed)"] = None,
priority: Annotated[int | None, "Filter by priority (0-4, 0=highest)"] = None,
issue_type: Annotated[
IssueType | None, "Filter by type (bug, feature, task, epic, chore)"
] = None,
issue_type: Annotated[IssueType | None, "Filter by type (bug, feature, task, epic, chore)"] = None,
assignee: Annotated[str | None, "Filter by assignee"] = None,
limit: Annotated[int, "Maximum number of issues to return (1-1000)"] = 50,
) -> list[Issue]:
@@ -110,9 +106,7 @@ async def beads_create_issue(
acceptance: Annotated[str | None, "Acceptance criteria"] = None,
external_ref: Annotated[str | None, "External reference (e.g., gh-9, jira-ABC)"] = None,
priority: Annotated[int, "Priority (0-4, 0=highest)"] = 2,
issue_type: Annotated[
IssueType, "Type: bug, feature, task, epic, or chore"
] = DEFAULT_ISSUE_TYPE,
issue_type: Annotated[IssueType, "Type: bug, feature, task, epic, or chore"] = DEFAULT_ISSUE_TYPE,
assignee: Annotated[str | None, "Assignee username"] = None,
labels: Annotated[list[str] | None, "List of labels"] = 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(
prefix: Annotated[
str | None, "Issue prefix (e.g., 'myproject' for myproject-1, myproject-2)"
] = None,
prefix: Annotated[str | None, "Issue prefix (e.g., 'myproject' for myproject-1, myproject-2)"] = None,
) -> str:
"""Initialize bd in current directory.

View File

@@ -370,30 +370,22 @@ async def test_init_creates_beads_directory(bd_executable):
# Ensure .beads doesn't exist yet
assert not beads_dir.exists()
# Create client WITHOUT beads_db set (this was the bug!)
client = BdClient(bd_path=bd_executable, beads_db=None)
# Create client WITHOUT beads_db set and WITH working_dir set to temp_dir
client = BdClient(bd_path=bd_executable, beads_db=None, working_dir=temp_dir)
# Change to temp directory and run init
original_cwd = os.getcwd()
try:
os.chdir(temp_dir)
# Initialize with custom prefix (no need to chdir!)
params = InitParams(prefix="test")
result = await client.init(params)
# Initialize with custom prefix
params = InitParams(prefix="test")
result = await client.init(params)
# Verify .beads directory was created in temp directory
assert beads_dir.exists(), f".beads directory not created in {temp_dir}"
assert beads_dir.is_dir(), f".beads exists but is not a directory"
# Verify .beads directory was created in current directory
assert beads_dir.exists(), f".beads directory not created in {temp_dir}"
assert beads_dir.is_dir(), f".beads exists but is not a directory"
# Verify database file was created with correct prefix
db_files = list(beads_dir.glob("*.db"))
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
db_files = list(beads_dir.glob("*.db"))
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)
# Verify success message
assert "initialized" in result.lower() or "created" in result.lower()

View File

@@ -20,8 +20,10 @@ class TestConfig:
# Mock shutil.which to return a test path
with patch("shutil.which", return_value="/usr/local/bin/bd"):
config = Config()
assert config.beads_path == "/usr/local/bin/bd"
# Mock os.access to say the file is executable
with patch("os.access", return_value=True):
config = Config()
assert config.beads_path == "/usr/local/bin/bd"
def test_beads_path_from_env(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that BEADS_PATH environment variable is respected."""

View File

@@ -48,7 +48,7 @@ wheels = [
[[package]]
name = "beads-mcp"
version = "0.9.4"
version = "0.9.6"
source = { editable = "." }
dependencies = [
{ name = "fastmcp" },