"""Tests for the Task Queue system.""" import json import os import sqlite3 from pathlib import Path from unittest.mock import MagicMock, patch import pytest # Set test mode before importing app modules os.environ["TIMMY_TEST_MODE"] = "1" # ── Model Tests ────────────────────────────────────────────────────────── def test_create_task(): from task_queue.models import create_task, TaskStatus, TaskPriority task = create_task( title="Test task", description="A test description", assigned_to="timmy", created_by="user", priority="normal", ) assert task.id assert task.title == "Test task" assert task.status == TaskStatus.PENDING_APPROVAL assert task.priority == TaskPriority.NORMAL assert task.assigned_to == "timmy" assert task.created_by == "user" def test_get_task(): from task_queue.models import create_task, get_task task = create_task(title="Get me", created_by="test") retrieved = get_task(task.id) assert retrieved is not None assert retrieved.title == "Get me" def test_get_task_not_found(): from task_queue.models import get_task assert get_task("nonexistent-id") is None def test_list_tasks(): from task_queue.models import create_task, list_tasks, TaskStatus create_task(title="List test 1", created_by="test") create_task(title="List test 2", created_by="test") tasks = list_tasks() assert len(tasks) >= 2 def test_list_tasks_with_status_filter(): from task_queue.models import ( create_task, list_tasks, update_task_status, TaskStatus, ) task = create_task(title="Filter test", created_by="test") update_task_status(task.id, TaskStatus.APPROVED) approved = list_tasks(status=TaskStatus.APPROVED) assert any(t.id == task.id for t in approved) def test_update_task_status(): from task_queue.models import ( create_task, update_task_status, TaskStatus, ) task = create_task(title="Status test", created_by="test") updated = update_task_status(task.id, TaskStatus.APPROVED) assert updated.status == TaskStatus.APPROVED def test_update_task_running_sets_started_at(): from task_queue.models import ( create_task, update_task_status, TaskStatus, ) task = create_task(title="Running test", created_by="test") updated = update_task_status(task.id, TaskStatus.RUNNING) assert updated.started_at is not None def test_update_task_completed_sets_completed_at(): from task_queue.models import ( create_task, update_task_status, TaskStatus, ) task = create_task(title="Complete test", created_by="test") updated = update_task_status(task.id, TaskStatus.COMPLETED, result="Done!") assert updated.completed_at is not None assert updated.result == "Done!" def test_update_task_fields(): from task_queue.models import create_task, update_task task = create_task(title="Modify test", created_by="test") updated = update_task(task.id, title="Modified title", priority="high") assert updated.title == "Modified title" assert updated.priority.value == "high" def test_get_counts_by_status(): from task_queue.models import create_task, get_counts_by_status create_task(title="Count test", created_by="test") counts = get_counts_by_status() assert "pending_approval" in counts def test_get_pending_count(): from task_queue.models import create_task, get_pending_count create_task(title="Pending count test", created_by="test") count = get_pending_count() assert count >= 1 def test_update_task_steps(): from task_queue.models import create_task, update_task_steps, get_task task = create_task(title="Steps test", created_by="test") steps = [ {"description": "Step 1", "status": "completed"}, {"description": "Step 2", "status": "running"}, ] ok = update_task_steps(task.id, steps) assert ok retrieved = get_task(task.id) assert len(retrieved.steps) == 2 assert retrieved.steps[0]["description"] == "Step 1" def test_auto_approve_not_triggered_by_default(): from task_queue.models import create_task, TaskStatus task = create_task(title="No auto", created_by="user", auto_approve=False) assert task.status == TaskStatus.PENDING_APPROVAL def test_get_task_summary_for_briefing(): from task_queue.models import create_task, get_task_summary_for_briefing create_task(title="Briefing test", created_by="test") summary = get_task_summary_for_briefing() assert "pending_approval" in summary assert "total" in summary # ── Route Tests ────────────────────────────────────────────────────────── @pytest.fixture def client(): """FastAPI test client.""" from fastapi.testclient import TestClient from dashboard.app import app return TestClient(app) def test_tasks_page(client): resp = client.get("/tasks") assert resp.status_code == 200 assert "TASK QUEUE" in resp.text def test_api_list_tasks(client): resp = client.get("/api/tasks") assert resp.status_code == 200 data = resp.json() assert "tasks" in data assert "count" in data def test_api_create_task(client): resp = client.post( "/api/tasks", json={ "title": "API created task", "description": "Test via API", "assigned_to": "timmy", "priority": "high", }, ) assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert data["task"]["title"] == "API created task" assert data["task"]["status"] == "pending_approval" def test_api_task_counts(client): resp = client.get("/api/tasks/counts") assert resp.status_code == 200 data = resp.json() assert "pending" in data assert "total" in data def test_form_create_task(client): resp = client.post( "/tasks/create", data={ "title": "Form created task", "description": "From form", "assigned_to": "forge", "priority": "normal", }, ) assert resp.status_code == 200 assert "Form created task" in resp.text def test_approve_task_htmx(client): # Create then approve create_resp = client.post( "/api/tasks", json={"title": "To approve", "assigned_to": "timmy"}, ) task_id = create_resp.json()["task"]["id"] resp = client.post(f"/tasks/{task_id}/approve") assert resp.status_code == 200 assert "APPROVED" in resp.text.upper() or "approved" in resp.text def test_veto_task_htmx(client): create_resp = client.post( "/api/tasks", json={"title": "To veto", "assigned_to": "timmy"}, ) task_id = create_resp.json()["task"]["id"] resp = client.post(f"/tasks/{task_id}/veto") assert resp.status_code == 200 assert "VETOED" in resp.text.upper() or "vetoed" in resp.text def test_modify_task_htmx(client): create_resp = client.post( "/api/tasks", json={"title": "To modify", "assigned_to": "timmy"}, ) task_id = create_resp.json()["task"]["id"] resp = client.post( f"/tasks/{task_id}/modify", data={"title": "Modified via HTMX"}, ) assert resp.status_code == 200 assert "Modified via HTMX" in resp.text def test_cancel_task_htmx(client): create_resp = client.post( "/api/tasks", json={"title": "To cancel", "assigned_to": "timmy"}, ) task_id = create_resp.json()["task"]["id"] resp = client.post(f"/tasks/{task_id}/cancel") assert resp.status_code == 200 def test_retry_failed_task(client): from task_queue.models import create_task, update_task_status, TaskStatus task = create_task(title="To retry", created_by="test") update_task_status(task.id, TaskStatus.FAILED, result="Something broke") resp = client.post(f"/tasks/{task.id}/retry") assert resp.status_code == 200 def test_pending_partial(client): resp = client.get("/tasks/pending") assert resp.status_code == 200 def test_active_partial(client): resp = client.get("/tasks/active") assert resp.status_code == 200 def test_completed_partial(client): resp = client.get("/tasks/completed") assert resp.status_code == 200 def test_api_approve_nonexistent(client): resp = client.patch("/api/tasks/nonexistent/approve") assert resp.status_code == 404 def test_api_veto_nonexistent(client): resp = client.patch("/api/tasks/nonexistent/veto") assert resp.status_code == 404 # ── Chat → Task Queue Integration ───────────────────────────────────────── def test_chat_queue_detection_add_to_queue(): """'add X to the queue' should be detected as a task request.""" from dashboard.routes.agents import _extract_task_from_message result = _extract_task_from_message("add run the tests to the task queue") assert result is not None assert "title" in result assert "description" in result def test_chat_queue_detection_schedule(): """'schedule this' should be detected as a task request.""" from dashboard.routes.agents import _extract_task_from_message result = _extract_task_from_message("schedule this for later") assert result is not None def test_chat_queue_detection_create_task(): """'create a task' should be detected.""" from dashboard.routes.agents import _extract_task_from_message result = _extract_task_from_message("create a task to refactor the login page") assert result is not None assert "refactor" in result["title"].lower() def test_chat_queue_detection_normal_message(): """Normal messages should NOT be detected as task requests.""" from dashboard.routes.agents import _extract_task_from_message assert _extract_task_from_message("hello how are you") is None assert _extract_task_from_message("what is the weather today") is None assert _extract_task_from_message("tell me a joke") is None def test_chat_creates_task_on_queue_request(client): """Posting 'add X to the queue' via chat should create a task.""" with patch("dashboard.routes.agents.timmy_chat") as mock_chat: mock_chat.return_value = "Sure, I'll do that." resp = client.post( "/agents/timmy/chat", data={"message": "add deploy the new feature to the task queue"}, ) assert resp.status_code == 200 assert "Task queued" in resp.text or "task queue" in resp.text.lower() # timmy_chat should NOT have been called — task was intercepted mock_chat.assert_not_called() def test_chat_normal_message_uses_timmy(client): """Normal messages should go through to Timmy as usual.""" with patch("dashboard.routes.agents.timmy_chat") as mock_chat: mock_chat.return_value = "Hello there!" resp = client.post( "/agents/timmy/chat", data={"message": "hello how are you"}, ) assert resp.status_code == 200 mock_chat.assert_called_once() # ── Briefing Integration ────────────────────────────────────────────────── def test_briefing_task_queue_summary(): """Briefing engine should include task queue data.""" from task_queue.models import create_task from timmy.briefing import _gather_task_queue_summary create_task(title="Briefing integration test", created_by="test") summary = _gather_task_queue_summary() assert "pending" in summary.lower() or "task" in summary.lower()