"""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