Files
microfish/backend/tests/test_local_service_import.py
Kunthawat Greethong 8b84378fe1 feat: SaaS foundation for CrowdSight
Elevate MiroFish/CrowdSight from single-container dev to a SaaS foundation:

- Local memory backend (Zep-compatible): memory services/models, local graph
  builder + updater, AgentActivity seam, import-boundary isolation; Zep stays
  default, local is opt-in behind MEMORY_BACKEND. Semantic parity not yet proven.
- Durable product persistence: projects/simulations/reports schema (migration
  0007) + tenant/owner-scoped ProductRepository + dual-write + scoped_project
  read-first + ArtifactStore abstraction; durable JobQueue + worker.py.
- SaaS hardening: durable RateLimiter (wired to login), UsageService (LLM
  accounting), redacted AuditService, idempotency, CORS allowlist, safe API
  errors, single-use PasswordResetService + endpoints (covers invite-pending).
- Exactly 3 roles (super_admin/admin/user) with tenant authz policy.
- Admin UI: GET/POST/PATCH /api/admin/users + GET/PUT /api/admin/settings
  (super-admin only, encrypted/masked); AdminView.vue + SettingsView.vue with
  admin/super-admin route guards, th/en i18n.
- Production deploy topology: multi-stage Dockerfile (frontend build + gunicorn
  wsgi + nginx SPA-proxy + supervisord worker), backend/wsgi.py, gunicorn dep.

Backend 197 passed; frontend 10 tests + build green. ruff unavailable (gap).
No commit of credentials; secrets handled via env/.env.example.
Deferred: Zep semantic A/B parity, object storage cutover, mobile QA, EasyPanel
container build of deploy topology.
2026-08-31 13:05:21 +07:00

169 lines
5.4 KiB
Python

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"