Files
PraisonAIUI/tests/test_feature_memory.py
a433c483c1 fix: document bootstrap entrypoints in examples (fixes #71) (#96)
* feat: standardize bootstrap entrypoint to 'aiui run app.py' across examples

Standardizes platform bootstrap patterns across examples per issue #71:

- Update examples README table to use 'aiui run app.py' consistently
- Update docstrings in examples 10, 12, 19-21 to show 'aiui run' instead of 'python app.py'
- Remove fragmented bootstrap approaches (uvicorn.run, asyncio patterns)
- Enforce single bootstrap SSOT as designed in CLI

Co-authored-by: Mervin Praison <[email protected]>

* feat: consolidate bootstrap entrypoints - auto-detect gateway apps (fixes #71)

- Add bootstrap detection in CLI run command
- Auto-detect gateway pattern (async main + AIUIGateway imports)
- Auto-switch to praisonai backend for gateway apps
- Delegate to user-defined main() when present
- Remove sys.path hacks from example 14
- Update examples README to show unified aiui run entrypoint
- Add BOOTSTRAP.md documentation for migration patterns

Co-authored-by: Mervin Praison <[email protected]>

* fix: resolve critical bootstrap entrypoint issues

- Fix AST parsing bug in gateway auto-detection (cli.py:1419)
- Revert background bots (email-bot, agentmail-bot) to python app.py
- Revert custom UI apps (agent-dashboard) to python app.py
- Update README.md to reflect correct run commands
- Fix import formatting and remove unused imports

These changes ensure that:
1. Gateway auto-detection works correctly by properly checking ast.alias.name
2. Background bots execute their __main__ blocks and don't start unnecessary web servers
3. Custom UI apps register their custom routes in __main__ blocks
4. Documentation matches actual working run commands

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Mervin Praison <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-06-15 16:36:35 +01:00

224 lines
7.3 KiB
Python

"""Per-feature test: Memory — Protocol compliance + API + CLI parity."""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from starlette.testclient import TestClient
# Reset memory manager before creating app to ensure clean state
from praisonaiui.features import memory as memory_mod
memory_mod._memory_manager = None
from praisonaiui.server import create_app
client = TestClient(create_app())
passed = failed = 0
errors = []
def check(name, r, status=200, keys=None):
global passed, failed
try:
assert r.status_code == status, f"Expected {status}, got {r.status_code}: {r.text[:200]}"
if keys:
data = r.json()
for k in keys:
assert k in data, f"Missing '{k}' in {list(data.keys())}"
passed += 1
print(f" ✓ {name}")
except AssertionError as e:
failed += 1; errors.append(f" ✗ {name}: {e}"); print(f" ✗ {name}: {e}")
# ── Protocol Compliance Tests ────────────────────────────────────────
print("\n── Memory: Protocol Tests ──")
from abc import ABC
from praisonaiui.features.memory import (
MemoryProtocol,
SDKMemoryManager,
SimpleMemoryManager,
get_memory_manager,
set_memory_manager,
)
# MemoryProtocol is ABC
assert issubclass(MemoryProtocol, ABC), "MemoryProtocol must be ABC"
print(" ✓ MemoryProtocol is ABC")
passed += 1
# SimpleMemoryManager implements MemoryProtocol
mgr = SimpleMemoryManager()
assert isinstance(mgr, MemoryProtocol), "SimpleMemoryManager must implement MemoryProtocol"
print(" ✓ SimpleMemoryManager implements MemoryProtocol")
passed += 1
# SDKMemoryManager implements MemoryProtocol
sdk_mgr = SDKMemoryManager()
assert isinstance(sdk_mgr, MemoryProtocol), "SDKMemoryManager must implement MemoryProtocol"
print(" ✓ SDKMemoryManager implements MemoryProtocol")
passed += 1
# Test protocol methods exist
for method in ["store", "search", "list_all", "get", "delete", "clear", "get_context", "health"]:
assert hasattr(mgr, method), f"Missing method: {method}"
print(" ✓ All protocol methods present")
passed += 1
# Test set/get manager swapping
original = get_memory_manager()
custom = SimpleMemoryManager()
set_memory_manager(custom)
assert get_memory_manager() is custom, "set_memory_manager must swap backend"
set_memory_manager(original) # restore
print(" ✓ Manager swapping works")
passed += 1
# Test SimpleMemoryManager CRUD
mgr2 = SimpleMemoryManager()
entry = mgr2.store("test text", memory_type="long")
assert entry["text"] == "test text"
assert entry["memory_type"] == "long"
assert "id" in entry
assert mgr2.get(entry["id"]) is not None
results = mgr2.search("test")
assert len(results) == 1
assert mgr2.list_all() == [entry]
assert mgr2.delete(entry["id"]) is True
assert mgr2.get(entry["id"]) is None
print(" ✓ SimpleMemoryManager CRUD works")
passed += 1
# Test get_context
mgr3 = SimpleMemoryManager()
mgr3.store("user likes dark mode")
mgr3.store("user prefers python")
ctx = mgr3.get_context("dark")
assert "dark mode" in ctx
assert "Relevant memories" in ctx
print(" ✓ get_context returns formatted text")
passed += 1
# Test health
h = mgr3.health()
assert h["status"] == "ok"
assert h["provider"] == "SimpleMemoryManager"
assert h["total_memories"] == 2
print(" ✓ SimpleMemoryManager health works")
passed += 1
# SDKMemoryManager graceful degradation (praisonaiagents not in test env)
sdk_h = sdk_mgr.health()
assert sdk_h["provider"] == "SDKMemoryManager"
# It should be "degraded" since praisonaiagents likely not installed in test env
print(f" ✓ SDKMemoryManager health: {sdk_h['status']}")
passed += 1
# SDKMemoryManager falls back to local index
sdk_entry = sdk_mgr.store("sdk test memory")
assert sdk_entry["text"] == "sdk test memory"
sdk_results = sdk_mgr.search("sdk test")
assert len(sdk_results) >= 1
print(" ✓ SDKMemoryManager local fallback works")
passed += 1
# ── API Tests ────────────────────────────────────────────────────────
print("\n── Memory: API Tests ──")
# Empty
check("GET /api/memory (empty)", client.get("/api/memory"), 200, ["memories", "count"])
# Add long-term memory
r = client.post("/api/memory", json={"text": "User prefers dark mode", "memory_type": "long"})
check("POST add (long)", r, 201, ["id", "text", "memory_type"])
m1 = r.json()["id"]
# Add short-term memory
r = client.post("/api/memory", json={"text": "Currently discussing Python", "memory_type": "short"})
check("POST add (short)", r, 201)
m2 = r.json()["id"]
# Add entity memory
r = client.post("/api/memory", json={"text": "John is a software engineer", "memory_type": "entity"})
check("POST add (entity)", r, 201)
m3 = r.json()["id"]
# List all (3)
r = client.get("/api/memory")
check("GET list all", r, 200)
assert r.json()["count"] == 3
# Filter by type
r = client.get("/api/memory?type=long")
check("GET ?type=long", r, 200)
assert r.json()["count"] == 1
assert r.json()["memories"][0]["text"] == "User prefers dark mode"
r = client.get("/api/memory?type=short")
check("GET ?type=short", r, 200)
assert r.json()["count"] == 1
# Get by ID
check("GET /:id", client.get(f"/api/memory/{m1}"), 200, ["id", "text", "memory_type"])
# Search
r = client.post("/api/memory/search", json={"query": "dark mode", "limit": 10})
check("POST search (match)", r, 200, ["results", "count"])
assert r.json()["count"] == 1
r = client.post("/api/memory/search", json={"query": "nonexistent thing"})
check("POST search (no match)", r, 200)
assert r.json()["count"] == 0
# Search with type filter
r = client.post("/api/memory/search", json={"query": "Python", "memory_type": "short"})
check("POST search (type filter)", r, 200)
assert r.json()["count"] == 1
# Context endpoint (NEW)
r = client.post("/api/memory/context", json={"query": "dark mode"})
check("POST /api/memory/context", r, 200, ["context"])
assert "dark mode" in r.json()["context"]
# Delete single
check("DELETE /:id", client.delete(f"/api/memory/{m1}"), 200, ["deleted"])
r = client.get("/api/memory")
assert r.json()["count"] == 2
# 404
check("GET deleted → 404", client.get(f"/api/memory/{m1}"), 404)
check("DELETE nonexistent → 404", client.delete("/api/memory/xxx"), 404)
# Clear all
check("DELETE clear all", client.delete("/api/memory?type=all"), 200, ["cleared"])
r = client.get("/api/memory")
assert r.json()["count"] == 0
# ── CLI Parity ───────────────────────────────────────────────────────
print("\n── Memory: CLI Parity ──")
from typer.testing import CliRunner
from praisonaiui.cli import app as cli_app
runner = CliRunner()
result = runner.invoke(cli_app, ["memory", "--help"])
check("CLI memory --help", type("R", (), {"status_code": 200 if result.exit_code == 0 else 500, "json": lambda: {}, "text": result.output})(), 200)
assert "list" in result.output
assert "add" in result.output
assert "search" in result.output
assert "clear" in result.output
assert "status" in result.output
assert "context" in result.output # NEW command
print(f"\n{'='*50}")
print(f" Memory: {passed} passed, {failed} failed")
print(f"{'='*50}")
if errors:
for e in errors: print(e)
sys.exit(1)