forked from Rockachopa/Timmy-time-dashboard
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>
214 lines
7.3 KiB
Python
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 == []
|