mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
0239f4abb4
Signed-off-by: phernandez <paul@basicmachines.co>
161 lines
5.6 KiB
Python
161 lines
5.6 KiB
Python
"""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
|