import json from datetime import datetime from pathlib import Path import pytest from sqlalchemy import create_engine from app.db import Base, create_session_factory from app.models.memory import MemoryEdge, MemoryGraph, MemoryNode from app.services.memory_entity_reader import LocalEntityReader from app.services.memory_tools import LocalMemoryTools FIXTURE = json.loads( Path(__file__).with_name("fixtures").joinpath("memory_parity", "tools_fixture.json").read_text() ) GRAPH_ID = FIXTURE["graph_id"] ORG_ID = FIXTURE["organization_id"] def _seed_fixture(): engine = create_engine("sqlite+pysqlite:///:memory:") Base.metadata.create_all(engine) session = create_session_factory(engine)() session.add( MemoryGraph( id=GRAPH_ID, organization_id=ORG_ID, project_id=FIXTURE["project_id"], ) ) session.add_all([MemoryNode(graph_id=GRAPH_ID, **node) for node in FIXTURE["nodes"]]) session.add_all( [ MemoryEdge( graph_id=GRAPH_ID, **{ **edge, "valid_at": _parse_datetime(edge["valid_at"]), "invalid_at": _parse_datetime(edge["invalid_at"]), "expired_at": _parse_datetime(edge["expired_at"]), }, ) for edge in FIXTURE["edges"] ] ) session.commit() return engine, session def _parse_datetime(value): return datetime.fromisoformat(value) if value else None def test_local_panorama_matches_legacy_all_graph_temporal_contract(): engine, session = _seed_fixture() try: tools = LocalMemoryTools(session, organization_id=ORG_ID, graph_id=GRAPH_ID) result = tools.panorama_search(graph_id=GRAPH_ID, query="Alice", include_expired=True) assert {node.name for node in result.all_nodes} == {"Alice", "Acme", "Beta"} assert {edge.fact for edge in result.all_edges} == { "Alice works for Acme.", "Alice previously worked for Acme.", "Acme partnered with Beta.", } assert result.active_facts == ["Alice works for Acme.", "Acme partnered with Beta."] assert len(result.historical_facts) == 1 assert result.historical_facts[0].endswith("Alice previously worked for Acme.") assert result.historical_facts[0].startswith("[2020-01-01T00:00:00") finally: session.close() engine.dispose() def test_local_panorama_keeps_graph_inventory_when_expired_facts_are_excluded(): engine, session = _seed_fixture() try: tools = LocalMemoryTools(session, organization_id=ORG_ID, graph_id=GRAPH_ID) result = tools.panorama_search(graph_id=GRAPH_ID, query="not in any fact", include_expired=False) assert len(result.all_nodes) == 3 assert len(result.all_edges) == 3 assert result.active_facts == ["Alice works for Acme.", "Acme partnered with Beta."] assert result.historical_facts == [] finally: session.close() engine.dispose() def test_local_insight_forge_matches_legacy_related_entity_contract(): engine, session = _seed_fixture() try: tools = LocalMemoryTools(session, organization_id=ORG_ID, graph_id=GRAPH_ID) result = tools.insight_forge( graph_id=GRAPH_ID, query="Alice", simulation_requirement="How might Alice's role change?", ) assert result.sub_queries == ["Alice"] assert result.semantic_facts == [ "Alice works for Acme.", "Alice previously worked for Acme.", ] assert {entity["uuid"] for entity in result.entity_insights} == {"node-alice", "node-acme"} assert all("related_facts" in entity for entity in result.entity_insights) assert all( set(entity["related_facts"]) == set(result.semantic_facts) for entity in result.entity_insights ) assert result.relationship_chains == [ "Alice --[WORKS_FOR]--> Acme", "Alice --[WORKED_FOR]--> Acme", ] assert result.total_facts == 2 assert result.total_entities == 2 assert result.total_relationships == 2 finally: session.close() engine.dispose() @pytest.mark.parametrize( "operation", [ lambda reader: reader.get_all_nodes(graph_id="graph-other"), lambda reader: reader.get_all_edges(graph_id="graph-other"), lambda reader: reader.filter_defined_entities(graph_id="graph-other"), lambda reader: reader.get_entity_with_context( graph_id="graph-other", entity_uuid="node-alice" ), lambda reader: reader.get_entities_by_type( graph_id="graph-other", entity_type="Person" ), ], ) def test_local_entity_reader_rejects_conflicting_graph_scope(operation): engine, session = _seed_fixture() try: reader = LocalEntityReader( session, organization_id=ORG_ID, graph_id=GRAPH_ID, ) with pytest.raises(ValueError, match="memory_graph_scope_conflict"): operation(reader) finally: session.close() engine.dispose()