forked from Rockachopa/Timmy-time-dashboard
feat: dockerize OpenFang as vendored tool runtime sidecar (#96)
This commit is contained in:
committed by
GitHub
parent
d7d7a5a80a
commit
b7c89d1101
226
tests/test_openfang_client.py
Normal file
226
tests/test_openfang_client.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""Chunk 2: OpenFang HTTP client — test first, implement second.
|
||||
|
||||
Tests cover:
|
||||
- Health check returns False when unreachable
|
||||
- Health check TTL caching
|
||||
- execute_hand() rejects unknown hands
|
||||
- execute_hand() success with mocked HTTP
|
||||
- execute_hand() graceful degradation on error
|
||||
- Convenience wrappers call the correct hand
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Health checks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_health_check_false_when_unreachable():
|
||||
"""Client should report unhealthy when OpenFang is not running."""
|
||||
from infrastructure.openfang.client import OpenFangClient
|
||||
|
||||
client = OpenFangClient(base_url="http://localhost:19999")
|
||||
assert client._check_health() is False
|
||||
|
||||
|
||||
def test_health_check_caching():
|
||||
"""Repeated .healthy calls within TTL should not re-check."""
|
||||
from infrastructure.openfang.client import OpenFangClient
|
||||
|
||||
client = OpenFangClient(base_url="http://localhost:19999")
|
||||
client._health_cache_ttl = 9999 # very long TTL
|
||||
# Force a first check (will be False)
|
||||
_ = client.healthy
|
||||
assert client._healthy is False
|
||||
|
||||
# Manually flip the cached value — next access should use cache
|
||||
client._healthy = True
|
||||
assert client.healthy is True # still cached, no re-check
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# execute_hand — unknown hand
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_hand_unknown_hand():
|
||||
"""Requesting an unknown hand returns success=False immediately."""
|
||||
from infrastructure.openfang.client import OpenFangClient
|
||||
|
||||
client = OpenFangClient(base_url="http://localhost:19999")
|
||||
result = await client.execute_hand("nonexistent_hand", {})
|
||||
assert result.success is False
|
||||
assert "Unknown hand" in result.error
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# execute_hand — success path (mocked HTTP)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_hand_success_mocked():
|
||||
"""When OpenFang returns 200 with output, HandResult.success is True."""
|
||||
from infrastructure.openfang.client import OpenFangClient
|
||||
|
||||
response_body = json.dumps({
|
||||
"success": True,
|
||||
"output": "Page loaded successfully",
|
||||
"metadata": {"url": "https://example.com"},
|
||||
}).encode()
|
||||
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status = 200
|
||||
mock_resp.read.return_value = response_body
|
||||
mock_resp.__enter__ = lambda s: s
|
||||
mock_resp.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
with patch("urllib.request.urlopen", return_value=mock_resp):
|
||||
client = OpenFangClient(base_url="http://localhost:8080")
|
||||
result = await client.execute_hand("browser", {"url": "https://example.com"})
|
||||
|
||||
assert result.success is True
|
||||
assert result.output == "Page loaded successfully"
|
||||
assert result.hand == "browser"
|
||||
assert result.latency_ms > 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# execute_hand — graceful degradation on connection error
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_hand_connection_error():
|
||||
"""When OpenFang is unreachable, HandResult.success is False (no crash)."""
|
||||
from infrastructure.openfang.client import OpenFangClient
|
||||
|
||||
client = OpenFangClient(base_url="http://localhost:19999")
|
||||
result = await client.execute_hand("browser", {"url": "https://example.com"})
|
||||
|
||||
assert result.success is False
|
||||
assert result.error # non-empty error message
|
||||
assert result.hand == "browser"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Convenience wrappers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_browse_calls_browser_hand():
|
||||
"""browse() should delegate to execute_hand('browser', ...)."""
|
||||
from infrastructure.openfang.client import OpenFangClient
|
||||
|
||||
client = OpenFangClient(base_url="http://localhost:19999")
|
||||
|
||||
calls = []
|
||||
original = client.execute_hand
|
||||
|
||||
async def spy(hand, params, **kw):
|
||||
calls.append((hand, params))
|
||||
return await original(hand, params, **kw)
|
||||
|
||||
client.execute_hand = spy
|
||||
await client.browse("https://example.com", "click button")
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0][0] == "browser"
|
||||
assert calls[0][1]["url"] == "https://example.com"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collect_calls_collector_hand():
|
||||
"""collect() should delegate to execute_hand('collector', ...)."""
|
||||
from infrastructure.openfang.client import OpenFangClient
|
||||
|
||||
client = OpenFangClient(base_url="http://localhost:19999")
|
||||
|
||||
calls = []
|
||||
original = client.execute_hand
|
||||
|
||||
async def spy(hand, params, **kw):
|
||||
calls.append((hand, params))
|
||||
return await original(hand, params, **kw)
|
||||
|
||||
client.execute_hand = spy
|
||||
await client.collect("example.com", depth="deep")
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0][0] == "collector"
|
||||
assert calls[0][1]["target"] == "example.com"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_predict_calls_predictor_hand():
|
||||
"""predict() should delegate to execute_hand('predictor', ...)."""
|
||||
from infrastructure.openfang.client import OpenFangClient
|
||||
|
||||
client = OpenFangClient(base_url="http://localhost:19999")
|
||||
|
||||
calls = []
|
||||
original = client.execute_hand
|
||||
|
||||
async def spy(hand, params, **kw):
|
||||
calls.append((hand, params))
|
||||
return await original(hand, params, **kw)
|
||||
|
||||
client.execute_hand = spy
|
||||
await client.predict("Will BTC hit 100k?", horizon="1m")
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0][0] == "predictor"
|
||||
assert calls[0][1]["question"] == "Will BTC hit 100k?"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HandResult dataclass
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_hand_result_defaults():
|
||||
"""HandResult should have sensible defaults."""
|
||||
from infrastructure.openfang.client import HandResult
|
||||
|
||||
r = HandResult(hand="browser", success=True)
|
||||
assert r.output == ""
|
||||
assert r.error == ""
|
||||
assert r.latency_ms == 0.0
|
||||
assert r.metadata == {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OPENFANG_HANDS constant
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_openfang_hands_tuple():
|
||||
"""The OPENFANG_HANDS constant should list all 7 hands."""
|
||||
from infrastructure.openfang.client import OPENFANG_HANDS
|
||||
|
||||
assert len(OPENFANG_HANDS) == 7
|
||||
assert "browser" in OPENFANG_HANDS
|
||||
assert "collector" in OPENFANG_HANDS
|
||||
assert "predictor" in OPENFANG_HANDS
|
||||
assert "lead" in OPENFANG_HANDS
|
||||
assert "twitter" in OPENFANG_HANDS
|
||||
assert "researcher" in OPENFANG_HANDS
|
||||
assert "clip" in OPENFANG_HANDS
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# status() summary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_status_returns_summary():
|
||||
"""status() should return a dict with url, healthy flag, and hands list."""
|
||||
from infrastructure.openfang.client import OpenFangClient
|
||||
|
||||
client = OpenFangClient(base_url="http://localhost:19999")
|
||||
s = client.status()
|
||||
|
||||
assert "url" in s
|
||||
assert "healthy" in s
|
||||
assert "available_hands" in s
|
||||
assert len(s["available_hands"]) == 7
|
||||
Reference in New Issue
Block a user