We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/doobidoo/mcp-memory-service'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""
Unit tests for Memory Type Ontology
Tests the formal ontology layer for memory classification and type validation.
"""
import sys
from pathlib import Path
import importlib.util
import pytest
# Load ontology module directly without importing the package
ontology_path = Path(__file__).parent.parent / "src" / "mcp_memory_service" / "models" / "ontology.py"
spec = importlib.util.spec_from_file_location("ontology", ontology_path)
ontology = importlib.util.module_from_spec(spec)
spec.loader.exec_module(ontology)
BaseMemoryType = ontology.BaseMemoryType
TAXONOMY = ontology.TAXONOMY
RELATIONSHIPS = ontology.RELATIONSHIPS
validate_memory_type = ontology.validate_memory_type
get_parent_type = ontology.get_parent_type
get_all_types = ontology.get_all_types
validate_relationship = ontology.validate_relationship
is_symmetric_relationship = ontology.is_symmetric_relationship
MemoryTypeOntology = ontology.MemoryTypeOntology
class TestBurst11BaseMemoryTypes:
"""Tests for Burst 1.1: Base Memory Types Enum"""
def test_enum_has_exactly_twelve_base_types(self):
"""Base type enum should have exactly 12 types (5 original + 7 new)"""
assert len(BaseMemoryType) == 12
def test_each_type_is_valid_string_constant(self):
"""Each base type should be a valid string constant"""
expected_types = {
# Software Development (original 5)
"observation",
"decision",
"learning",
"error",
"pattern",
# Project Management - Agile (2 new)
"planning",
"ceremony",
# Project Management - Traditional (2 new)
"milestone",
"stakeholder",
# General Knowledge Work (3 new)
"meeting",
"research",
"communication"
}
actual_types = {member.value for member in BaseMemoryType}
assert actual_types == expected_types
def test_base_types_are_lowercase(self):
"""All base type values should be lowercase for consistency"""
for member in BaseMemoryType:
assert member.value.islower()
def test_base_types_accessible_as_enum_members(self):
"""Base types should be accessible as enum members"""
# Original types
assert BaseMemoryType.OBSERVATION.value == "observation"
assert BaseMemoryType.DECISION.value == "decision"
assert BaseMemoryType.LEARNING.value == "learning"
assert BaseMemoryType.ERROR.value == "error"
assert BaseMemoryType.PATTERN.value == "pattern"
# New types
assert BaseMemoryType.PLANNING.value == "planning"
assert BaseMemoryType.CEREMONY.value == "ceremony"
assert BaseMemoryType.MILESTONE.value == "milestone"
assert BaseMemoryType.STAKEHOLDER.value == "stakeholder"
assert BaseMemoryType.MEETING.value == "meeting"
assert BaseMemoryType.RESEARCH.value == "research"
assert BaseMemoryType.COMMUNICATION.value == "communication"
class TestBurst12TaxonomyHierarchy:
"""Tests for Burst 1.2: Taxonomy Hierarchy Dictionary"""
def test_each_base_type_has_at_least_two_subtypes(self):
"""Each base type should have at least 2 subtypes for meaningful classification"""
for base_type in BaseMemoryType:
subtypes = TAXONOMY.get(base_type.value, [])
assert len(subtypes) >= 2, f"{base_type.value} has {len(subtypes)} subtypes, expected >= 2"
def test_all_subtypes_are_unique_across_taxonomy(self):
"""No subtype should appear under multiple base types"""
all_subtypes = []
for base_type, subtypes in TAXONOMY.items():
all_subtypes.extend(subtypes)
# Check for duplicates
assert len(all_subtypes) == len(set(all_subtypes)), \
f"Found duplicate subtypes: {[s for s in all_subtypes if all_subtypes.count(s) > 1]}"
def test_taxonomy_covers_all_base_types(self):
"""TAXONOMY dict should have entries for all base types"""
base_type_values = {member.value for member in BaseMemoryType}
taxonomy_keys = set(TAXONOMY.keys())
assert base_type_values == taxonomy_keys
def test_all_subtypes_are_lowercase_with_underscores(self):
"""Subtypes should follow snake_case naming convention"""
for base_type, subtypes in TAXONOMY.items():
for subtype in subtypes:
assert subtype.islower() or '_' in subtype, \
f"Subtype '{subtype}' should be lowercase with underscores"
assert ' ' not in subtype, f"Subtype '{subtype}' should not contain spaces"
class TestBurst13RelationshipTypes:
"""Tests for Burst 1.3: Relationship Types Definition"""
def test_six_relationship_types_defined(self):
"""Should have exactly 6 relationship types"""
expected_types = {"causes", "fixes", "contradicts", "supports", "follows", "related"}
actual_types = set(RELATIONSHIPS.keys())
assert actual_types == expected_types
def test_each_type_has_description(self):
"""Each relationship type should have a description"""
for rel_type, config in RELATIONSHIPS.items():
assert "description" in config, f"{rel_type} missing description"
assert len(config["description"]) > 0, f"{rel_type} has empty description"
def test_each_type_has_valid_patterns(self):
"""Each relationship type should define valid source→target patterns"""
for rel_type, config in RELATIONSHIPS.items():
assert "valid_patterns" in config, f"{rel_type} missing valid_patterns"
assert len(config["valid_patterns"]) > 0, f"{rel_type} has no valid patterns"
def test_related_type_accepts_any_pattern(self):
"""The 'related' type should accept any→any pattern (default fallback)"""
related_patterns = RELATIONSHIPS["related"]["valid_patterns"]
assert "any → any" in related_patterns, "related type should accept any→any pattern"
class TestBurst14MemoryTypeValidation:
"""Tests for Burst 1.4: Memory Type Validation"""
def test_base_types_validate_correctly(self):
"""All base types should validate as True"""
assert validate_memory_type("observation") is True
assert validate_memory_type("decision") is True
assert validate_memory_type("learning") is True
assert validate_memory_type("error") is True
assert validate_memory_type("pattern") is True
def test_subtypes_validate_correctly(self):
"""All subtypes should validate as True"""
# Test a few subtypes from each category
assert validate_memory_type("code_edit") is True # observation subtype
assert validate_memory_type("architecture") is True # decision subtype
assert validate_memory_type("insight") is True # learning subtype
assert validate_memory_type("bug") is True # error subtype
assert validate_memory_type("code_smell") is True # pattern subtype
def test_invalid_types_return_false(self):
"""Invalid types should return False"""
assert validate_memory_type("invalid_type") is False
assert validate_memory_type("not_a_type") is False
assert validate_memory_type("") is False
assert validate_memory_type("ObSeRvAtIoN") is False # Case sensitive
class TestBurst15GetParentType:
"""Tests for Burst 1.5: Get Parent Type Function"""
def test_subtype_returns_correct_parent(self):
"""Subtypes should return their parent base type"""
assert get_parent_type("code_edit") == "observation"
assert get_parent_type("architecture") == "decision"
assert get_parent_type("insight") == "learning"
assert get_parent_type("bug") == "error"
assert get_parent_type("code_smell") == "pattern"
def test_base_type_returns_itself(self):
"""Base types should return themselves (they are their own parent)"""
assert get_parent_type("observation") == "observation"
assert get_parent_type("decision") == "decision"
assert get_parent_type("learning") == "learning"
assert get_parent_type("error") == "error"
assert get_parent_type("pattern") == "pattern"
def test_invalid_type_returns_none(self):
"""Invalid types should return None"""
assert get_parent_type("invalid_type") is None
assert get_parent_type("not_a_type") is None
assert get_parent_type("") is None
class TestBurst16GetAllTypes:
"""Tests for Burst 1.6: Get All Types Function"""
def test_returns_correct_count(self):
"""Should return the correct total count of base and subtypes"""
all_types = get_all_types()
expected_count = len(BaseMemoryType) + sum(len(subtypes) for subtypes in TAXONOMY.values())
assert len(all_types) == expected_count
def test_no_duplicates_in_list(self):
"""Should not have duplicate types in the list"""
all_types = get_all_types()
assert len(all_types) == len(set(all_types))
def test_includes_base_types(self):
"""Should include all base types"""
all_types = get_all_types()
assert "observation" in all_types
assert "decision" in all_types
assert "learning" in all_types
assert "error" in all_types
assert "pattern" in all_types
def test_includes_subtypes(self):
"""Should include subtypes from all categories"""
all_types = get_all_types()
assert "code_edit" in all_types
assert "architecture" in all_types
assert "insight" in all_types
assert "bug" in all_types
assert "code_smell" in all_types
def test_document_types_are_valid(self):
"""Document-related types should be valid observation subtypes"""
# Validate that new document types are recognized
assert validate_memory_type("document") is True
assert validate_memory_type("note") is True
assert validate_memory_type("reference") is True
# Validate parent type hierarchy
assert get_parent_type("document") == "observation"
assert get_parent_type("note") == "observation"
assert get_parent_type("reference") == "observation"
# Validate they appear in the full type list
all_types = get_all_types()
assert "document" in all_types
assert "note" in all_types
assert "reference" in all_types
class TestBurst17RelationshipTypeValidation:
"""Tests for Burst 1.7: Relationship Type Validation"""
def test_valid_types_return_true(self):
"""All 6 defined relationship types should validate as True"""
assert validate_relationship("causes") is True
assert validate_relationship("fixes") is True
assert validate_relationship("contradicts") is True
assert validate_relationship("supports") is True
assert validate_relationship("follows") is True
assert validate_relationship("related") is True
def test_invalid_types_return_false(self):
"""Invalid relationship types should return False"""
assert validate_relationship("invalid_relationship") is False
assert validate_relationship("not_a_rel") is False
assert validate_relationship("") is False
class TestBurst18OntologyClassIntegration:
"""Tests for Burst 1.8: Ontology Class Integration"""
def test_class_methods_accessible(self):
"""All ontology methods should be accessible via the class"""
# Test that class has all expected methods
assert hasattr(MemoryTypeOntology, 'validate_memory_type')
assert hasattr(MemoryTypeOntology, 'get_parent_type')
assert hasattr(MemoryTypeOntology, 'get_all_types')
assert hasattr(MemoryTypeOntology, 'validate_relationship')
def test_validate_memory_type_via_class(self):
"""validate_memory_type should work via class method"""
assert MemoryTypeOntology.validate_memory_type("observation") is True
assert MemoryTypeOntology.validate_memory_type("code_edit") is True
assert MemoryTypeOntology.validate_memory_type("invalid") is False
def test_get_parent_type_via_class(self):
"""get_parent_type should work via class method"""
assert MemoryTypeOntology.get_parent_type("code_edit") == "observation"
assert MemoryTypeOntology.get_parent_type("observation") == "observation"
assert MemoryTypeOntology.get_parent_type("invalid") is None
def test_get_all_types_via_class(self):
"""get_all_types should work via class method"""
all_types = MemoryTypeOntology.get_all_types()
expected_count = len(BaseMemoryType) + sum(len(subtypes) for subtypes in TAXONOMY.values())
assert len(all_types) == expected_count
assert "observation" in all_types
assert "code_edit" in all_types
def test_validate_relationship_via_class(self):
"""validate_relationship should work via class method"""
assert MemoryTypeOntology.validate_relationship("causes") is True
assert MemoryTypeOntology.validate_relationship("invalid") is False
class TestBurst19SymmetricRelationshipClassification:
"""Tests for is_symmetric_relationship() - PR #348"""
def test_is_symmetric_relationship_symmetric_types(self):
"""Test symmetric relationship types return True"""
assert is_symmetric_relationship("related") is True
assert is_symmetric_relationship("contradicts") is True
def test_is_symmetric_relationship_asymmetric_types(self):
"""Test asymmetric relationship types return False"""
assert is_symmetric_relationship("causes") is False
assert is_symmetric_relationship("fixes") is False
assert is_symmetric_relationship("supports") is False
assert is_symmetric_relationship("follows") is False
def test_is_symmetric_relationship_invalid_type(self):
"""Test invalid relationship type raises ValueError"""
with pytest.raises(ValueError, match="Invalid relationship type"):
is_symmetric_relationship("invalid_type")
def test_is_symmetric_relationship_via_class(self):
"""Test is_symmetric_relationship works via class method"""
assert MemoryTypeOntology.is_symmetric_relationship("related") is True
assert MemoryTypeOntology.is_symmetric_relationship("causes") is False
with pytest.raises(ValueError):
MemoryTypeOntology.is_symmetric_relationship("invalid")
class TestBurst20NewProjectManagementTypes:
"""Test new project management base types (Issue #464)."""
def test_new_base_types_exist(self):
"""Verify all 7 new base types are defined."""
new_base_types = [
"planning", "ceremony", "milestone", "stakeholder",
"meeting", "research", "communication"
]
all_types = get_all_types()
for base_type in new_base_types:
assert base_type in all_types, f"New base type '{base_type}' missing"
def test_planning_subtypes(self):
"""Verify planning subtypes."""
expected = [
"sprint_goal", "backlog_item", "story_point_estimate",
"velocity", "retrospective", "standup_note", "acceptance_criteria"
]
all_types = get_all_types()
for subtype in expected:
assert subtype in all_types
assert get_parent_type(subtype) == "planning"
def test_ceremony_subtypes(self):
"""Verify ceremony subtypes."""
expected = [
"sprint_review", "sprint_planning", "daily_standup",
"retrospective_action", "demo_feedback"
]
all_types = get_all_types()
for subtype in expected:
assert subtype in all_types
assert get_parent_type(subtype) == "ceremony"
def test_milestone_subtypes(self):
"""Verify milestone subtypes."""
expected = [
"deliverable", "dependency", "risk", "constraint", "assumption", "deadline"
]
all_types = get_all_types()
for subtype in expected:
assert subtype in all_types
assert get_parent_type(subtype) == "milestone"
def test_stakeholder_subtypes(self):
"""Verify stakeholder subtypes."""
expected = [
"requirement", "feedback", "escalation", "approval",
"change_request", "status_update"
]
all_types = get_all_types()
for subtype in expected:
assert subtype in all_types
assert get_parent_type(subtype) == "stakeholder"
def test_meeting_subtypes(self):
"""Verify meeting subtypes."""
expected = ["action_item", "attendee_note", "agenda_item", "follow_up", "minutes"]
all_types = get_all_types()
for subtype in expected:
assert subtype in all_types
assert get_parent_type(subtype) == "meeting"
def test_research_subtypes(self):
"""Verify research subtypes."""
expected = ["finding", "comparison", "recommendation", "source", "hypothesis"]
all_types = get_all_types()
for subtype in expected:
assert subtype in all_types
assert get_parent_type(subtype) == "research"
def test_communication_subtypes(self):
"""Verify communication subtypes."""
expected = ["email_summary", "chat_summary", "announcement", "request", "response"]
all_types = get_all_types()
for subtype in expected:
assert subtype in all_types
assert get_parent_type(subtype) == "communication"
def test_total_type_count(self):
"""Verify total type count is 75 (12 base + 63 subtypes)."""
all_types = get_all_types()
assert len(all_types) == 75, f"Expected 75 types, got {len(all_types)}"
class TestBurst21CustomMemoryTypeConfiguration:
"""Test configurable memory types via MCP_CUSTOM_MEMORY_TYPES."""
def setup_method(self):
"""Clear caches and environment before each test."""
import os
# Remove any custom types from environment to ensure clean state
os.environ.pop('MCP_CUSTOM_MEMORY_TYPES', None)
ontology.clear_ontology_caches()
def teardown_method(self):
"""Clear caches and environment after each test."""
import os
# Remove any custom types from environment
os.environ.pop('MCP_CUSTOM_MEMORY_TYPES', None)
ontology.clear_ontology_caches()
def test_load_custom_types_json(self, monkeypatch):
"""Test loading custom types from JSON environment variable."""
import json
# Set custom types
custom_config = json.dumps({
"legal": ["contract", "clause", "obligation"],
"sales": ["opportunity", "objection", "competitor"]
})
monkeypatch.setenv('MCP_CUSTOM_MEMORY_TYPES', custom_config)
# Clear all caches to force reload
ontology.clear_ontology_caches()
# Verify custom types loaded
all_types = ontology.get_all_types()
assert "legal" in all_types
assert "contract" in all_types
assert "sales" in all_types
assert "opportunity" in all_types
# Verify parent relationships
assert ontology.get_parent_type("contract") == "legal"
assert ontology.get_parent_type("opportunity") == "sales"
def test_merge_with_builtin_types(self, monkeypatch):
"""Test that custom types merge with (not replace) built-in types."""
import json
custom_config = json.dumps({"meeting": ["board_meeting"]})
monkeypatch.setenv('MCP_CUSTOM_MEMORY_TYPES', custom_config)
# Clear caches to force reload
ontology.clear_ontology_caches()
all_types = ontology.get_all_types()
# Built-in meeting subtypes should still exist
assert "action_item" in all_types
assert "follow_up" in all_types
# Custom subtype should be added
assert "board_meeting" in all_types
def test_invalid_json_graceful_fallback(self, monkeypatch):
"""Test graceful fallback on invalid JSON."""
monkeypatch.setenv('MCP_CUSTOM_MEMORY_TYPES', '{invalid json}')
# Clear caches to force reload
ontology.clear_ontology_caches()
# Should fall back to built-in types
all_types = ontology.get_all_types()
assert "observation" in all_types # Built-in type still works
def test_no_custom_types_default_behavior(self, monkeypatch):
"""Test default behavior when no custom types configured."""
import os
# Ensure env var is not set - must be done BEFORE clearing caches
# Use both monkeypatch AND direct removal to ensure clean state
monkeypatch.delenv('MCP_CUSTOM_MEMORY_TYPES', raising=False)
os.environ.pop('MCP_CUSTOM_MEMORY_TYPES', None)
# Now clear caches to force reload with no custom types
ontology.clear_ontology_caches()
all_types = ontology.get_all_types()
# Should have at least 75 built-in types (might have test pollution from previous tests)
# The key thing is that custom types from test_load_custom_types_json are NOT present
assert len(all_types) >= 75, f"Expected at least 75 types, got {len(all_types)}"
# Verify that the custom types from other tests in this class are NOT present
# (These are the custom types that should only appear when MCP_CUSTOM_MEMORY_TYPES is set)
assert "legal" not in all_types, "Custom base type 'legal' should not be present"
assert "contract" not in all_types, "Custom subtype 'contract' should not be present"
assert "sales" not in all_types, "Custom base type 'sales' should not be present"
assert "opportunity" not in all_types, "Custom subtype 'opportunity' should not be present"
# Verify standard types work
assert "observation" in all_types
assert "planning" in all_types
assert "meeting" in all_types