Skip to main content
Glama
test_cluster_detector.py10.4 kB
"""Unit tests for ClusterDetector (T036-T037). Tests the action determination and confidence calculation logic in isolation from storage and clustering algorithms. """ from __future__ import annotations from unittest.mock import MagicMock, patch import pytest from cortexgraph.agents.cluster_detector import ( LINK_THRESHOLD, MERGE_THRESHOLD, MIN_CLUSTER_SIZE, ClusterDetector, ) from cortexgraph.agents.models import ClusterAction # ============================================================================= # Test Fixtures # ============================================================================= @pytest.fixture def detector() -> ClusterDetector: """Create ClusterDetector with mock storage.""" mock_storage = MagicMock() mock_storage.memories = {} with patch("cortexgraph.agents.cluster_detector.get_storage", return_value=mock_storage): d = ClusterDetector(dry_run=True) d._storage = mock_storage return d # ============================================================================= # T036: Unit Tests for Action Determination # ============================================================================= class TestActionDetermination: """Tests for _determine_action() method (T036).""" def test_merge_action_high_cohesion(self, detector: ClusterDetector) -> None: """Cohesion >= 0.75 should return MERGE action.""" assert detector._determine_action(0.75) == ClusterAction.MERGE assert detector._determine_action(0.80) == ClusterAction.MERGE assert detector._determine_action(0.90) == ClusterAction.MERGE assert detector._determine_action(1.0) == ClusterAction.MERGE def test_merge_action_boundary(self, detector: ClusterDetector) -> None: """Test MERGE threshold boundary.""" # Just below threshold - LINK assert detector._determine_action(0.749) == ClusterAction.LINK # At threshold - MERGE assert detector._determine_action(MERGE_THRESHOLD) == ClusterAction.MERGE def test_link_action_medium_cohesion(self, detector: ClusterDetector) -> None: """Cohesion 0.40-0.75 should return LINK action.""" assert detector._determine_action(0.40) == ClusterAction.LINK assert detector._determine_action(0.50) == ClusterAction.LINK assert detector._determine_action(0.60) == ClusterAction.LINK assert detector._determine_action(0.70) == ClusterAction.LINK def test_link_action_boundary(self, detector: ClusterDetector) -> None: """Test LINK threshold boundary.""" # Just below threshold - IGNORE assert detector._determine_action(0.399) == ClusterAction.IGNORE # At threshold - LINK assert detector._determine_action(LINK_THRESHOLD) == ClusterAction.LINK def test_ignore_action_low_cohesion(self, detector: ClusterDetector) -> None: """Cohesion < 0.40 should return IGNORE action.""" assert detector._determine_action(0.0) == ClusterAction.IGNORE assert detector._determine_action(0.10) == ClusterAction.IGNORE assert detector._determine_action(0.20) == ClusterAction.IGNORE assert detector._determine_action(0.30) == ClusterAction.IGNORE assert detector._determine_action(0.39) == ClusterAction.IGNORE def test_thresholds_documented(self) -> None: """Verify thresholds match documentation.""" assert MERGE_THRESHOLD == 0.75 assert LINK_THRESHOLD == 0.40 assert MIN_CLUSTER_SIZE == 2 # ============================================================================= # T037: Unit Tests for Confidence Calculation # ============================================================================= class TestConfidenceCalculation: """Tests for _calculate_confidence() method (T037).""" def test_confidence_increases_with_cohesion(self, detector: ClusterDetector) -> None: """Higher cohesion should produce higher confidence.""" conf_low = detector._calculate_confidence(0.30, 3) conf_mid = detector._calculate_confidence(0.50, 3) conf_high = detector._calculate_confidence(0.80, 3) assert conf_low < conf_mid < conf_high def test_confidence_in_valid_range(self, detector: ClusterDetector) -> None: """Confidence should always be in [0.0, 1.0] range.""" # Test various combinations test_cases = [ (0.0, 2), (0.5, 3), (1.0, 5), (0.9, 10), # Very large cluster (0.1, 2), # Low cohesion small cluster ] for cohesion, size in test_cases: confidence = detector._calculate_confidence(cohesion, size) assert 0.0 <= confidence <= 1.0, f"Failed for cohesion={cohesion}, size={size}" def test_confidence_optimal_cluster_size(self, detector: ClusterDetector) -> None: """Optimal cluster size (3-5) should have highest confidence factor.""" cohesion = 0.80 # Small cluster (size 2) - lower confidence conf_small = detector._calculate_confidence(cohesion, 2) # Optimal cluster (size 3-5) - higher confidence conf_optimal = detector._calculate_confidence(cohesion, 4) # Large cluster (size 10) - lower confidence conf_large = detector._calculate_confidence(cohesion, 10) assert conf_optimal > conf_small assert conf_optimal > conf_large def test_confidence_small_cluster_penalty(self, detector: ClusterDetector) -> None: """Clusters of size 2 should have reduced confidence.""" cohesion = 0.80 conf_size_2 = detector._calculate_confidence(cohesion, 2) conf_size_3 = detector._calculate_confidence(cohesion, 3) # Size 2 should be less confident than size 3 assert conf_size_2 < conf_size_3 def test_confidence_large_cluster_penalty(self, detector: ClusterDetector) -> None: """Very large clusters should have reduced confidence.""" cohesion = 0.80 conf_size_5 = detector._calculate_confidence(cohesion, 5) conf_size_9 = detector._calculate_confidence(cohesion, 9) conf_size_15 = detector._calculate_confidence(cohesion, 15) # Larger clusters should be less confident assert conf_size_5 >= conf_size_9 >= conf_size_15 def test_confidence_zero_cohesion(self, detector: ClusterDetector) -> None: """Zero cohesion should produce zero or near-zero confidence.""" confidence = detector._calculate_confidence(0.0, 3) assert confidence == 0.0 def test_confidence_perfect_cohesion(self, detector: ClusterDetector) -> None: """Perfect cohesion with optimal size should produce high confidence.""" confidence = detector._calculate_confidence(1.0, 4) # Should be very high (1.0 * 1.0 = 1.0 for optimal size) assert confidence >= 0.9 # ============================================================================= # Additional Unit Tests # ============================================================================= class TestClusterKeyGeneration: """Tests for _cluster_key() method.""" def test_cluster_key_sorted(self, detector: ClusterDetector) -> None: """Cluster key should be sorted for consistency.""" key1 = detector._cluster_key(["mem-3", "mem-1", "mem-2"]) key2 = detector._cluster_key(["mem-1", "mem-2", "mem-3"]) key3 = detector._cluster_key(["mem-2", "mem-3", "mem-1"]) # All should produce same key assert key1 == key2 == key3 def test_cluster_key_format(self, detector: ClusterDetector) -> None: """Cluster key should be pipe-separated sorted IDs.""" key = detector._cluster_key(["b", "a", "c"]) assert key == "a|b|c" class TestProcessItemWithCache: """Tests for process_item() using cached values.""" def test_process_item_uses_cached_cohesion(self, detector: ClusterDetector) -> None: """process_item() should use cached cohesion values.""" # Set up cache detector._storage.memories = { "mem-1": MagicMock(id="mem-1"), "mem-2": MagicMock(id="mem-2"), } detector._cached_clusters = { "mem-1": ["mem-1", "mem-2"], "mem-2": ["mem-1", "mem-2"], } detector._cached_cohesion = { "mem-1|mem-2": 0.85, # High cohesion } result = detector.process_item("mem-1") assert result.cohesion == 0.85 assert result.action == ClusterAction.MERGE def test_process_item_returns_correct_members(self, detector: ClusterDetector) -> None: """process_item() should return correct cluster members.""" # Set up cache detector._storage.memories = { "mem-1": MagicMock(id="mem-1"), "mem-2": MagicMock(id="mem-2"), "mem-3": MagicMock(id="mem-3"), } detector._cached_clusters = { "mem-1": ["mem-1", "mem-2", "mem-3"], "mem-2": ["mem-1", "mem-2", "mem-3"], "mem-3": ["mem-1", "mem-2", "mem-3"], } detector._cached_cohesion = { "mem-1|mem-2|mem-3": 0.55, } result = detector.process_item("mem-2") assert set(result.memory_ids) == {"mem-1", "mem-2", "mem-3"} assert result.action == ClusterAction.LINK class TestInitialization: """Tests for ClusterDetector initialization.""" def test_default_values(self) -> None: """Test default initialization values.""" with patch("cortexgraph.agents.cluster_detector.get_storage", return_value=MagicMock()): detector = ClusterDetector() assert detector.dry_run is False assert detector.similarity_threshold == 0.40 assert detector.min_cluster_size == 2 def test_custom_values(self) -> None: """Test custom initialization values.""" with patch("cortexgraph.agents.cluster_detector.get_storage", return_value=MagicMock()): detector = ClusterDetector( dry_run=True, rate_limit=50, similarity_threshold=0.60, min_cluster_size=3, ) assert detector.dry_run is True assert detector.rate_limit == 50 assert detector.similarity_threshold == 0.60 assert detector.min_cluster_size == 3

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/prefrontalsys/mnemex'

If you have feedback or need assistance with the MCP directory API, please join our Discord server