"""Tests for ProjectResolver - unified project resolution logic."""
import pytest
from basic_memory.project_resolver import (
ProjectResolver,
ResolvedProject,
ResolutionMode,
)
class TestProjectResolver:
"""Test ProjectResolver class."""
def test_env_constraint_has_highest_priority(self, monkeypatch):
"""Environment constraint should win over explicit/default."""
monkeypatch.setenv("BASIC_MEMORY_MCP_PROJECT", "constrained-project")
resolver = ProjectResolver.from_env(default_project="default-project")
result = resolver.resolve(project="explicit-project")
assert result.project == "constrained-project"
assert result.mode == ResolutionMode.ENV_CONSTRAINT
assert result.is_resolved is True
def test_explicit_project_has_second_priority(self):
"""Explicit project parameter should override default."""
resolver = ProjectResolver(default_project="default-project")
result = resolver.resolve(project="explicit-project")
assert result.project == "explicit-project"
assert result.mode == ResolutionMode.EXPLICIT
def test_default_project_is_used_as_fallback(self):
"""Default project should be used when explicit is missing."""
resolver = ProjectResolver(default_project="my-default")
result = resolver.resolve(project=None)
assert result.project == "my-default"
assert result.mode == ResolutionMode.DEFAULT
def test_no_resolution_when_no_default_and_no_discovery(self):
"""Without explicit/default/discovery, resolution should return NONE."""
resolver = ProjectResolver(default_project=None)
result = resolver.resolve(project=None)
assert result.project is None
assert result.mode == ResolutionMode.NONE
assert result.is_resolved is False
def test_discovery_resolution_when_allowed(self):
"""Discovery mode should return DISCOVERY when allowed."""
resolver = ProjectResolver(default_project=None)
result = resolver.resolve(project=None, allow_discovery=True)
assert result.project is None
assert result.mode == ResolutionMode.DISCOVERY
assert result.is_discovery_mode is True
def test_require_project_success(self):
"""require_project returns result when project resolves."""
resolver = ProjectResolver(default_project="required-project")
result = resolver.require_project()
assert result.project == "required-project"
assert result.is_resolved is True
def test_require_project_raises_on_failure(self):
"""require_project raises ValueError when project cannot resolve."""
resolver = ProjectResolver(default_project=None)
with pytest.raises(ValueError, match="No project specified"):
resolver.require_project()
def test_require_project_custom_error_message(self):
"""require_project uses custom error message."""
resolver = ProjectResolver(default_project=None)
with pytest.raises(ValueError, match="Custom error message"):
resolver.require_project(error_message="Custom error message")
def test_from_env_without_env_var(self, monkeypatch):
"""from_env without BASIC_MEMORY_MCP_PROJECT set."""
monkeypatch.delenv("BASIC_MEMORY_MCP_PROJECT", raising=False)
resolver = ProjectResolver.from_env(default_project="test")
assert resolver.constrained_project is None
result = resolver.resolve(project="explicit")
assert result.mode == ResolutionMode.EXPLICIT
def test_from_env_with_env_var(self, monkeypatch):
"""from_env with BASIC_MEMORY_MCP_PROJECT set."""
monkeypatch.setenv("BASIC_MEMORY_MCP_PROJECT", "env-project")
resolver = ProjectResolver.from_env()
assert resolver.constrained_project == "env-project"
class TestResolvedProject:
"""Test ResolvedProject dataclass."""
def test_is_resolved_true(self):
"""is_resolved returns True when project is set."""
result = ResolvedProject(
project="test",
mode=ResolutionMode.EXPLICIT,
reason="test",
)
assert result.is_resolved is True
def test_is_resolved_false(self):
"""is_resolved returns False when project is None."""
result = ResolvedProject(
project=None,
mode=ResolutionMode.NONE,
reason="test",
)
assert result.is_resolved is False
def test_is_discovery_mode_discovery(self):
"""is_discovery_mode is True for DISCOVERY."""
result = ResolvedProject(
project=None,
mode=ResolutionMode.DISCOVERY,
reason="test",
)
assert result.is_discovery_mode is True
def test_is_discovery_mode_none(self):
"""is_discovery_mode is True for NONE with no project."""
result = ResolvedProject(
project=None,
mode=ResolutionMode.NONE,
reason="test",
)
assert result.is_discovery_mode is True
def test_is_discovery_mode_false(self):
"""is_discovery_mode is False when project is resolved."""
result = ResolvedProject(
project="test",
mode=ResolutionMode.EXPLICIT,
reason="test",
)
assert result.is_discovery_mode is False
def test_frozen_dataclass(self):
"""ResolvedProject is immutable."""
result = ResolvedProject(
project="test",
mode=ResolutionMode.EXPLICIT,
reason="test",
)
with pytest.raises(AttributeError):
result.project = "changed" # type: ignore