Spaces:
Running
on
Zero
Running
on
Zero
| """ | |
| Comprehensive tests for warbler_cda.semantic_anchors module. | |
| Tests the SemanticAnchorGraph with mocked dependencies. | |
| """ | |
| import pytest | |
| from unittest.mock import Mock, patch | |
| import time | |
| class TestSemanticAnchorGraphInitialization: | |
| """Test SemanticAnchorGraph initialization.""" | |
| def test_graph_default_init(self): | |
| """Graph should initialize with default settings.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph() | |
| assert graph.embedding_provider is not None | |
| assert graph.memory_pool is not None | |
| assert graph.anchors == {} | |
| assert graph.clusters == {} | |
| assert graph.max_age_days == 30 | |
| assert graph.consolidation_threshold == 0.8 | |
| assert graph.eviction_heat_threshold == 0.1 | |
| def test_graph_custom_config(self): | |
| """Graph should accept custom configuration.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| config = { | |
| "max_age_days": 60, | |
| "consolidation_threshold": 0.9, | |
| "eviction_heat_threshold": 0.05, | |
| "enable_memory_pooling": False | |
| } | |
| graph = SemanticAnchorGraph(config=config) | |
| assert graph.max_age_days == 60 | |
| assert graph.consolidation_threshold == 0.9 | |
| assert graph.eviction_heat_threshold == 0.05 | |
| assert graph.enable_memory_pooling is False | |
| def test_graph_custom_embedding_provider(self): | |
| """Graph should accept custom embedding provider.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| mock_provider = Mock() | |
| graph = SemanticAnchorGraph(embedding_provider=mock_provider) | |
| assert graph.embedding_provider == mock_provider | |
| class TestCreateOrUpdateAnchor: | |
| """Test create_or_update_anchor method.""" | |
| def test_create_new_anchor(self): | |
| """create_or_update_anchor should create new anchor.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph() | |
| anchor_id = graph.create_or_update_anchor( | |
| concept_text="test concept", | |
| utterance_id="u1", | |
| context={"source": "test"} | |
| ) | |
| assert anchor_id in graph.anchors | |
| assert graph.anchors[anchor_id].concept_text == "test concept" | |
| assert graph.metrics["total_anchors_created"] == 1 | |
| def test_update_existing_anchor(self): | |
| """create_or_update_anchor should update similar anchor.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph(config={"consolidation_threshold": 0.7}) | |
| # Create first anchor | |
| anchor_id1 = graph.create_or_update_anchor( | |
| concept_text="test concept", | |
| utterance_id="u1", | |
| context={} | |
| ) | |
| initial_updates = graph.metrics["total_updates"] | |
| # Create very similar anchor - should update existing | |
| anchor_id2 = graph.create_or_update_anchor( | |
| concept_text="test concept", # Same text | |
| utterance_id="u2", | |
| context={} | |
| ) | |
| # Should be same anchor | |
| assert anchor_id1 == anchor_id2 | |
| assert graph.metrics["total_updates"] == initial_updates + 1 | |
| assert graph.anchors[anchor_id1].provenance.update_count >= 2 | |
| def test_create_anchor_with_privacy_hooks(self): | |
| """create_or_update_anchor should use privacy hooks if available.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| mock_privacy = Mock() | |
| mock_privacy.scrub_content_for_anchor_injection.return_value = ( | |
| "scrubbed content", | |
| {"privacy_hook_applied": True} | |
| ) | |
| mock_privacy.validate_privacy_compliance.return_value = (True, []) | |
| graph = SemanticAnchorGraph(privacy_hooks=mock_privacy) | |
| anchor_id = graph.create_or_update_anchor( | |
| concept_text="sensitive data", | |
| utterance_id="u1", | |
| context={} | |
| ) | |
| # Should have called privacy hooks | |
| mock_privacy.scrub_content_for_anchor_injection.assert_called_once() | |
| mock_privacy.validate_privacy_compliance.assert_called_once() | |
| class TestGetSemanticClusters: | |
| """Test get_semantic_clusters method.""" | |
| def test_get_clusters_empty_graph(self): | |
| """get_semantic_clusters should return empty list for empty graph.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph() | |
| clusters = graph.get_semantic_clusters() | |
| assert clusters == [] | |
| def test_get_clusters_single_anchor(self): | |
| """get_semantic_clusters should return empty list with single anchor.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph() | |
| graph.create_or_update_anchor("concept1", "u1", {}) | |
| clusters = graph.get_semantic_clusters() | |
| assert clusters == [] | |
| def test_get_clusters_similar_anchors(self): | |
| """get_semantic_clusters should group similar anchors.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph(config={"consolidation_threshold": 0.5}) | |
| # Create similar anchors | |
| graph.create_or_update_anchor("jumping", "u1", {}) | |
| time.sleep(0.01) | |
| graph.create_or_update_anchor("leaping", "u2", {}) | |
| time.sleep(0.01) | |
| graph.create_or_update_anchor("completely different topic", "u3", {}) | |
| clusters = graph.get_semantic_clusters(max_clusters=5) | |
| # Should find at least some clusters | |
| assert isinstance(clusters, list) | |
| class TestGetAnchorDiff: | |
| """Test get_anchor_diff method.""" | |
| def test_get_diff_no_changes(self): | |
| """get_anchor_diff should show no changes for future timestamp.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph() | |
| graph.create_or_update_anchor("concept", "u1", {}) | |
| # Query from future | |
| diff = graph.get_anchor_diff(since_timestamp=time.time() + 1000) | |
| assert len(diff["added"]) == 0 | |
| assert len(diff["updated"]) == 0 | |
| def test_get_diff_added_anchors(self): | |
| """get_anchor_diff should detect newly added anchors.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph(config={"consolidation_threshold": 0.95}) # Set high to prevent accidental consolidation | |
| past_time = time.time() - 100 | |
| graph.create_or_update_anchor("concept1", "u1", {}) | |
| graph.create_or_update_anchor("completely different concept2", "u2", {}) # Make it clearly different | |
| diff = graph.get_anchor_diff(since_timestamp=past_time) | |
| assert len(diff["added"]) == 2 | |
| assert diff["total_anchors"] == 2 | |
| class TestApplyLifecyclePolicies: | |
| """Test apply_lifecycle_policies method.""" | |
| def test_lifecycle_aging(self): | |
| """apply_lifecycle_policies should age all anchors.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph() | |
| graph.create_or_update_anchor("concept", "u1", {}) | |
| actions = graph.apply_lifecycle_policies() | |
| assert actions["aged"] == 1 | |
| def test_lifecycle_eviction_low_heat(self): | |
| """apply_lifecycle_policies should evict cold anchors.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph(config={"eviction_heat_threshold": 0.5}) | |
| anchor_id = graph.create_or_update_anchor("concept", "u1", {}) | |
| # Manually set low heat | |
| graph.anchors[anchor_id].heat = 0.1 | |
| actions = graph.apply_lifecycle_policies() | |
| assert actions["evicted"] == 1 | |
| assert anchor_id not in graph.anchors | |
| def test_lifecycle_eviction_old_age(self): | |
| """apply_lifecycle_policies should evict old anchors.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| from warbler_cda.anchor_data_classes import AnchorProvenance | |
| graph = SemanticAnchorGraph(config={"max_age_days": 1}) | |
| anchor_id = graph.create_or_update_anchor("concept", "u1", {}) | |
| # Manually set old timestamp (40 days ago) | |
| old_time = time.time() - (40 * 24 * 3600) | |
| graph.anchors[anchor_id].provenance.first_seen = old_time | |
| actions = graph.apply_lifecycle_policies() | |
| assert actions["evicted"] >= 1 | |
| class TestGetStabilityMetrics: | |
| """Test get_stability_metrics method.""" | |
| def test_stability_metrics_empty_graph(self): | |
| """get_stability_metrics should handle empty graph.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph() | |
| metrics = graph.get_stability_metrics() | |
| assert metrics["total_anchors"] == 0 | |
| assert metrics["average_age_days"] == 0 | |
| assert metrics["stability_score"] == 1.0 | |
| def test_stability_metrics_with_anchors(self): | |
| """get_stability_metrics should calculate metrics.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph(config={"consolidation_threshold": 0.95}) # Prevent consolidation | |
| graph.create_or_update_anchor("first concept", "u1", {}) | |
| graph.create_or_update_anchor("very different second concept", "u2", {}) | |
| metrics = graph.get_stability_metrics() | |
| assert metrics["total_anchors"] == 2 | |
| assert metrics["average_heat"] > 0 | |
| assert 0 <= metrics["stability_score"] <= 1.0 | |
| assert "provider_info" in metrics | |
| class TestPrivacyIntegration: | |
| """Test privacy hooks integration.""" | |
| def test_get_privacy_metrics_no_hooks(self): | |
| """get_privacy_metrics should handle missing privacy hooks.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph() | |
| metrics = graph.get_privacy_metrics() | |
| assert metrics["privacy_hooks_enabled"] is False | |
| def test_get_privacy_metrics_with_hooks(self): | |
| """get_privacy_metrics should call privacy hooks.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| mock_privacy = Mock() | |
| mock_privacy.get_privacy_metrics.return_value = { | |
| "pii_detections": 5, | |
| "scrubbing_applied": 3 | |
| } | |
| graph = SemanticAnchorGraph(privacy_hooks=mock_privacy) | |
| metrics = graph.get_privacy_metrics() | |
| assert metrics["pii_detections"] == 5 | |
| mock_privacy.get_privacy_metrics.assert_called_once() | |
| class TestHelperMethods: | |
| """Test helper methods.""" | |
| def test_generate_anchor_id(self): | |
| """_generate_anchor_id should create unique IDs.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph() | |
| id1 = graph._generate_anchor_id("concept1") | |
| time.sleep(0.01) | |
| id2 = graph._generate_anchor_id("concept2") | |
| assert id1 != id2 | |
| assert id1.startswith("anchor_") | |
| assert id2.startswith("anchor_") | |
| def test_calculate_drift(self): | |
| """_calculate_drift should compute semantic drift.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph() | |
| # Identical embeddings = 0 drift | |
| emb1 = [1.0, 0.0, 0.0] | |
| emb2 = [1.0, 0.0, 0.0] | |
| drift = graph._calculate_drift(emb1, emb2) | |
| assert drift < 0.01 | |
| # Different embeddings = higher drift | |
| emb3 = [0.0, 1.0, 0.0] | |
| drift2 = graph._calculate_drift(emb1, emb3) | |
| assert drift2 > 0.5 | |
| def test_find_similar_anchor_none(self): | |
| """_find_similar_anchor should return None when no similar anchors.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph() | |
| result = graph._find_similar_anchor([0.1, 0.2, 0.3]) | |
| assert result is None | |
| def test_find_similar_anchor_match(self): | |
| """_find_similar_anchor should find similar anchor.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph(config={"consolidation_threshold": 0.7}) | |
| anchor_id = graph.create_or_update_anchor("test", "u1", {}) | |
| # Get the embedding | |
| anchor = graph.anchors[anchor_id] | |
| similar_id = graph._find_similar_anchor(anchor.embedding) | |
| assert similar_id == anchor_id | |
| class TestIntegration: | |
| """Integration tests for complete anchor lifecycle.""" | |
| def test_full_anchor_lifecycle(self): | |
| """Test complete lifecycle: create, update, age, evict.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph(config={ | |
| "max_age_days": 1, | |
| "eviction_heat_threshold": 0.05 | |
| }) | |
| # Create anchor | |
| anchor_id = graph.create_or_update_anchor("concept", "u1", {}) | |
| assert anchor_id in graph.anchors | |
| # Update anchor | |
| graph.create_or_update_anchor("concept", "u2", {}) | |
| assert graph.anchors[anchor_id].provenance.update_count >= 2 | |
| # Age anchor (set old timestamp) | |
| graph.anchors[anchor_id].provenance.first_seen = time.time() - (10 * 24 * 3600) | |
| # Apply lifecycle | |
| actions = graph.apply_lifecycle_policies() | |
| # Should be evicted due to age | |
| assert actions["evicted"] >= 1 | |
| def test_stability_metrics_workflow(self): | |
| """Test stability metrics calculation workflow.""" | |
| from warbler_cda.semantic_anchors import SemanticAnchorGraph | |
| graph = SemanticAnchorGraph(config={"consolidation_threshold": 0.95}) # Prevent consolidation | |
| # Create multiple distinct anchors | |
| distinct_concepts = [ | |
| "quantum physics fundamentals", | |
| "machine learning algorithms", | |
| "ancient Greek philosophy", | |
| "modern art techniques", | |
| "sustainable agriculture" | |
| ] | |
| for i, concept in enumerate(distinct_concepts): | |
| graph.create_or_update_anchor(concept, f"u{i}", {}) | |
| # Get metrics | |
| metrics = graph.get_stability_metrics() | |
| assert metrics["total_anchors"] == 5 | |
| assert metrics["average_heat"] > 0 | |
| assert "provider_info" in metrics | |
| assert "memory_pool_metrics" in metrics | |