mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-07-09 14:04:19 +00:00
First commit
This commit is contained in:
23
tests/unit/test_agent_runtime.py
Normal file
23
tests/unit/test_agent_runtime.py
Normal 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
19
tests/unit/test_cache.py
Normal 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'
|
||||
12
tests/unit/test_cache_distributed.py
Normal file
12
tests/unit/test_cache_distributed.py
Normal 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}
|
||||
5
tests/unit/test_imports_compile.py
Normal file
5
tests/unit/test_imports_compile.py
Normal 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
|
||||
14
tests/unit/test_langgraph_checkpoint_saver.py
Normal file
14
tests/unit/test_langgraph_checkpoint_saver.py
Normal 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'
|
||||
29
tests/unit/test_langgraph_telemetry.py
Normal file
29
tests/unit/test_langgraph_telemetry.py
Normal 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
12
tests/unit/test_rag.py
Normal 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()
|
||||
6
tests/unit/test_rag_oracle_sql_generation.py
Normal file
6
tests/unit/test_rag_oracle_sql_generation.py
Normal 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")
|
||||
48
tests/unit/test_resilient_checkpointer.py
Normal file
48
tests/unit/test_resilient_checkpointer.py
Normal 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
19
tests/unit/test_sse.py
Normal 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'
|
||||
30
tests/unit/test_sse_replay_dedup.py
Normal file
30
tests/unit/test_sse_replay_dedup.py
Normal 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()
|
||||
23
tests/unit/test_token_cost_enterprise.py
Normal file
23
tests/unit/test_token_cost_enterprise.py
Normal 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
|
||||
14
tests/unit/test_workflow_static.py
Normal file
14
tests/unit/test_workflow_static.py
Normal 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
|
||||
Reference in New Issue
Block a user