forked from Rockachopa/Timmy-time-dashboard
227 lines
7.5 KiB
Python
227 lines
7.5 KiB
Python
"""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
|