"""
Tests for issue tracker abstraction module.
"""
import os
import pytest
from maniple_mcp.config import IssueTrackerConfig, default_config
from maniple_mcp.issue_tracker import (
BEADS_BACKEND,
ISSUE_TRACKER_ENV_VAR,
PEBBLES_BACKEND,
IssueTrackerBackend,
detect_issue_tracker,
)
class TestIssueTrackerBackendProtocol:
"""Tests for IssueTrackerBackend protocol compliance."""
def test_beads_backend_is_protocol(self):
"""Beads backend should satisfy IssueTrackerBackend protocol."""
assert isinstance(BEADS_BACKEND, IssueTrackerBackend)
def test_pebbles_backend_is_protocol(self):
"""Pebbles backend should satisfy IssueTrackerBackend protocol."""
assert isinstance(PEBBLES_BACKEND, IssueTrackerBackend)
class TestBackendCommands:
"""Tests for backend command templates."""
def test_beads_commands_include_expected_keys(self):
"""Beads commands should include all required templates."""
expected_keys = {
"list",
"ready",
"show",
"update",
"close",
"create",
"comment",
"dep_add",
"dep_tree",
}
assert set(BEADS_BACKEND.commands.keys()) == expected_keys
def test_pebbles_commands_include_expected_keys(self):
"""Pebbles commands should include all required templates."""
expected_keys = {
"list",
"ready",
"show",
"update",
"close",
"create",
"comment",
"dep_add",
"dep_tree",
}
assert set(PEBBLES_BACKEND.commands.keys()) == expected_keys
def test_beads_commands_start_with_cli(self):
"""Beads commands should start with the beads CLI prefix."""
for command in BEADS_BACKEND.commands.values():
assert command.startswith("bd --no-db")
def test_pebbles_commands_start_with_cli(self):
"""Pebbles commands should start with the pebbles CLI prefix."""
for command in PEBBLES_BACKEND.commands.values():
assert command.startswith("pb ")
class TestDetectIssueTracker:
"""Tests for issue tracker detection."""
def test_detects_beads(self, tmp_path):
"""Detection should return beads backend when .beads exists."""
project_path = tmp_path / "repo"
project_path.mkdir()
(project_path / ".beads").mkdir()
detected = detect_issue_tracker(str(project_path))
assert detected == BEADS_BACKEND
def test_detects_pebbles(self, tmp_path):
"""Detection should return pebbles backend when .pebbles exists."""
project_path = tmp_path / "repo"
project_path.mkdir()
(project_path / ".pebbles").mkdir()
detected = detect_issue_tracker(str(project_path))
assert detected == PEBBLES_BACKEND
def test_detects_pebbles_when_both_markers_present(self, tmp_path, caplog):
"""Detection should prefer pebbles when both markers are present."""
project_path = tmp_path / "repo"
project_path.mkdir()
(project_path / ".beads").mkdir()
(project_path / ".pebbles").mkdir()
detected = detect_issue_tracker(str(project_path))
assert detected == PEBBLES_BACKEND
assert "defaulting to pebbles" in caplog.text
def test_returns_none_when_no_marker(self, tmp_path):
"""Detection should return None when no tracker marker exists."""
project_path = tmp_path / "repo"
project_path.mkdir()
detected = detect_issue_tracker(str(project_path))
assert detected is None
class TestEnvVarOverride:
"""Tests for MANIPLE_ISSUE_TRACKER environment variable override."""
@pytest.fixture(autouse=True)
def clean_env(self, monkeypatch):
"""Ensure the env var is cleared before each test."""
monkeypatch.delenv(ISSUE_TRACKER_ENV_VAR, raising=False)
monkeypatch.delenv("CLAUDE_TEAM_ISSUE_TRACKER", raising=False)
def test_env_var_selects_beads(self, tmp_path, monkeypatch):
"""Env var should select beads even with no markers."""
project_path = tmp_path / "repo"
project_path.mkdir()
monkeypatch.setenv(ISSUE_TRACKER_ENV_VAR, "beads")
detected = detect_issue_tracker(str(project_path))
assert detected == BEADS_BACKEND
def test_env_var_selects_pebbles(self, tmp_path, monkeypatch):
"""Env var should select pebbles even with no markers."""
project_path = tmp_path / "repo"
project_path.mkdir()
monkeypatch.setenv(ISSUE_TRACKER_ENV_VAR, "pebbles")
detected = detect_issue_tracker(str(project_path))
assert detected == PEBBLES_BACKEND
def test_env_var_overrides_markers(self, tmp_path, monkeypatch):
"""Env var should take priority over marker detection."""
project_path = tmp_path / "repo"
project_path.mkdir()
(project_path / ".pebbles").mkdir() # Pebbles marker
monkeypatch.setenv(ISSUE_TRACKER_ENV_VAR, "beads")
detected = detect_issue_tracker(str(project_path))
assert detected == BEADS_BACKEND
def test_env_var_case_insensitive(self, tmp_path, monkeypatch):
"""Env var should be case-insensitive."""
project_path = tmp_path / "repo"
project_path.mkdir()
monkeypatch.setenv(ISSUE_TRACKER_ENV_VAR, "BEADS")
detected = detect_issue_tracker(str(project_path))
assert detected == BEADS_BACKEND
def test_invalid_env_var_falls_through(self, tmp_path, monkeypatch, caplog):
"""Invalid env var value should log warning and fall through."""
project_path = tmp_path / "repo"
project_path.mkdir()
(project_path / ".pebbles").mkdir()
monkeypatch.setenv(ISSUE_TRACKER_ENV_VAR, "invalid_tracker")
detected = detect_issue_tracker(str(project_path))
assert detected == PEBBLES_BACKEND
assert "Unknown issue tracker 'invalid_tracker'" in caplog.text
def test_deprecated_env_var_selects_beads(self, tmp_path, monkeypatch):
"""Deprecated CLAUDE_TEAM_ISSUE_TRACKER should still be respected."""
project_path = tmp_path / "repo"
project_path.mkdir()
monkeypatch.setenv("CLAUDE_TEAM_ISSUE_TRACKER", "beads")
detected = detect_issue_tracker(str(project_path))
assert detected == BEADS_BACKEND
def test_env_precedence(self, tmp_path, monkeypatch):
"""MANIPLE_ISSUE_TRACKER should take precedence over CLAUDE_TEAM_ISSUE_TRACKER."""
project_path = tmp_path / "repo"
project_path.mkdir()
monkeypatch.setenv("CLAUDE_TEAM_ISSUE_TRACKER", "pebbles")
monkeypatch.setenv(ISSUE_TRACKER_ENV_VAR, "beads")
detected = detect_issue_tracker(str(project_path))
assert detected == BEADS_BACKEND
class TestConfigOverride:
"""Tests for config.issue_tracker.override setting."""
@pytest.fixture(autouse=True)
def clean_env(self, monkeypatch):
"""Ensure the env var is cleared before each test."""
monkeypatch.delenv(ISSUE_TRACKER_ENV_VAR, raising=False)
monkeypatch.delenv("CLAUDE_TEAM_ISSUE_TRACKER", raising=False)
def test_config_override_selects_beads(self, tmp_path):
"""Config override should select beads even with no markers."""
project_path = tmp_path / "repo"
project_path.mkdir()
config = default_config()
config.issue_tracker = IssueTrackerConfig(override="beads")
detected = detect_issue_tracker(str(project_path), config=config)
assert detected == BEADS_BACKEND
def test_config_override_selects_pebbles(self, tmp_path):
"""Config override should select pebbles even with no markers."""
project_path = tmp_path / "repo"
project_path.mkdir()
config = default_config()
config.issue_tracker = IssueTrackerConfig(override="pebbles")
detected = detect_issue_tracker(str(project_path), config=config)
assert detected == PEBBLES_BACKEND
def test_config_override_overrides_markers(self, tmp_path):
"""Config override should take priority over marker detection."""
project_path = tmp_path / "repo"
project_path.mkdir()
(project_path / ".pebbles").mkdir() # Pebbles marker
config = default_config()
config.issue_tracker = IssueTrackerConfig(override="beads")
detected = detect_issue_tracker(str(project_path), config=config)
assert detected == BEADS_BACKEND
def test_env_var_overrides_config(self, tmp_path, monkeypatch):
"""Env var should take priority over config override."""
project_path = tmp_path / "repo"
project_path.mkdir()
config = default_config()
config.issue_tracker = IssueTrackerConfig(override="pebbles")
monkeypatch.setenv(ISSUE_TRACKER_ENV_VAR, "beads")
detected = detect_issue_tracker(str(project_path), config=config)
assert detected == BEADS_BACKEND
def test_none_override_falls_through_to_markers(self, tmp_path):
"""None config override should fall through to marker detection."""
project_path = tmp_path / "repo"
project_path.mkdir()
(project_path / ".beads").mkdir()
config = default_config()
config.issue_tracker = IssueTrackerConfig(override=None)
detected = detect_issue_tracker(str(project_path), config=config)
assert detected == BEADS_BACKEND