First commit

This commit is contained in:
2026-06-19 22:17:09 -03:00
commit 239203ee2a
533 changed files with 75195 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
import pytest
from types import SimpleNamespace
from app.agents.runtime import AgentRuntimeMixin
class DummyAgent(AgentRuntimeMixin):
def __init__(self):
self.settings = SimpleNamespace(CACHE_TTL_SECONDS=10)
self.calls = 0
self.cache = None
self.rag_service = None
self.telemetry = None
class LLM:
async def ainvoke(inner, messages):
self.calls += 1
return 'ok'
self.llm = LLM()
@pytest.mark.asyncio
async def test_agent_runtime_without_cache_invokes_llm():
agent = DummyAgent()
answer = await agent._invoke_llm_cached({'user_text':'oi'}, 'dummy', [{'role':'user','content':'oi'}])
assert answer == 'ok'
assert agent.calls == 1

19
tests/unit/test_cache.py Normal file
View File

@@ -0,0 +1,19 @@
import asyncio
import pytest
from agent_framework.cache.cache import InMemoryCache, DistributedCache
@pytest.mark.asyncio
async def test_in_memory_cache_ttl_expires():
cache = InMemoryCache()
await cache.set('k', {'v': 1}, ttl_seconds=1)
assert await cache.get('k') == {'v': 1}
await asyncio.sleep(1.05)
assert await cache.get('k') is None
@pytest.mark.asyncio
async def test_distributed_cache_promotes_l2_to_l1():
l1, l2 = InMemoryCache(), InMemoryCache()
cache = DistributedCache(l1, l2)
await l2.set('x', 'from-l2')
assert await cache.get('x') == 'from-l2'
assert await l1.get('x') == 'from-l2'

View File

@@ -0,0 +1,12 @@
import pytest
from agent_framework.cache.cache import DistributedCache, InMemoryCache
@pytest.mark.asyncio
async def test_distributed_cache_populates_l1_from_l2():
l1 = InMemoryCache(); l2 = InMemoryCache()
await l2.set("k", {"v": 1})
cache = DistributedCache(l1, l2)
assert await cache.get("k") == {"v": 1}
await l2.delete("k")
assert await cache.get("k") == {"v": 1}

View File

@@ -0,0 +1,5 @@
def test_core_imports():
import agent_framework.cache.cache
import agent_framework.rag.rag_service
import agent_framework.checkpoints.langgraph_saver
import agent_framework.observability.telemetry

View File

@@ -0,0 +1,14 @@
import pytest
from types import SimpleNamespace
from agent_framework.checkpoints.langgraph_saver import RepositoryCheckpointSaver
@pytest.mark.asyncio
async def test_repository_checkpoint_saver_put_get(tmp_path):
settings = SimpleNamespace(CHECKPOINT_REPOSITORY_PROVIDER='sqlite', SQLITE_DB_PATH=str(tmp_path/'db.sqlite'))
saver = RepositoryCheckpointSaver(settings)
config = {'configurable': {'thread_id': 't1'}}
next_config = await saver.aput(config, {'id': 'cp1', 'channel_values': {'x': 1}}, {'source': 'test'}, {})
assert next_config['configurable']['checkpoint_id'] == 'cp1'
tup = await saver.aget_tuple(config)
checkpoint = tup.checkpoint if hasattr(tup, 'checkpoint') else tup['checkpoint']
assert checkpoint['id'] == 'cp1'

View File

@@ -0,0 +1,29 @@
import pytest
from agent_framework.observability.langgraph_telemetry import LangGraphDeepTelemetry
class FakeTelemetry:
def __init__(self): self.events=[]
async def event(self, name, payload=None, kind='event'):
self.events.append((name, payload or {}, kind))
def span(self, name, **attrs):
class CM:
async def __aenter__(self_inner): return None
async def __aexit__(self_inner, exc_type, exc, tb): return False
return CM()
@pytest.mark.asyncio
async def test_langgraph_node_emits_started_completed():
telemetry = FakeTelemetry()
tracer = LangGraphDeepTelemetry(telemetry)
async with tracer.node('router', {'session_id': 's1'}):
pass
names = [e[0] for e in telemetry.events]
assert 'langgraph.node.started' in names
assert 'langgraph.node.completed' in names
@pytest.mark.asyncio
async def test_langgraph_edge_event():
telemetry = FakeTelemetry()
tracer = LangGraphDeepTelemetry(telemetry)
await tracer.edge('routing', 'billing', {'session_id': 's1'}, {'confidence': 0.9})
assert telemetry.events[0][0] == 'langgraph.edge.selected'

12
tests/unit/test_rag.py Normal file
View File

@@ -0,0 +1,12 @@
import pytest
from types import SimpleNamespace
from agent_framework.rag.rag_service import RagService
@pytest.mark.asyncio
async def test_rag_service_retrieves_relevant_document():
settings = SimpleNamespace(VECTOR_STORE_PROVIDER='memory', GRAPH_STORE_PROVIDER='memory', RAG_TOP_K=2)
rag = RagService(settings)
await rag.add_documents(['fatura alta por roaming internacional', 'pedido de troca de aparelho'], namespace='billing')
result = await rag.retrieve('minha fatura veio alta', namespace='billing')
assert result.documents
assert 'fatura' in result.as_prompt_context().lower()

View File

@@ -0,0 +1,6 @@
from agent_framework.rag.graph_store import InMemoryGraphStore
def test_inmemory_graph_has_pgql_method_for_interface_parity():
graph = InMemoryGraphStore()
assert hasattr(graph, "pgql")

View File

@@ -0,0 +1,48 @@
import pytest
from agent_framework.checkpoints.checkpoint_repository import (
CheckpointRecoveryError,
InMemoryCheckpointRepository,
ResilientCheckpointRepository,
)
@pytest.mark.asyncio
async def test_integrity_envelope_and_recovery_skips_corrupt_latest():
raw = InMemoryCheckpointRepository()
repo = ResilientCheckpointRepository(raw, compact_every=100, keep_last=10, recovery_scan_limit=5)
await repo.put("thread-1", {"checkpoint_id": "ok-1", "checkpoint": {"id": "ok-1", "value": 1}})
await repo.put("thread-1", {"checkpoint_id": "ok-2", "checkpoint": {"id": "ok-2", "value": 2}})
# Simula corrupção no último registro persistido.
raw._data["thread-1"][-1]["payload"]["checkpoint"]["value"] = 999
recovered = await repo.get_latest("thread-1")
assert recovered["checkpoint_id"] == "ok-1"
assert recovered["checkpoint"]["value"] == 1
@pytest.mark.asyncio
async def test_compaction_keeps_last_n_checkpoints():
raw = InMemoryCheckpointRepository()
repo = ResilientCheckpointRepository(raw, compact_every=1, keep_last=3, recovery_scan_limit=10)
for i in range(7):
await repo.put("thread-compact", {"checkpoint_id": f"cp-{i}", "checkpoint": {"id": f"cp-{i}"}})
assert len(raw._data["thread-compact"]) <= 3
latest = await repo.get_latest("thread-compact")
assert latest["checkpoint_id"] == "cp-6"
@pytest.mark.asyncio
async def test_recovery_raises_when_only_corrupt_checkpoints_exist():
raw = InMemoryCheckpointRepository()
repo = ResilientCheckpointRepository(raw, compact_every=100, keep_last=10, recovery_scan_limit=5)
await repo.put("thread-bad", {"checkpoint_id": "bad", "checkpoint": {"id": "bad", "value": 1}})
raw._data["thread-bad"][-1]["payload"]["checkpoint"]["value"] = 2
with pytest.raises(CheckpointRecoveryError):
await repo.get_latest("thread-bad")

19
tests/unit/test_sse.py Normal file
View File

@@ -0,0 +1,19 @@
import pytest
from types import SimpleNamespace
from agent_framework.sse.events import SSEEvent, SSEHub
@pytest.mark.asyncio
async def test_sse_event_encoding():
encoded = SSEEvent(event='message', data={'text':'ok'}, id=10).encode()
assert 'id: 10' in encoded
assert 'event: message' in encoded
assert 'data: {"text": "ok"}' in encoded
@pytest.mark.asyncio
async def test_sse_hub_emit_and_replay(tmp_path):
settings = SimpleNamespace(SQLITE_DB_PATH=str(tmp_path/'db.sqlite'), SSE_KEEPALIVE_SECONDS=0.1, SSE_EVENT_REPLAY_LIMIT=10, SESSION_REPOSITORY_PROVIDER='sqlite', SSE_STORE_PROVIDER='sqlite')
hub = SSEHub(settings)
eid = await hub.emit('s1', 'flow.start', {'a': 1})
replayed = await hub.replay('s1', 0)
assert replayed[0].id == eid
assert replayed[0].event == 'flow.start'

View File

@@ -0,0 +1,30 @@
import pytest
from types import SimpleNamespace
from agent_framework.sse.events import SSEHub, SSEEvent
class MemorySSEStore:
def __init__(self):
self.rows=[]; self.next_id=1
def append_sse_event(self, session_id, event, payload):
row={"id": self.next_id, "session_id": session_id, "event_name": event, "payload": payload}
self.next_id += 1; self.rows.append(row); return row["id"]
def list_sse_events(self, session_id, after_id, limit):
return [r for r in self.rows if r["session_id"] == session_id and r["id"] > after_id][:limit]
@pytest.mark.asyncio
async def test_subscribe_skips_live_event_already_replayed():
hub = SSEHub(SimpleNamespace(SSE_KEEPALIVE_SECONDS=0.01, SSE_EVENT_REPLAY_LIMIT=100, SQLITE_DB_PATH=':memory:', SESSION_REPOSITORY_PROVIDER='sqlite'), telemetry=None)
hub.store = MemorySSEStore()
eid = await hub.emit("s1", "message.responded", {"text":"ok"})
# Same event remains in live queue and is also in replay store.
gen = hub.subscribe("s1", 0)
chunks=[]
chunks.append(await gen.__anext__()) # replay event
chunks.append(await gen.__anext__()) # connected
chunks.append(await gen.__anext__()) # keepalive, not duplicated event
assert chunks[0].startswith(f"id: {eid}")
assert "message.responded" in chunks[0]
assert "message.responded" not in chunks[2]
await gen.aclose()

View File

@@ -0,0 +1,23 @@
from types import SimpleNamespace
from agent_framework.observability.token_cost import TokenUsageCollector, TokenUsage
def test_token_usage_extracts_cached_and_reasoning_tokens():
usage = TokenUsage.from_openai_usage({
"prompt_tokens": 1000,
"completion_tokens": 500,
"total_tokens": 1600,
"prompt_tokens_details": {"cached_tokens": 250},
"completion_tokens_details": {"reasoning_tokens": 100},
})
assert usage.prompt_tokens == 1000
assert usage.cached_tokens == 250
assert usage.reasoning_tokens == 100
assert usage.total_tokens == 1600
def test_cost_tracker_uses_model_prices_json():
settings = SimpleNamespace(MODEL_PRICES_JSON='{"my-model":{"input_per_1m":"1","output_per_1m":"2","cached_input_per_1m":"0.1"}}', USD_BRL_RATE='5')
enriched = TokenUsageCollector(settings).enrich("my-model", {"prompt_tokens": 1000, "completion_tokens": 1000, "prompt_tokens_details": {"cached_tokens": 500}})
assert enriched["cost_usd"] > 0
assert abs(enriched["cost_brl"] - enriched["cost_usd"] * 5) < 1e-9

View File

@@ -0,0 +1,14 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
WORKFLOW = ROOT / 'agent_template_backend' / 'app' / 'workflows' / 'agent_graph.py'
def test_workflow_uses_framework_checkpointer_not_memory_saver():
src = WORKFLOW.read_text()
assert 'create_langgraph_checkpointer(self.settings)' in src
assert 'MemorySaver()' not in src
def test_workflow_wraps_nodes_with_langgraph_telemetry():
src = WORKFLOW.read_text()
assert 'self._node("input_guardrails", self.input_guardrails)' in src
assert 'async with self.langgraph_telemetry.node(name, state)' in src