#!/usr/bin/env python3 """Phase 5.1 Pattern Matcher tests.""" import asyncio import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent / "ontology_platform")) from ont_platform.core.graph.pattern_matcher import PatternMatcher, PathResult, CycleResult class MockAdapter: """Mock Neo4j adapter for testing.""" async def execute_cypher(self, cypher: str, params=None): """Mock Cypher execution.""" params = params or {} # find_paths if "shortestPath" in cypher or "RELATES*" in cypher and "start_id" in params: return [ { "result": { "path": [params.get("start_id"), 2, 3, params.get("end_id")], "length": 3, "confidence": 0.87, } }, { "result": { "path": [params.get("start_id"), 5, params.get("end_id")], "length": 2, "confidence": 0.90, } }, ] # find_cycles if "start)-[" in cypher or ("RELATES*" in cypher and "end_id" not in params): return [ {"result": {"cycle": [1, 2, 3, 1], "length": 3}}, {"result": {"cycle": [4, 5, 6, 4], "length": 3}}, ] # find_motifs - detect by type in params motif_type = params.get("motif_type", "") if motif_type == "triangle": return [ { "result": { "motif_type": "triangle", "nodes": [1, 2, 3], "labels": ["Entity_1", "Entity_2", "Entity_3"], } }, { "result": { "motif_type": "triangle", "nodes": [4, 5, 6], "labels": ["Entity_4", "Entity_5", "Entity_6"], } }, ] if motif_type == "chain": return [ { "result": { "motif_type": "chain", "nodes": [1, 2, 3, 4], "labels": ["A", "B", "C", "D"], "length": 4, } }, ] if motif_type == "star": return [ { "result": { "motif_type": "star", "hub": 1, "spokes": [2, 3, 4], "hub_label": "Central", "spoke_labels": ["Spoke1", "Spoke2", "Spoke3"], } }, ] # analyze_entity_connectivity if "in_degree" in cypher: return [ { "metrics": { "entity_id": params.get("entity_id"), "entity_label": f"Entity_{params.get('entity_id')}", "in_degree": 3, "out_degree": 4, "total_degree": 7, "reachable_entities": 12, "reachability_ratio": 0.857, } } ] return [] async def test_find_paths(): """Test path finding between entities.""" print("\n[TEST 1] Find Paths") adapter = MockAdapter() matcher = PatternMatcher(adapter) paths = await matcher.find_paths(start_entity_id=1, end_entity_id=4, max_length=5) assert len(paths) > 0, "Should find at least one path" assert all("path" in p and "length" in p for p in paths), "All paths should have required fields" shortest = min(paths, key=lambda p: p["length"]) assert shortest["length"] >= 2, "Path length should be >= 2" print(f" [OK] Found {len(paths)} paths from 1 to 4") print(f" [OK] Shortest path: {shortest['path']} (length {shortest['length']})") print(f" [OK] Average confidence: {sum(p['confidence'] for p in paths) / len(paths):.3f}") print(" [PASS]") async def test_same_entity_path(): """Test path from entity to itself.""" print("\n[TEST 2] Same Entity Path") adapter = MockAdapter() matcher = PatternMatcher(adapter) paths = await matcher.find_paths(start_entity_id=1, end_entity_id=1, max_length=5) assert len(paths) == 1, "Should return single path to itself" assert paths[0]["path"] == [1], "Path should be entity itself" assert paths[0]["length"] == 0, "Length to itself should be 0" assert paths[0]["confidence"] == 1.0, "Confidence should be 1.0" print(" [OK] Path to self: [1], length 0, confidence 1.0") print(" [PASS]") async def test_find_cycles(): """Test cycle detection.""" print("\n[TEST 3] Find Cycles") adapter = MockAdapter() matcher = PatternMatcher(adapter) cycles = await matcher.find_cycles(min_length=2, max_length=5) assert len(cycles) > 0, "Should find cycles" assert all( "cycle" in c and "length" in c and c["length"] > 1 for c in cycles ), "All cycles should have required fields" print(f" [OK] Found {len(cycles)} cycles") for i, cycle in enumerate(cycles, 1): print(f" Cycle {i}: {cycle['cycle']} (length {cycle['length']})") print(" [PASS]") async def test_find_motifs_triangle(): """Test triangle motif detection.""" print("\n[TEST 4] Find Motifs (Triangle)") adapter = MockAdapter() matcher = PatternMatcher(adapter) motifs = await matcher.find_motifs(motif_type="triangle", limit=100) assert len(motifs) > 0, "Should find triangle motifs" assert all(m.get("motif_type") == "triangle" for m in motifs), "All should be triangles" print(f" [OK] Found {len(motifs)} triangle motifs") for motif in motifs: print(f" Triangle: {motif['nodes']}") print(" [PASS]") async def test_find_motifs_chain(): """Test chain motif detection.""" print("\n[TEST 5] Find Motifs (Chain)") adapter = MockAdapter() matcher = PatternMatcher(adapter) motifs = await matcher.find_motifs(motif_type="chain", limit=100) assert len(motifs) >= 0, "Should return chain motifs (may be empty)" if motifs: assert all(m.get("motif_type") == "chain" for m in motifs), "All should be chains" print(f" [OK] Found {len(motifs)} chain motifs") print(" [PASS]") async def test_find_motifs_star(): """Test star motif detection.""" print("\n[TEST 6] Find Motifs (Star)") adapter = MockAdapter() matcher = PatternMatcher(adapter) motifs = await matcher.find_motifs(motif_type="star", limit=100) assert len(motifs) >= 0, "Should return star motifs (may be empty)" if motifs: assert all(m.get("motif_type") == "star" for m in motifs), "All should be stars" assert all("hub" in m for m in motifs), "Stars should have hub" assert all("spokes" in m for m in motifs), "Stars should have spokes" print(f" [OK] Found {len(motifs)} star motifs") for motif in motifs: print(f" Hub: {motif['hub']}, Spokes: {motif['spokes']}") print(" [PASS]") async def test_find_motifs_invalid(): """Test invalid motif type.""" print("\n[TEST 7] Invalid Motif Type") adapter = MockAdapter() matcher = PatternMatcher(adapter) result = await matcher.find_motifs(motif_type="invalid", limit=100) assert isinstance(result, dict) and "error" in result, "Should return error for invalid motif" print(f" [OK] Correctly rejects invalid motif: {result['error']}") print(" [PASS]") async def test_analyze_entity_connectivity(): """Test entity connectivity analysis.""" print("\n[TEST 8] Entity Connectivity Analysis") adapter = MockAdapter() matcher = PatternMatcher(adapter) metrics = await matcher.analyze_entity_connectivity(entity_id=1) assert "in_degree" in metrics, "Should have in_degree" assert "out_degree" in metrics, "Should have out_degree" assert "total_degree" in metrics, "Should have total_degree" assert "reachable_entities" in metrics, "Should have reachable_entities" assert metrics["total_degree"] == metrics["in_degree"] + metrics["out_degree"] print(f" [OK] Entity 1 metrics:") print(f" In-degree: {metrics['in_degree']}") print(f" Out-degree: {metrics['out_degree']}") print(f" Total degree: {metrics['total_degree']}") print(f" Reachable entities: {metrics['reachable_entities']}") print(f" Reachability ratio: {metrics['reachability_ratio']:.3f}") print(" [PASS]") async def test_path_validation(): """Test path length validation.""" print("\n[TEST 9] Path Length Validation") adapter = MockAdapter() matcher = PatternMatcher(adapter) try: await matcher.find_paths(start_entity_id=1, end_entity_id=2, max_length=0) assert False, "Should reject max_length < 2" except ValueError as e: assert "max_length must be between 2 and 6" in str(e) print(" [OK] Correctly rejects max_length < 2") try: await matcher.find_paths(start_entity_id=1, end_entity_id=2, max_length=7) assert False, "Should reject max_length > 6" except ValueError as e: assert "max_length must be between 2 and 6" in str(e) print(" [OK] Correctly rejects max_length > 6") print(" [PASS]") async def test_cycle_validation(): """Test cycle detection validation.""" print("\n[TEST 10] Cycle Detection Validation") adapter = MockAdapter() matcher = PatternMatcher(adapter) try: await matcher.find_cycles(min_length=1) assert False, "Should reject min_length < 2" except ValueError as e: assert "min_length must be >= 2" in str(e) print(" [OK] Correctly rejects min_length < 2") try: await matcher.find_cycles(min_length=5, max_length=3) assert False, "Should reject max_length < min_length" except ValueError as e: assert "max_length must be between min_length and 6" in str(e) print(" [OK] Correctly rejects max_length < min_length") print(" [PASS]") async def main(): """Run all tests.""" print("=" * 70) print("Phase 5.1 Pattern Matcher Tests") print("=" * 70) try: await test_find_paths() await test_same_entity_path() await test_find_cycles() await test_find_motifs_triangle() await test_find_motifs_chain() await test_find_motifs_star() await test_find_motifs_invalid() await test_analyze_entity_connectivity() await test_path_validation() await test_cycle_validation() print("\n" + "=" * 70) print("All tests passed!") print("=" * 70) print("\nPhase 5.1 Pattern Matcher capabilities:") print(" [OK] Path finding between entities") print(" [OK] Cycle detection") print(" [OK] Motif detection (triangle, chain, star)") print(" [OK] Entity connectivity analysis") print(" [OK] Input validation") return True except AssertionError as e: print(f"\nTest failed: {e}") import traceback traceback.print_exc() return False except Exception as e: print(f"\nUnexpected error: {e}") import traceback traceback.print_exc() return False if __name__ == "__main__": success = asyncio.run(main()) sys.exit(0 if success else 1)