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.
169 lines
5.4 KiB
Python
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"
|