mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
ed9487708e
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
279 lines
10 KiB
Python
279 lines
10 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_cloud_mode_requires_project(self):
|
|
"""In cloud mode, project is required."""
|
|
resolver = ProjectResolver(cloud_mode=True)
|
|
with pytest.raises(ValueError, match="Project is required for cloud mode"):
|
|
resolver.resolve(project=None)
|
|
|
|
def test_cloud_mode_with_explicit_project(self):
|
|
"""In cloud mode, explicit project is accepted."""
|
|
resolver = ProjectResolver(cloud_mode=True)
|
|
result = resolver.resolve(project="my-project")
|
|
|
|
assert result.project == "my-project"
|
|
assert result.mode == ResolutionMode.CLOUD_EXPLICIT
|
|
assert result.is_resolved is True
|
|
assert result.is_discovery_mode is False
|
|
|
|
def test_cloud_mode_discovery_allowed(self):
|
|
"""In cloud mode with allow_discovery, None is acceptable."""
|
|
resolver = ProjectResolver(cloud_mode=True)
|
|
result = resolver.resolve(project=None, allow_discovery=True)
|
|
|
|
assert result.project is None
|
|
assert result.mode == ResolutionMode.CLOUD_DISCOVERY
|
|
assert result.is_resolved is False
|
|
assert result.is_discovery_mode is True
|
|
|
|
def test_local_mode_env_constraint_priority(self, monkeypatch):
|
|
"""Env constraint has highest priority in local mode."""
|
|
monkeypatch.setenv("BASIC_MEMORY_MCP_PROJECT", "constrained-project")
|
|
resolver = ProjectResolver.from_env(
|
|
cloud_mode=False,
|
|
default_project_mode=True,
|
|
default_project="default-project",
|
|
)
|
|
|
|
# Even with explicit project and default, env constraint wins
|
|
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_local_mode_explicit_project(self):
|
|
"""Explicit project parameter has second priority."""
|
|
resolver = ProjectResolver(
|
|
cloud_mode=False,
|
|
default_project_mode=True,
|
|
default_project="default-project",
|
|
)
|
|
|
|
result = resolver.resolve(project="explicit-project")
|
|
|
|
assert result.project == "explicit-project"
|
|
assert result.mode == ResolutionMode.EXPLICIT
|
|
|
|
def test_local_mode_default_project(self):
|
|
"""Default project is used when default_project_mode is true."""
|
|
resolver = ProjectResolver(
|
|
cloud_mode=False,
|
|
default_project_mode=True,
|
|
default_project="my-default",
|
|
)
|
|
|
|
result = resolver.resolve(project=None)
|
|
|
|
assert result.project == "my-default"
|
|
assert result.mode == ResolutionMode.DEFAULT
|
|
|
|
def test_local_mode_no_default_when_mode_disabled(self):
|
|
"""Default project is NOT used when default_project_mode is false."""
|
|
resolver = ProjectResolver(
|
|
cloud_mode=False,
|
|
default_project_mode=False,
|
|
default_project="my-default",
|
|
)
|
|
|
|
result = resolver.resolve(project=None)
|
|
|
|
assert result.project is None
|
|
assert result.mode == ResolutionMode.NONE
|
|
assert result.is_resolved is False
|
|
|
|
def test_local_mode_no_resolution_possible(self):
|
|
"""When nothing is configured, resolution returns None."""
|
|
resolver = ProjectResolver(cloud_mode=False)
|
|
result = resolver.resolve(project=None)
|
|
|
|
assert result.project is None
|
|
assert result.mode == ResolutionMode.NONE
|
|
assert "no default project configured" in result.reason
|
|
|
|
def test_require_project_success(self):
|
|
"""require_project returns result when project resolved."""
|
|
resolver = ProjectResolver(
|
|
cloud_mode=False,
|
|
default_project_mode=True,
|
|
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 not resolved."""
|
|
resolver = ProjectResolver(cloud_mode=False, default_project_mode=False)
|
|
|
|
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(cloud_mode=False, default_project_mode=False)
|
|
|
|
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(
|
|
cloud_mode=False,
|
|
default_project_mode=True,
|
|
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"
|
|
|
|
def test_cloud_mode_uses_default_project(self):
|
|
"""In cloud mode, default project is used when default_project_mode=True."""
|
|
resolver = ProjectResolver(
|
|
cloud_mode=True,
|
|
default_project_mode=True,
|
|
default_project="my-default",
|
|
)
|
|
result = resolver.resolve(project=None)
|
|
|
|
assert result.project == "my-default"
|
|
assert result.mode == ResolutionMode.DEFAULT
|
|
assert result.is_resolved is True
|
|
|
|
def test_cloud_mode_no_default_still_requires_project(self):
|
|
"""In cloud mode with default_project_mode=False, project is still required."""
|
|
resolver = ProjectResolver(
|
|
cloud_mode=True,
|
|
default_project_mode=False,
|
|
)
|
|
with pytest.raises(ValueError, match="Project is required for cloud mode"):
|
|
resolver.resolve(project=None)
|
|
|
|
def test_cloud_mode_explicit_overrides_default(self):
|
|
"""In cloud mode, explicit project wins over default project."""
|
|
resolver = ProjectResolver(
|
|
cloud_mode=True,
|
|
default_project_mode=True,
|
|
default_project="my-default",
|
|
)
|
|
result = resolver.resolve(project="explicit-project")
|
|
|
|
assert result.project == "explicit-project"
|
|
assert result.mode == ResolutionMode.CLOUD_EXPLICIT
|
|
|
|
def test_cloud_mode_default_mode_true_but_no_default_project(self):
|
|
"""In cloud mode with default_project_mode=True but no default_project, still raises."""
|
|
resolver = ProjectResolver(
|
|
cloud_mode=True,
|
|
default_project_mode=True,
|
|
default_project=None,
|
|
)
|
|
with pytest.raises(ValueError, match="Project is required for cloud mode"):
|
|
resolver.resolve(project=None)
|
|
|
|
def test_cloud_mode_discovery_fallback_after_no_default(self):
|
|
"""In cloud mode, discovery still works as last resort when no default is configured."""
|
|
resolver = ProjectResolver(
|
|
cloud_mode=True,
|
|
default_project_mode=False,
|
|
)
|
|
result = resolver.resolve(project=None, allow_discovery=True)
|
|
|
|
assert result.project is None
|
|
assert result.mode == ResolutionMode.CLOUD_DISCOVERY
|
|
assert result.is_discovery_mode is True
|
|
|
|
def test_cloud_mode_env_constraint_overrides_everything(self, monkeypatch):
|
|
"""In cloud mode, env constraint has highest priority."""
|
|
monkeypatch.setenv("BASIC_MEMORY_MCP_PROJECT", "constrained-project")
|
|
resolver = ProjectResolver.from_env(
|
|
cloud_mode=True,
|
|
default_project_mode=True,
|
|
default_project="default-project",
|
|
)
|
|
result = resolver.resolve(project="explicit-project")
|
|
|
|
assert result.project == "constrained-project"
|
|
assert result.mode == ResolutionMode.ENV_CONSTRAINT
|
|
|
|
|
|
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_cloud(self):
|
|
"""is_discovery_mode is True for CLOUD_DISCOVERY."""
|
|
result = ResolvedProject(
|
|
project=None,
|
|
mode=ResolutionMode.CLOUD_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
|