This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/tests/test_openfang_client.py
Alexander Whitestone 9d78eb31d1 ruff (#169)
* polish: streamline nav, extract inline styles, improve tablet UX

- Restructure desktop nav from 8+ flat links + overflow dropdown into
  5 grouped dropdowns (Core, Agents, Intel, System, More) matching
  the mobile menu structure to reduce decision fatigue
- Extract all inline styles from mission_control.html and base.html
  notification elements into mission-control.css with semantic classes
- Replace JS-built innerHTML with secure DOM construction in
  notification loader and chat history
- Add CONNECTING state to connection indicator (amber) instead of
  showing OFFLINE before WebSocket connects
- Add tablet breakpoint (1024px) with larger touch targets for
  Apple Pencil / stylus use and safe-area padding for iPad toolbar
- Add active-link highlighting in desktop dropdown menus
- Rename "Mission Control" page title to "System Overview" to
  disambiguate from the chat home page
- Add "Home — Timmy Time" page title to index.html

https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h

* fix(security): move auth-gate credentials to environment variables

Hardcoded username, password, and HMAC secret in auth-gate.py replaced
with os.environ lookups. Startup now refuses to run if any variable is
unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example.

https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h

* refactor(tooling): migrate from black+isort+bandit to ruff

Replace three separate linting/formatting tools with a single ruff
invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs),
.pre-commit-config.yaml, and CI workflow. Fixes all ruff errors
including unused imports, missing raise-from, and undefined names.
Ruff config maps existing bandit skips to equivalent S-rules.

https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 12:23:35 -04:00

235 lines
7.6 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 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