mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
c44291830c
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
245 lines
8.6 KiB
Python
245 lines
8.6 KiB
Python
"""Tests for the SchemaClient typed API client.
|
|
|
|
Covers __init__, validate(), infer(), and diff() methods.
|
|
"""
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from httpx import AsyncClient, Response, Request
|
|
|
|
from basic_memory.mcp.clients.schema import SchemaClient
|
|
from basic_memory.schemas.schema import ValidationReport, InferenceReport, DriftReport
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def http_client():
|
|
"""Provide a real AsyncClient (unused transport — we mock responses)."""
|
|
async with AsyncClient(base_url="http://test") as client:
|
|
yield client
|
|
|
|
|
|
@pytest.fixture
|
|
def schema_client(http_client):
|
|
"""Create a SchemaClient with a test project id."""
|
|
return SchemaClient(http_client, "test-project-id")
|
|
|
|
|
|
class TestSchemaClientInit:
|
|
"""Tests for SchemaClient.__init__."""
|
|
|
|
def test_stores_http_client(self, http_client):
|
|
client = SchemaClient(http_client, "proj-123")
|
|
assert client.http_client is http_client
|
|
|
|
def test_stores_project_id(self, http_client):
|
|
client = SchemaClient(http_client, "proj-123")
|
|
assert client.project_id == "proj-123"
|
|
|
|
def test_builds_base_path(self, http_client):
|
|
client = SchemaClient(http_client, "proj-123")
|
|
assert client._base_path == "/v2/projects/proj-123/schema"
|
|
|
|
|
|
class TestSchemaClientValidate:
|
|
"""Tests for SchemaClient.validate()."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_validate_no_params(self, schema_client, monkeypatch):
|
|
"""Validate with no note_type or identifier sends empty params."""
|
|
report_data = {
|
|
"note_type": None,
|
|
"total_notes": 0,
|
|
"valid_count": 0,
|
|
"warning_count": 0,
|
|
"error_count": 0,
|
|
"results": [],
|
|
}
|
|
|
|
request = Request("POST", "http://test/v2/projects/test-project-id/schema/validate")
|
|
mock_response = Response(200, json=report_data, request=request)
|
|
|
|
async def mock_call_post(client, url, **kwargs):
|
|
assert url == "/v2/projects/test-project-id/schema/validate"
|
|
assert kwargs.get("params") == {}
|
|
return mock_response
|
|
|
|
monkeypatch.setattr("basic_memory.mcp.clients.schema.call_post", mock_call_post)
|
|
|
|
result = await schema_client.validate()
|
|
assert isinstance(result, ValidationReport)
|
|
assert result.total_notes == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_validate_with_note_type(self, schema_client, monkeypatch):
|
|
"""Validate sends note_type as query param."""
|
|
report_data = {
|
|
"note_type": "person",
|
|
"total_notes": 5,
|
|
"valid_count": 4,
|
|
"warning_count": 1,
|
|
"error_count": 0,
|
|
"results": [],
|
|
}
|
|
|
|
request = Request("POST", "http://test/v2/projects/test-project-id/schema/validate")
|
|
mock_response = Response(200, json=report_data, request=request)
|
|
|
|
async def mock_call_post(client, url, **kwargs):
|
|
assert kwargs["params"]["note_type"] == "person"
|
|
return mock_response
|
|
|
|
monkeypatch.setattr("basic_memory.mcp.clients.schema.call_post", mock_call_post)
|
|
|
|
result = await schema_client.validate(note_type="person")
|
|
assert result.note_type == "person"
|
|
assert result.total_notes == 5
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_validate_with_identifier(self, schema_client, monkeypatch):
|
|
"""Validate sends identifier as query param."""
|
|
report_data = {
|
|
"note_type": None,
|
|
"total_notes": 1,
|
|
"valid_count": 1,
|
|
"warning_count": 0,
|
|
"error_count": 0,
|
|
"results": [],
|
|
}
|
|
|
|
request = Request("POST", "http://test/v2/projects/test-project-id/schema/validate")
|
|
mock_response = Response(200, json=report_data, request=request)
|
|
|
|
async def mock_call_post(client, url, **kwargs):
|
|
assert kwargs["params"]["identifier"] == "people/alice"
|
|
return mock_response
|
|
|
|
monkeypatch.setattr("basic_memory.mcp.clients.schema.call_post", mock_call_post)
|
|
|
|
result = await schema_client.validate(identifier="people/alice")
|
|
assert result.total_notes == 1
|
|
|
|
|
|
class TestSchemaClientInfer:
|
|
"""Tests for SchemaClient.infer()."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_infer_default_threshold(self, schema_client, monkeypatch):
|
|
"""Infer sends note_type and default threshold."""
|
|
report_data = {
|
|
"note_type": "person",
|
|
"notes_analyzed": 10,
|
|
"field_frequencies": [],
|
|
"suggested_schema": {},
|
|
"suggested_required": ["name"],
|
|
"suggested_optional": ["email"],
|
|
"excluded": [],
|
|
}
|
|
|
|
request = Request("POST", "http://test/v2/projects/test-project-id/schema/infer")
|
|
mock_response = Response(200, json=report_data, request=request)
|
|
|
|
async def mock_call_post(client, url, **kwargs):
|
|
assert url == "/v2/projects/test-project-id/schema/infer"
|
|
assert kwargs["params"]["note_type"] == "person"
|
|
assert kwargs["params"]["threshold"] == 0.25
|
|
return mock_response
|
|
|
|
monkeypatch.setattr("basic_memory.mcp.clients.schema.call_post", mock_call_post)
|
|
|
|
result = await schema_client.infer("person")
|
|
assert isinstance(result, InferenceReport)
|
|
assert result.notes_analyzed == 10
|
|
assert result.suggested_required == ["name"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_infer_custom_threshold(self, schema_client, monkeypatch):
|
|
"""Infer passes custom threshold."""
|
|
report_data = {
|
|
"note_type": "meeting",
|
|
"notes_analyzed": 5,
|
|
"field_frequencies": [],
|
|
"suggested_schema": {},
|
|
"suggested_required": [],
|
|
"suggested_optional": [],
|
|
"excluded": [],
|
|
}
|
|
|
|
request = Request("POST", "http://test/v2/projects/test-project-id/schema/infer")
|
|
mock_response = Response(200, json=report_data, request=request)
|
|
|
|
async def mock_call_post(client, url, **kwargs):
|
|
assert kwargs["params"]["threshold"] == 0.5
|
|
return mock_response
|
|
|
|
monkeypatch.setattr("basic_memory.mcp.clients.schema.call_post", mock_call_post)
|
|
|
|
result = await schema_client.infer("meeting", threshold=0.5)
|
|
assert result.note_type == "meeting"
|
|
|
|
|
|
class TestSchemaClientDiff:
|
|
"""Tests for SchemaClient.diff()."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff(self, schema_client, monkeypatch):
|
|
"""Diff calls GET with note_type in path."""
|
|
report_data = {
|
|
"note_type": "person",
|
|
"new_fields": [],
|
|
"dropped_fields": [],
|
|
"cardinality_changes": [],
|
|
}
|
|
|
|
request = Request("GET", "http://test/v2/projects/test-project-id/schema/diff/person")
|
|
mock_response = Response(200, json=report_data, request=request)
|
|
|
|
async def mock_call_get(client, url, **kwargs):
|
|
assert url == "/v2/projects/test-project-id/schema/diff/person"
|
|
return mock_response
|
|
|
|
monkeypatch.setattr("basic_memory.mcp.clients.schema.call_get", mock_call_get)
|
|
|
|
result = await schema_client.diff("person")
|
|
assert isinstance(result, DriftReport)
|
|
assert result.note_type == "person"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_with_drift(self, schema_client, monkeypatch):
|
|
"""Diff returns populated drift report."""
|
|
report_data = {
|
|
"note_type": "person",
|
|
"new_fields": [
|
|
{
|
|
"name": "role",
|
|
"source": "observation",
|
|
"count": 8,
|
|
"total": 10,
|
|
"percentage": 80.0,
|
|
}
|
|
],
|
|
"dropped_fields": [
|
|
{
|
|
"name": "email",
|
|
"source": "observation",
|
|
"count": 1,
|
|
"total": 10,
|
|
"percentage": 10.0,
|
|
}
|
|
],
|
|
"cardinality_changes": ["skills: single -> array"],
|
|
}
|
|
|
|
request = Request("GET", "http://test/v2/projects/test-project-id/schema/diff/person")
|
|
mock_response = Response(200, json=report_data, request=request)
|
|
|
|
async def mock_call_get(client, url, **kwargs):
|
|
return mock_response
|
|
|
|
monkeypatch.setattr("basic_memory.mcp.clients.schema.call_get", mock_call_get)
|
|
|
|
result = await schema_client.diff("person")
|
|
assert len(result.new_fields) == 1
|
|
assert result.new_fields[0].name == "role"
|
|
assert len(result.dropped_fields) == 1
|
|
assert result.cardinality_changes == ["skills: single -> array"]
|