Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions tests/test_mcp/test_tools_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"""Tests for mcp_server/tools_layer.py — tools with correct mock ctx."""

import pytest
from unittest.mock import MagicMock, AsyncMock
from mcp_server.tools_layer import (
_validate_layer,
_fire_hook,
_get_cache_key,
memory_remember,
memory_recall,
memory_forget,
memory_session_start,
memory_session_end,
memory_episode_save,
memory_graph_add,
memory_stats,
)


# ── Helpers ──


def test_validate_layer_valid():
assert _validate_layer("user") == "user"
assert _validate_layer("agent") == "agent"


def test_validate_layer_invalid():
with pytest.raises(ValueError, match="Invalid layer"):
_validate_layer("admin")


def test_get_cache_key():
key = _get_cache_key("user", "alice")
assert "user" in key
assert "alice" in key


def test_fire_hook_no_handlers():
result = _fire_hook("nonexistent_hook", "user", {})
assert result.get("skipped") is True


# ── Mock ctx helper ──


def _make_ctx(layer="user"):
"""Create mock MCP ctx with AppContext."""
ctx = MagicMock()
app = MagicMock()
app.mm = MagicMock()
app.rate_limiter = MagicMock()
app.rate_limiter.check = AsyncMock(return_value={"allowed": True, "remaining": 100, "reset_in": 60})
app.emotion_trigger = MagicMock()
app.emotion_trigger.should_save = MagicMock(return_value=(False, "", 0.0))
app.user_hooks = MagicMock()
app.agent_hooks = MagicMock()
app.user_graph = MagicMock()
app.agent_graph = MagicMock()
ctx.request_context = MagicMock()
ctx.request_context.lifespan_context = app
return ctx, app


# ── memory_remember ──


@pytest.mark.asyncio
async def test_remember_user():
ctx, app = _make_ctx()
app.mm.user_memory.return_value.remember = AsyncMock(return_value=1)
result = await memory_remember(layer="user", user_id="u1", key="name", value="Alice", ctx=ctx)
assert result["status"] == "ok"


@pytest.mark.asyncio
async def test_remember_agent():
ctx, app = _make_ctx()
app.mm.agent_memory.return_value.remember = AsyncMock(return_value=1)
app.agent_graph.add_node = AsyncMock(return_value=1)
result = await memory_remember(layer="agent", user_id="u1", key="decision", value="Use X", ctx=ctx)
assert result["status"] == "ok"


@pytest.mark.asyncio
async def test_remember_rate_limited():
ctx, app = _make_ctx()
app.rate_limiter.check = AsyncMock(return_value={"allowed": False, "remaining": 0, "reset_in": 60})
result = await memory_remember(layer="user", user_id="u1", key="k", value="v", ctx=ctx)
assert "error" in result


@pytest.mark.asyncio
async def test_remember_invalid_layer():
ctx, _ = _make_ctx()
with pytest.raises(ValueError, match="Invalid layer"):
await memory_remember(layer="bad", user_id="u1", key="k", value="v", ctx=ctx)


# ── memory_recall ──


@pytest.mark.asyncio
async def test_recall():
ctx, app = _make_ctx()
app.mm.user_memory.return_value.recall = AsyncMock(return_value=[{"key": "n"}])
result = await memory_recall(layer="user", user_id="u1", query="name", ctx=ctx)
assert "results" in result


# ── memory_forget ──


@pytest.mark.asyncio
async def test_forget():
ctx, app = _make_ctx()
app.mm.user_memory.return_value.forget = AsyncMock(return_value=True)
result = await memory_forget(layer="user", user_id="u1", key="k", ctx=ctx)
assert result.get("deleted") is True


# ── memory_session_start/end ──


@pytest.mark.asyncio
async def test_session_start():
ctx, app = _make_ctx()
app.mm.user_memory.return_value.l2 = MagicMock()
app.mm.user_memory.return_value.l2.create_session = AsyncMock(return_value="s1")
result = await memory_session_start(layer="user", user_id="u1", ctx=ctx)
assert "session_id" in result


@pytest.mark.asyncio
async def test_session_end():
ctx, app = _make_ctx()
app.mm.user_memory.return_value.l2 = MagicMock()
app.mm.user_memory.return_value.l2.close_session = AsyncMock()
result = await memory_session_end(layer="user", user_id="u1", session_id="s1", summary="done", ctx=ctx)
assert result["status"] == "ok"


# ── memory_episode_save ──


@pytest.mark.asyncio
async def test_episode_save():
ctx, app = _make_ctx()
app.mm.user_memory.return_value.l3 = MagicMock()
app.mm.user_memory.return_value.l3.save = AsyncMock(return_value=1)
result = await memory_episode_save(layer="user", user_id="u1", summary="Event", weight=0.8, ctx=ctx)
assert "episode_id" in result


# ── memory_graph_add ──


@pytest.mark.asyncio
async def test_graph_add():
ctx, app = _make_ctx()
app.user_graph.add_node = AsyncMock(return_value=1)
result = await memory_graph_add(layer="user", user_id="u1", content="Fact", node_type="fact", ctx=ctx)
assert "node_id" in result


# ── memory_stats ──


@pytest.mark.asyncio
async def test_stats():
ctx, app = _make_ctx()
mem = app.mm.user_memory.return_value
mem.l1 = MagicMock()
mem.l1.size = MagicMock(return_value=0)
mem.l2 = MagicMock()
mem.l2.count_sessions = AsyncMock(return_value=0)
mem.l3 = MagicMock()
mem.l3.count = AsyncMock(return_value=0)
mem.l4 = MagicMock()
mem.l4.count = AsyncMock(return_value=0)
wiki = app.user_wiki
wiki.count = AsyncMock(return_value=0)
graph = app.user_graph
graph.count_nodes = AsyncMock(return_value=0)
result = await memory_stats(layer="user", user_id="u1", ctx=ctx)
assert isinstance(result, dict)
50 changes: 50 additions & 0 deletions tests/test_shared/test_embeddings_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Tests for shared/embeddings.py — hash embedding, similarity, cache."""

import pytest
from shared.embeddings import _hash_embedding, similarity


def test_hash_embedding_dim():
result = _hash_embedding("test text", dim=128)
assert len(result) == 128


def test_hash_embedding_normalized():
result = _hash_embedding("test", dim=64)
norm = sum(x**2 for x in result) ** 0.5
assert abs(norm - 1.0) < 0.01


def test_hash_embedding_deterministic():
r1 = _hash_embedding("hello", dim=32)
r2 = _hash_embedding("hello", dim=32)
assert r1 == r2


def test_hash_embedding_different_inputs():
r1 = _hash_embedding("hello", dim=32)
r2 = _hash_embedding("world", dim=32)
assert r1 != r2


def test_similarity_identical():
v = [1.0, 0.0, 0.0]
assert similarity(v, v) == pytest.approx(1.0)


def test_similarity_orthogonal():
v1 = [1.0, 0.0, 0.0]
v2 = [0.0, 1.0, 0.0]
assert similarity(v1, v2) == pytest.approx(0.0)


def test_similarity_zero_vector():
v1 = [1.0, 0.0]
v2 = [0.0, 0.0]
assert similarity(v1, v2) == 0.0


def test_similarity_opposite():
v1 = [1.0, 0.0]
v2 = [-1.0, 0.0]
assert similarity(v1, v2) == pytest.approx(-1.0)
137 changes: 137 additions & 0 deletions tests/test_shared/test_middleware_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""Tests for shared/middleware.py — actual API."""

import asyncio
from shared.middleware import (
MiddlewareContext,
ImportanceGateMiddleware,
DedupMiddleware,
ValidationMiddleware,
AuditMiddleware,
MiddlewarePipeline,
)


def test_middleware_context_defaults():
ctx = MiddlewareContext()
assert ctx.tool_name == ""
assert ctx.user_id == "default"
assert ctx.blocked is False


def test_importance_gate_passes_non_matching_tool():
gate = ImportanceGateMiddleware()
ctx = MiddlewareContext(args={"importance": 0.1}, tool_name="other_tool")

async def handler(c):
return {"ok": True}

result = asyncio.run(gate.process(ctx, handler))
assert result == {"ok": True}
assert ctx.blocked is False


def test_importance_gate_blocks_low():
gate = ImportanceGateMiddleware()
ctx = MiddlewareContext(args={"value": "hi"}, tool_name="memory_user_remember")

async def handler(c):
return {"ok": True}

result = asyncio.run(gate.process(ctx, handler))
assert ctx.blocked is True


def test_importance_gate_allows_high():
gate = ImportanceGateMiddleware()
ctx = MiddlewareContext(
args={
"value": "This is a critical and important decision about our architecture that affects production systems and requires immediate attention"
},
tool_name="memory_user_remember",
)

async def handler(c):
return {"ok": True}

result = asyncio.run(gate.process(ctx, handler))
assert ctx.blocked is False


def test_validation_blocks_empty_user():
val = ValidationMiddleware()
ctx = MiddlewareContext(user_id="", tool_name="memory_remember")

async def handler(c):
return {"ok": True}

asyncio.run(val.process(ctx, handler))
assert ctx.blocked is True


def test_validation_blocks_missing_key():
val = ValidationMiddleware()
ctx = MiddlewareContext(user_id="u1", tool_name="memory_user_remember", args={})

async def handler(c):
return {"ok": True}

asyncio.run(val.process(ctx, handler))
assert ctx.blocked is True


def test_validation_allows_valid():
val = ValidationMiddleware()
ctx = MiddlewareContext(user_id="u1", tool_name="memory_user_remember", args={"key": "k"})

async def handler(c):
return {"ok": True}

asyncio.run(val.process(ctx, handler))
assert ctx.blocked is False


def test_dedup_catches_duplicates():
dedup = DedupMiddleware()
ctx1 = MiddlewareContext(tool_name="test_tool", user_id="u1", args={"k": "v"})

async def handler(c):
return {"ok": True}

r1 = asyncio.run(dedup.process(ctx1, handler))
assert r1 == {"ok": True}

ctx2 = MiddlewareContext(tool_name="test_tool", user_id="u1", args={"k": "v"})
r2 = asyncio.run(dedup.process(ctx2, handler))
assert ctx2.metadata.get("deduped") is True


def test_pipeline_runs():
pipe = MiddlewarePipeline()

class CountMiddleware:
name = "count"

async def process(self, ctx, next_fn):
ctx.metadata["count"] = True
return await next_fn(ctx)

pipe.add(CountMiddleware())

async def handler(ctx):
return {"ok": True}

ctx = MiddlewareContext()
result = asyncio.run(pipe.execute(ctx, handler))
assert result == {"ok": True}
assert ctx.metadata.get("count") is True


def test_audit_sets_metadata():
audit = AuditMiddleware()
ctx = MiddlewareContext()

async def handler(c):
return {"ok": True}

asyncio.run(audit.process(ctx, handler))
assert "elapsed" in ctx.metadata
Loading
Loading