189 lines
6.0 KiB
Python
189 lines
6.0 KiB
Python
"""Tests for the YantrikDB REST API."""
|
|
|
|
import math
|
|
import threading
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from yantrikdb import YantrikDB
|
|
|
|
DIM = 8
|
|
|
|
|
|
def _vec(seed: float) -> list[float]:
|
|
raw = [(seed + i) * 0.1 for i in range(DIM)]
|
|
norm = math.sqrt(sum(x * x for x in raw))
|
|
return [x / norm for x in raw]
|
|
|
|
|
|
class _MockEmbedder:
|
|
def encode(self, text: str) -> list[float]:
|
|
seed = float(hash(text) % 1000) / 100.0
|
|
return _vec(seed)
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
"""Create a test client with in-memory YantrikDB, bypassing lifespan."""
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI as _FastAPI
|
|
|
|
import yantrikdb.api as api_mod
|
|
|
|
# Create a test lifespan that creates the DB on the event loop thread
|
|
@asynccontextmanager
|
|
async def test_lifespan(app):
|
|
db = YantrikDB(db_path=":memory:", embedding_dim=DIM)
|
|
db.set_embedder(_MockEmbedder())
|
|
lock = threading.Lock()
|
|
app.state.db = db
|
|
app.state.lock = lock
|
|
original = api_mod._db
|
|
api_mod._db = lambda: (db, lock)
|
|
try:
|
|
yield
|
|
finally:
|
|
api_mod._db = original
|
|
db.close()
|
|
|
|
test_app = _FastAPI(title="YantrikDB", version="0.1.0", lifespan=test_lifespan)
|
|
for route in api_mod.app.routes:
|
|
test_app.routes.append(route)
|
|
|
|
with TestClient(test_app, raise_server_exceptions=True) as c:
|
|
yield c
|
|
|
|
|
|
class TestMemoryEndpoints:
|
|
def test_record_memory(self, client):
|
|
resp = client.post("/memories", json={"text": "hello world"})
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "rid" in data
|
|
|
|
def test_get_memory(self, client):
|
|
rid = client.post("/memories", json={"text": "test"}).json()["rid"]
|
|
resp = client.get(f"/memories/{rid}")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["rid"] == rid
|
|
assert resp.json()["text"] == "test"
|
|
assert "embedding" not in resp.json()
|
|
|
|
def test_get_memory_not_found(self, client):
|
|
resp = client.get("/memories/nonexistent")
|
|
assert resp.status_code == 404
|
|
|
|
def test_recall_memories(self, client):
|
|
client.post("/memories", json={"text": "the sky is blue"})
|
|
client.post("/memories", json={"text": "grass is green"})
|
|
|
|
resp = client.post("/memories/recall", json={"query": "sky color"})
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "count" in data
|
|
assert "results" in data
|
|
|
|
def test_recall_empty(self, client):
|
|
resp = client.post("/memories/recall", json={"query": "anything"})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["count"] == 0
|
|
|
|
def test_forget_memory(self, client):
|
|
rid = client.post("/memories", json={"text": "forget me"}).json()["rid"]
|
|
resp = client.delete(f"/memories/{rid}")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["forgotten"] is True
|
|
|
|
# Memory is tombstoned — get() still returns it but it won't appear in recall
|
|
resp = client.get(f"/memories/{rid}")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["consolidation_status"] == "tombstoned"
|
|
|
|
def test_correct_memory(self, client):
|
|
rid = client.post("/memories", json={"text": "wrong"}).json()["rid"]
|
|
resp = client.post(f"/memories/{rid}/correct", json={
|
|
"new_text": "correct",
|
|
"correction_note": "fixed",
|
|
})
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "new_rid" in data or "corrected_rid" in data
|
|
|
|
def test_record_with_fields(self, client):
|
|
resp = client.post("/memories", json={
|
|
"text": "important fact",
|
|
"memory_type": "semantic",
|
|
"importance": 0.9,
|
|
"valence": 0.5,
|
|
"metadata": {"source": "test"},
|
|
})
|
|
assert resp.status_code == 200
|
|
|
|
|
|
class TestEntityEndpoints:
|
|
def test_relate(self, client):
|
|
resp = client.post("/entities/relate", json={
|
|
"src": "Alice", "dst": "Bob", "rel_type": "knows",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert "edge_id" in resp.json()
|
|
|
|
def test_get_edges(self, client):
|
|
client.post("/entities/relate", json={
|
|
"src": "Alice", "dst": "Bob",
|
|
})
|
|
resp = client.get("/entities/Alice/edges")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["count"] >= 1
|
|
|
|
def test_get_edges_empty(self, client):
|
|
resp = client.get("/entities/Unknown/edges")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["count"] == 0
|
|
|
|
|
|
class TestCognitionEndpoints:
|
|
def test_think(self, client):
|
|
resp = client.post("/think")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "consolidation_count" in data
|
|
|
|
def test_conflicts_empty(self, client):
|
|
resp = client.get("/conflicts")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["count"] == 0
|
|
|
|
def test_triggers_empty(self, client):
|
|
resp = client.get("/triggers")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["count"] == 0
|
|
|
|
|
|
class TestSystemEndpoints:
|
|
def test_stats(self, client):
|
|
client.post("/memories", json={"text": "test"})
|
|
resp = client.get("/stats")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["active_memories"] == 1
|
|
|
|
def test_export(self, client):
|
|
client.post("/memories", json={"text": "exportable"})
|
|
resp = client.get("/export")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["version"] == "yantrikdb-export-v1"
|
|
assert len(data["memories"]) == 1
|
|
|
|
def test_openapi_schema(self, client):
|
|
resp = client.get("/openapi.json")
|
|
assert resp.status_code == 200
|
|
schema = resp.json()
|
|
assert schema["info"]["title"] == "YantrikDB"
|
|
assert "/memories" in schema["paths"]
|
|
assert "/memories/recall" in schema["paths"]
|
|
assert "/stats" in schema["paths"]
|