import json import os import subprocess import sys _ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) def _py_env(): env = os.environ.copy() env["PYTHONPATH"] = _ROOT return env def _run_import_without_zep(module_name, symbol): script = f""" import builtins original_import = builtins.__import__ def guarded_import(name, *args, **kwargs): if name == 'zep_cloud' or name.startswith('zep_cloud.'): raise RuntimeError('zep_imported_during_local_service_import') return original_import(name, *args, **kwargs) builtins.__import__ = guarded_import from app.services.{module_name} import {symbol} as imported_symbol print(imported_symbol.__name__) """ return subprocess.run( [sys.executable, "-c", script], capture_output=True, text=True, env=_py_env(), check=False, ) def _run_import_explicit_zep(module_name, symbol): """Fresh import that REQUIRES zep_cloud, to prove explicit Zep paths still resolve.""" script = f""" import builtins original_import = builtins.__import__ _zep_seen = [] def tracking_import(name, *args, **kwargs): if name == 'zep_cloud' or name.startswith('zep_cloud.'): _zep_seen.append(name) return original_import(name, *args, **kwargs) builtins.__import__ = tracking_import from app.services.{module_name} import {symbol} as imported_symbol print(imported_symbol.__name__) print('ZEP_LOADED' if _zep_seen else 'NO_ZEP') """ return subprocess.run( [sys.executable, "-c", script], capture_output=True, text=True, env=_py_env(), check=False, ) def test_local_service_import_does_not_eagerly_import_zep_client(): result = _run_import_without_zep("local_graph_memory_updater", "LocalGraphMemoryUpdater") assert result.returncode == 0, result.stderr assert result.stdout.strip() == "LocalGraphMemoryUpdater" def test_local_simulation_manager_import_does_not_eagerly_import_zep_client(): result = _run_import_without_zep("simulation_manager", "SimulationManager") assert result.returncode == 0, result.stderr assert result.stdout.strip() == "SimulationManager" def test_local_simulation_runner_import_does_not_eagerly_import_zep_client(): result = _run_import_without_zep("simulation_runner", "SimulationRunner") assert result.returncode == 0, result.stderr assert result.stdout.strip() == "SimulationRunner" def test_local_report_agent_import_does_not_eagerly_import_zep_client(): result = _run_import_without_zep("report_agent", "ReportAgent") assert result.returncode == 0, result.stderr assert result.stdout.strip() == "ReportAgent" def test_local_graph_builder_import_does_not_eagerly_import_zep_client(): result = _run_import_without_zep("graph_builder", "GraphBuilderService") assert result.returncode == 0, result.stderr assert result.stdout.strip() == "GraphBuilderService" def test_explicit_zep_tools_service_import_still_resolves(): # The lazy refactor must not break explicit Zep-only consumers. result = _run_import_explicit_zep("zep_tools", "ZepToolsService") assert result.returncode == 0, result.stderr lines = result.stdout.strip().splitlines() assert lines[0] == "ZepToolsService" assert "ZEP_LOADED" in lines def test_explicit_zep_entity_reader_import_still_resolves(): result = _run_import_explicit_zep("zep_entity_reader", "ZepEntityReader") assert result.returncode == 0, result.stderr lines = result.stdout.strip().splitlines() assert lines[0] == "ZepEntityReader" assert "ZEP_LOADED" in lines _IDENTITY_PROBE = """ import json from app.services.memory_activity import AgentActivity as Shared from app.services.zep_graph_memory_updater import ZepGraphMemoryUpdater zep_alias = getattr(ZepGraphMemoryUpdater, "AgentActivity", None) if zep_alias is None: import app.services.zep_graph_memory_updater as zup zep_alias = getattr(zup, "AgentActivity", None) print(json.dumps( { "zep_alias_resolved": zep_alias is not None, "shared_identity": zep_alias is Shared, "module": Shared.__module__, }, ensure_ascii=False, )) """ def test_local_and_zep_activity_contract_share_identity(): result = subprocess.run( [sys.executable, "-c", _IDENTITY_PROBE], capture_output=True, text=True, env=_py_env(), check=False, ) assert result.returncode == 0, result.stderr payload = json.loads(result.stdout.strip()) # When the Zep updater aliases the shared class, identity must be exact. if payload["zep_alias_resolved"]: assert payload["shared_identity"] is True assert payload["module"] == "app.services.memory_activity" _GRAPH_BUILDER_SEAM_PROBE = """ from app.services import graph_builder assert getattr(graph_builder, "Zep", None) is None from app.services import GraphBuilderService as Cls # The seam is a module-level sentinel that callers monkeypatch; it must remain # reassignable without constructing a real Zep client at import time. graph_builder.Zep = "sentinel" print(getattr(graph_builder, "Zep", None)) """ def test_graph_builder_zep_seam_is_preserved_and_reassignable(): result = subprocess.run( [sys.executable, "-c", _GRAPH_BUILDER_SEAM_PROBE], capture_output=True, text=True, env=_py_env(), check=False, ) assert result.returncode == 0, result.stderr assert result.stdout.strip() == "sentinel"