mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Service-level tests now exercise provider, permissions, project, journal, messaging, work_session, metrics, kanban, extraction, learning, notification, dashboard, llm_routing, a2a, task, repository_base, audit, db_seed, branch_name, indexed_document, query_helpers, agent. API route tests cover provider, journal, project, sessions, dashboard, work_session, tasks, a2a, groups, notifications, agents, channels, messages, kanban, api_resources. Pure-function helpers covered: handlers, deps_helpers, middleware, middleware_docs, transcription, pr templates, agents_config, errors, logging, journal/notification/channel/a2a access, task_lifecycle, streaming, converters, crypto, schemas (common + websocket), events, permissions extras. pyproject ruff per-file-ignores extended for tests so PLR2004 (status code magic values), PLC0415 (lazy imports), PLR0913 (fixture params), ARG001 (unused fixture deps), SIM105, and E501 don't fight test idioms.
449 lines
14 KiB
Python
449 lines
14 KiB
Python
"""Tasks API route coverage — list/get/lifecycle endpoints."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from fastapi import FastAPI
|
|
from httpx import ASGITransport, AsyncClient
|
|
from roboco.api.deps import get_agent_context, get_db
|
|
from roboco.api.routes.tasks import router as tasks_router
|
|
from roboco.db.tables import AgentTable, ProjectTable, TaskTable
|
|
from roboco.models import AgentRole, AgentStatus, Team
|
|
from roboco.models.base import (
|
|
TaskNature,
|
|
TaskStatus,
|
|
TaskType,
|
|
)
|
|
from roboco.models.permissions import AgentContext
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import AsyncIterator
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def task_client(
|
|
db_session: AsyncSession,
|
|
) -> AsyncIterator[dict]:
|
|
main_pm = AgentTable(
|
|
id=uuid4(),
|
|
name="MainPM",
|
|
slug=f"main-pm-{uuid4().hex[:8]}",
|
|
role=AgentRole.MAIN_PM,
|
|
team=None,
|
|
status=AgentStatus.ACTIVE,
|
|
model_config={},
|
|
system_prompt="pm",
|
|
capabilities=[],
|
|
permissions={},
|
|
metrics={},
|
|
)
|
|
db_session.add(main_pm)
|
|
await db_session.flush()
|
|
project = ProjectTable(
|
|
id=uuid4(),
|
|
name="TR-Proj",
|
|
slug=f"tr-proj-{uuid4().hex[:6]}",
|
|
git_url="https://example.com/r.git",
|
|
assigned_cell=Team.BACKEND,
|
|
created_by=main_pm.id,
|
|
)
|
|
db_session.add(project)
|
|
await db_session.flush()
|
|
|
|
app = FastAPI()
|
|
app.include_router(tasks_router, prefix="/api/tasks")
|
|
|
|
async def _override_db():
|
|
yield db_session
|
|
|
|
async def _override_agent() -> AgentContext:
|
|
return AgentContext(agent_id=main_pm.id, role=AgentRole.MAIN_PM, team=None)
|
|
|
|
app.dependency_overrides[get_db] = _override_db
|
|
app.dependency_overrides[get_agent_context] = _override_agent
|
|
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
yield {
|
|
"client": client,
|
|
"agent": main_pm,
|
|
"project": project,
|
|
"db": db_session,
|
|
}
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
_HDR = {"X-Agent-ID": str(uuid4()), "X-Agent-Role": "main_pm"}
|
|
|
|
|
|
def _seed_task(
|
|
setup: dict, *, status: TaskStatus = TaskStatus.PENDING, **kw
|
|
) -> TaskTable:
|
|
task = TaskTable(
|
|
id=uuid4(),
|
|
title=kw.pop("title", "t"),
|
|
description=kw.pop("description", "d"),
|
|
acceptance_criteria=["ac"],
|
|
status=status,
|
|
priority=kw.pop("priority", 2),
|
|
task_type=TaskType.CODE,
|
|
nature=TaskNature.TECHNICAL,
|
|
project_id=setup["project"].id,
|
|
created_by=setup["agent"].id,
|
|
team=kw.pop("team", Team.BACKEND),
|
|
**kw,
|
|
)
|
|
setup["db"].add(task)
|
|
return task
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_task(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(
|
|
"/api/tasks",
|
|
json={
|
|
"title": "Test Task",
|
|
"description": "Some description",
|
|
"acceptance_criteria": ["criteria"],
|
|
"team": "backend",
|
|
"project_id": str(task_client["project"].id),
|
|
},
|
|
headers=_HDR,
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_task_missing_project_id(task_client: dict) -> None:
|
|
"""Create with no project_id should fail validation."""
|
|
client = task_client["client"]
|
|
response = await client.post(
|
|
"/api/tasks",
|
|
json={
|
|
"title": "Test",
|
|
"description": "x",
|
|
"acceptance_criteria": ["a"],
|
|
"team": "backend",
|
|
},
|
|
headers=_HDR,
|
|
)
|
|
assert response.status_code in (400, 422)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_tasks(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
_seed_task(task_client)
|
|
await task_client["db"].flush()
|
|
response = await client.get("/api/tasks", headers=_HDR)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_tasks_filter_by_team(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.get("/api/tasks?team=backend", headers=_HDR)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_tasks_filter_by_status(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.get("/api/tasks?status=pending", headers=_HDR)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_my_tasks(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.get("/api/tasks/my", headers=_HDR)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_pending_tasks(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.get("/api/tasks/pending", headers=_HDR)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_blocked_tasks(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.get("/api/tasks/blocked", headers=_HDR)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_awaiting_qa(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.get("/api/tasks/awaiting-qa", headers=_HDR)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_task_not_found(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.get(f"/api/tasks/{uuid4()}", headers=_HDR)
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_task_by_id(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
task = _seed_task(task_client)
|
|
await task_client["db"].flush()
|
|
response = await client.get(f"/api/tasks/{task.id}", headers=_HDR)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_task(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
task = _seed_task(task_client)
|
|
await task_client["db"].flush()
|
|
response = await client.patch(
|
|
f"/api/tasks/{task.id}",
|
|
json={"title": "Renamed"},
|
|
headers=_HDR,
|
|
)
|
|
assert response.status_code in (200, 422)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_task(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
task = _seed_task(task_client)
|
|
await task_client["db"].flush()
|
|
response = await client.delete(f"/api/tasks/{task.id}", headers=_HDR)
|
|
assert response.status_code in (200, 204, 422)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_task_not_found(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.delete(f"/api/tasks/{uuid4()}", headers=_HDR)
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_subtasks_of_unknown_task(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.get(f"/api/tasks/{uuid4()}/subtasks", headers=_HDR)
|
|
# Either 404 or empty list depending on implementation.
|
|
assert response.status_code in (200, 404)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_count_endpoint_returns_response(task_client: dict) -> None:
|
|
"""Count route may take query params we don't supply; just ensure it's reached."""
|
|
client = task_client["client"]
|
|
response = await client.get("/api/tasks/count", headers=_HDR)
|
|
assert response.status_code in (200, 422)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Additional list endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_awaiting_docs(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.get("/api/tasks/awaiting-docs", headers=_HDR)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_team_tasks(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.get("/api/tasks/team/backend", headers=_HDR)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_task_stats(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.get("/api/tasks/stats", headers=_HDR)
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_task_stats_by_team(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.get("/api/tasks/stats/by-team", headers=_HDR)
|
|
assert response.status_code == 200
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Lifecycle: claim/unclaim (404 paths)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_claim_unknown_task_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(
|
|
f"/api/tasks/{uuid4()}/claim",
|
|
json={"role": "developer"},
|
|
headers=_HDR,
|
|
)
|
|
assert response.status_code in (400, 403, 404, 422)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unclaim_unknown_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(f"/api/tasks/{uuid4()}/unclaim", headers=_HDR)
|
|
assert response.status_code in (400, 404)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_submit_for_qa_unknown_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(
|
|
f"/api/tasks/{uuid4()}/submit-qa",
|
|
json={},
|
|
headers=_HDR,
|
|
)
|
|
assert response.status_code in (400, 404, 422)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pass_qa_unknown_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(
|
|
f"/api/tasks/{uuid4()}/pass-qa",
|
|
json={"notes": "looks good and is sufficiently detailed"},
|
|
headers=_HDR,
|
|
)
|
|
assert response.status_code in (400, 403, 404, 422)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fail_qa_unknown_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(
|
|
f"/api/tasks/{uuid4()}/fail-qa",
|
|
json={"notes": "broken in many ways"},
|
|
headers=_HDR,
|
|
)
|
|
assert response.status_code in (400, 403, 404, 422)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_complete_unknown_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(
|
|
f"/api/tasks/{uuid4()}/complete",
|
|
json={},
|
|
headers=_HDR,
|
|
)
|
|
assert response.status_code in (400, 403, 404, 422)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_block_unknown_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(
|
|
f"/api/tasks/{uuid4()}/block",
|
|
json={"reason": "blocker", "blocker_type": "external", "what_needed": "x"},
|
|
headers=_HDR,
|
|
)
|
|
assert response.status_code in (400, 403, 404, 422)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unblock_unknown_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(f"/api/tasks/{uuid4()}/unblock", headers=_HDR)
|
|
assert response.status_code in (400, 403, 404, 422)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pause_unknown_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(f"/api/tasks/{uuid4()}/pause", headers=_HDR)
|
|
assert response.status_code in (400, 403, 404, 422)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resume_unknown_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(f"/api/tasks/{uuid4()}/resume", headers=_HDR)
|
|
assert response.status_code in (400, 403, 404, 422)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_unknown_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(
|
|
f"/api/tasks/{uuid4()}/cancel",
|
|
json={"reason": "no longer needed"},
|
|
headers=_HDR,
|
|
)
|
|
assert response.status_code in (400, 403, 404, 422)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_progress_unknown_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(
|
|
f"/api/tasks/{uuid4()}/progress",
|
|
json={"message": "doing things", "percentage": 25},
|
|
headers=_HDR,
|
|
)
|
|
assert response.status_code in (400, 404)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_checkpoint_unknown_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(
|
|
f"/api/tasks/{uuid4()}/checkpoints",
|
|
json={
|
|
"state_summary": "halfway",
|
|
"remaining_work": ["finish API"],
|
|
},
|
|
headers=_HDR,
|
|
)
|
|
assert response.status_code in (400, 404)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_commit_unknown_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(
|
|
f"/api/tasks/{uuid4()}/commits",
|
|
json={"hash": "abc123", "message": "fix"},
|
|
headers=_HDR,
|
|
)
|
|
assert response.status_code in (400, 404)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_escalate_unknown_returns_404(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
response = await client.post(
|
|
f"/api/tasks/{uuid4()}/escalate",
|
|
json={"reason": "needs PM input"},
|
|
headers=_HDR,
|
|
)
|
|
assert response.status_code in (400, 403, 404, 422)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_sessions_for_task(task_client: dict) -> None:
|
|
client = task_client["client"]
|
|
task = _seed_task(task_client)
|
|
await task_client["db"].flush()
|
|
response = await client.get(f"/api/tasks/{task.id}/sessions", headers=_HDR)
|
|
assert response.status_code == 200
|