forked from Rockachopa/Timmy-time-dashboard
* 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>
235 lines
7.6 KiB
Python
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
|