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/integrations/test_paperclip_client.py
Trip T ea2dbdb4b5 fix: test DB isolation, Discord recovery, and over-mocked tests
Test data was bleeding into production tasks.db because
swarm.task_queue.models.DB_PATH (relative path) was never patched in
conftest.clean_database. Fixed by switching to absolute paths via
settings.repo_root and adding the missing module to the patching list.

Discord bot could leak orphaned clients on retry after ERROR state.
Added _cleanup_stale() to close stale client/task before each start()
attempt, with improved logging in the token watcher.

Rewrote test_paperclip_client.py to use httpx.MockTransport instead of
patching _get/_post/_delete — tests now exercise real HTTP status codes,
error handling, and JSON parsing. Added end-to-end test for
capture_error → create_task DB isolation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 20:33:59 -04:00

214 lines
7.3 KiB
Python

"""Tests for the Paperclip API client.
Uses httpx.MockTransport so every test exercises the real HTTP path
(_get/_post/_delete, status-code handling, JSON parsing, error paths)
instead of patching the transport methods away.
"""
import json
from unittest.mock import patch
import httpx
from integrations.paperclip.client import PaperclipClient
from integrations.paperclip.models import CreateIssueRequest
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _mock_transport(routes: dict[str, tuple[int, dict | list | None]]):
"""Build an httpx.MockTransport from a {method+path: (status, body)} map.
Example:
_mock_transport({
"GET /api/health": (200, {"status": "ok"}),
"DELETE /api/issues/i1": (204, None),
})
"""
def handler(request: httpx.Request) -> httpx.Response:
key = f"{request.method} {request.url.path}"
if key in routes:
status, body = routes[key]
content = json.dumps(body).encode() if body is not None else b""
return httpx.Response(
status, content=content, headers={"content-type": "application/json"}
)
return httpx.Response(404, json={"error": "not found"})
return httpx.MockTransport(handler)
def _client_with(routes: dict[str, tuple[int, dict | list | None]]) -> PaperclipClient:
"""Create a PaperclipClient whose internal httpx.AsyncClient uses a mock transport."""
client = PaperclipClient(base_url="http://fake:3100", api_key="test-key")
client._client = httpx.AsyncClient(
transport=_mock_transport(routes),
base_url="http://fake:3100",
headers={"Accept": "application/json", "Authorization": "Bearer test-key"},
)
return client
# ---------------------------------------------------------------------------
# health
# ---------------------------------------------------------------------------
async def test_healthy_returns_true_on_200():
client = _client_with({"GET /api/health": (200, {"status": "ok"})})
assert await client.healthy() is True
async def test_healthy_returns_false_on_500():
client = _client_with({"GET /api/health": (500, {"error": "down"})})
assert await client.healthy() is False
async def test_healthy_returns_false_on_404():
client = _client_with({}) # no routes → 404
assert await client.healthy() is False
# ---------------------------------------------------------------------------
# agents
# ---------------------------------------------------------------------------
async def test_list_agents_parses_response():
raw = [{"id": "a1", "name": "Codex", "role": "engineer", "status": "active"}]
client = _client_with({"GET /api/companies/comp-1/agents": (200, raw)})
agents = await client.list_agents(company_id="comp-1")
assert len(agents) == 1
assert agents[0].name == "Codex"
assert agents[0].id == "a1"
async def test_list_agents_empty_on_server_error():
client = _client_with({"GET /api/companies/comp-1/agents": (503, None)})
agents = await client.list_agents(company_id="comp-1")
assert agents == []
async def test_list_agents_graceful_on_404():
client = _client_with({})
agents = await client.list_agents(company_id="comp-1")
assert agents == []
# ---------------------------------------------------------------------------
# issues
# ---------------------------------------------------------------------------
async def test_list_issues():
raw = [{"id": "i1", "title": "Fix bug"}]
client = _client_with({"GET /api/companies/comp-1/issues": (200, raw)})
issues = await client.list_issues(company_id="comp-1")
assert len(issues) == 1
assert issues[0].title == "Fix bug"
async def test_get_issue():
raw = {"id": "i1", "title": "Fix bug", "description": "It's broken"}
client = _client_with({"GET /api/issues/i1": (200, raw)})
issue = await client.get_issue("i1")
assert issue is not None
assert issue.id == "i1"
async def test_get_issue_not_found():
client = _client_with({"GET /api/issues/nonexistent": (404, None)})
issue = await client.get_issue("nonexistent")
assert issue is None
async def test_create_issue():
raw = {"id": "i2", "title": "New feature"}
client = _client_with({"POST /api/companies/comp-1/issues": (201, raw)})
req = CreateIssueRequest(title="New feature")
issue = await client.create_issue(req, company_id="comp-1")
assert issue is not None
assert issue.id == "i2"
async def test_create_issue_no_company_id():
"""Missing company_id returns None without making any HTTP call."""
client = _client_with({})
with patch("integrations.paperclip.client.settings") as mock_settings:
mock_settings.paperclip_company_id = ""
issue = await client.create_issue(CreateIssueRequest(title="Test"))
assert issue is None
async def test_delete_issue_returns_true_on_success():
client = _client_with({"DELETE /api/issues/i1": (204, None)})
result = await client.delete_issue("i1")
assert result is True
async def test_delete_issue_returns_false_on_error():
client = _client_with({"DELETE /api/issues/i1": (500, None)})
result = await client.delete_issue("i1")
assert result is False
# ---------------------------------------------------------------------------
# comments
# ---------------------------------------------------------------------------
async def test_add_comment():
raw = {"id": "c1", "issue_id": "i1", "content": "Done"}
client = _client_with({"POST /api/issues/i1/comments": (201, raw)})
comment = await client.add_comment("i1", "Done")
assert comment is not None
assert comment.content == "Done"
async def test_list_comments():
raw = [{"id": "c1", "issue_id": "i1", "content": "LGTM"}]
client = _client_with({"GET /api/issues/i1/comments": (200, raw)})
comments = await client.list_comments("i1")
assert len(comments) == 1
# ---------------------------------------------------------------------------
# goals
# ---------------------------------------------------------------------------
async def test_list_goals():
raw = [{"id": "g1", "title": "Ship MVP"}]
client = _client_with({"GET /api/companies/comp-1/goals": (200, raw)})
goals = await client.list_goals(company_id="comp-1")
assert len(goals) == 1
assert goals[0].title == "Ship MVP"
async def test_create_goal():
raw = {"id": "g2", "title": "Scale to 1000 users"}
client = _client_with({"POST /api/companies/comp-1/goals": (201, raw)})
goal = await client.create_goal("Scale to 1000 users", company_id="comp-1")
assert goal is not None
# ---------------------------------------------------------------------------
# heartbeat runs
# ---------------------------------------------------------------------------
async def test_list_heartbeat_runs():
raw = [{"id": "r1", "agent_id": "a1", "status": "running"}]
client = _client_with({"GET /api/companies/comp-1/heartbeat-runs": (200, raw)})
runs = await client.list_heartbeat_runs(company_id="comp-1")
assert len(runs) == 1
async def test_list_heartbeat_runs_server_error():
client = _client_with({"GET /api/companies/comp-1/heartbeat-runs": (500, None)})
runs = await client.list_heartbeat_runs(company_id="comp-1")
assert runs == []