Files
pocketpaw/tests/test_deep_work_api.py
Rohit Kushwaha 5542941ece fix: rename Python package from pocketclaw to pocketpaw
Complete the package rename: src/pocketclaw/ → src/pocketpaw/,
all imports, pyproject.toml entry point, docs code examples,
installer references, and test patch targets updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 23:54:14 +05:30

474 lines
17 KiB
Python

# Tests for Deep Work project CRUD — Manager + API endpoints
# Created: 2026-02-12
# Tests project lifecycle via MissionControlManager and FastAPI endpoints
import tempfile
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pocketpaw.mission_control import (
FileMissionControlStore,
MissionControlManager,
reset_mission_control_manager,
reset_mission_control_store,
)
from pocketpaw.mission_control.api import router
from pocketpaw.mission_control.models import TaskStatus
# ============================================================================
# Fixtures
# ============================================================================
@pytest.fixture
def temp_store_path():
"""Create a temporary directory for test storage."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
@pytest.fixture
def test_app(temp_store_path, monkeypatch):
"""Create a test FastAPI app with Mission Control router."""
reset_mission_control_store()
reset_mission_control_manager()
store = FileMissionControlStore(temp_store_path)
manager = MissionControlManager(store)
import pocketpaw.mission_control.manager as manager_module
import pocketpaw.mission_control.store as store_module
monkeypatch.setattr(store_module, "_store_instance", store)
monkeypatch.setattr(manager_module, "_manager_instance", manager)
app = FastAPI()
app.include_router(router, prefix="/api/mission-control")
return app
@pytest.fixture
def client(test_app):
"""Create a test client."""
return TestClient(test_app)
@pytest.fixture
def manager(test_app, monkeypatch):
"""Get the test manager instance."""
import pocketpaw.mission_control.manager as manager_module
return manager_module._manager_instance
# ============================================================================
# Manager Tests
# ============================================================================
class TestProjectManager:
"""Tests for project operations on MissionControlManager."""
@pytest.mark.asyncio
async def test_create_project(self, manager):
"""Test creating a project via the manager."""
project = await manager.create_project(
title="Test Project",
description="A test project",
tags=["test", "demo"],
)
assert project.title == "Test Project"
assert project.description == "A test project"
assert project.tags == ["test", "demo"]
assert project.creator_id == "human"
assert project.status.value == "draft"
assert project.id is not None
@pytest.mark.asyncio
async def test_get_project(self, manager):
"""Test retrieving a project by ID."""
created = await manager.create_project(title="Fetch Me")
fetched = await manager.get_project(created.id)
assert fetched is not None
assert fetched.id == created.id
assert fetched.title == "Fetch Me"
@pytest.mark.asyncio
async def test_get_project_not_found(self, manager):
"""Test retrieving a non-existent project."""
result = await manager.get_project("nonexistent-id")
assert result is None
@pytest.mark.asyncio
async def test_list_projects(self, manager):
"""Test listing all projects."""
await manager.create_project(title="Project A")
await manager.create_project(title="Project B")
projects = await manager.list_projects()
assert len(projects) == 2
@pytest.mark.asyncio
async def test_list_projects_by_status(self, manager):
"""Test listing projects filtered by status."""
p1 = await manager.create_project(title="Draft Project")
p2 = await manager.create_project(title="Approved Project")
# Move p2 to approved
from pocketpaw.deep_work.models import ProjectStatus
p2.status = ProjectStatus.APPROVED
await manager.update_project(p2)
drafts = await manager.list_projects(status="draft")
assert len(drafts) == 1
assert drafts[0].id == p1.id
approved = await manager.list_projects(status="approved")
assert len(approved) == 1
assert approved[0].id == p2.id
@pytest.mark.asyncio
async def test_get_project_tasks(self, manager):
"""Test getting tasks that belong to a project."""
project = await manager.create_project(title="Task Parent")
# Create tasks — some with project_id, some without
t1 = await manager.create_task(title="Task 1")
t1.project_id = project.id
await manager._store.save_task(t1)
t2 = await manager.create_task(title="Task 2")
t2.project_id = project.id
await manager._store.save_task(t2)
await manager.create_task(title="Unrelated Task")
tasks = await manager.get_project_tasks(project.id)
assert len(tasks) == 2
task_titles = {t.title for t in tasks}
assert "Task 1" in task_titles
assert "Task 2" in task_titles
@pytest.mark.asyncio
async def test_get_project_progress(self, manager):
"""Test progress returns correct counts with mixed statuses/types."""
project = await manager.create_project(title="Progress Test")
# Create tasks with different statuses and types
# 1: agent task, done
t1 = await manager.create_task(title="Agent Done")
t1.project_id = project.id
t1.task_type = "agent"
t1.status = TaskStatus.DONE
await manager._store.save_task(t1)
# 2: agent task, in_progress
t2 = await manager.create_task(title="Agent WIP")
t2.project_id = project.id
t2.task_type = "agent"
t2.status = TaskStatus.IN_PROGRESS
await manager._store.save_task(t2)
# 3: human task, inbox (pending)
t3 = await manager.create_task(title="Human Pending")
t3.project_id = project.id
t3.task_type = "human"
t3.status = TaskStatus.INBOX
await manager._store.save_task(t3)
# 4: human task, done
t4 = await manager.create_task(title="Human Done")
t4.project_id = project.id
t4.task_type = "human"
t4.status = TaskStatus.DONE
await manager._store.save_task(t4)
# 5: agent task, blocked
t5 = await manager.create_task(title="Agent Blocked")
t5.project_id = project.id
t5.task_type = "agent"
t5.status = TaskStatus.BLOCKED
await manager._store.save_task(t5)
progress = await manager.get_project_progress(project.id)
assert progress["total"] == 5
assert progress["completed"] == 2
assert progress["in_progress"] == 1
assert progress["blocked"] == 1
assert progress["human_pending"] == 1 # only t3 (human + not done)
assert progress["percent"] == 40.0 # 2/5 * 100
@pytest.mark.asyncio
async def test_get_project_progress_empty(self, manager):
"""Test progress for a project with no tasks."""
project = await manager.create_project(title="Empty Project")
progress = await manager.get_project_progress(project.id)
assert progress["total"] == 0
assert progress["completed"] == 0
assert progress["percent"] == 0.0
@pytest.mark.asyncio
async def test_delete_project(self, manager):
"""Test deleting a project also deletes its tasks."""
project = await manager.create_project(title="Delete Me")
# Add tasks to the project
t1 = await manager.create_task(title="Project Task 1")
t1.project_id = project.id
await manager._store.save_task(t1)
t2 = await manager.create_task(title="Project Task 2")
t2.project_id = project.id
await manager._store.save_task(t2)
# Also create a task NOT in this project
unrelated = await manager.create_task(title="Unrelated Task")
deleted = await manager.delete_project(project.id)
assert deleted is True
# Verify project gone
fetched = await manager.get_project(project.id)
assert fetched is None
# Verify project tasks deleted
assert await manager.get_task(t1.id) is None
assert await manager.get_task(t2.id) is None
# Verify unrelated task still exists
assert await manager.get_task(unrelated.id) is not None
@pytest.mark.asyncio
async def test_delete_project_not_found(self, manager):
"""Test deleting a non-existent project."""
deleted = await manager.delete_project("nonexistent-id")
assert deleted is False
# ============================================================================
# API Tests
# ============================================================================
class TestProjectAPI:
"""Tests for project REST endpoints."""
def test_create_project(self, client):
"""Test creating a project via API."""
response = client.post(
"/api/mission-control/projects",
json={
"title": "API Project",
"description": "Created via API",
"tags": ["api", "test"],
},
)
assert response.status_code == 200
data = response.json()
assert data["project"]["title"] == "API Project"
assert data["project"]["description"] == "Created via API"
assert data["project"]["tags"] == ["api", "test"]
assert data["project"]["status"] == "draft"
def test_create_project_validation(self, client):
"""Test that title is required and validated."""
response = client.post(
"/api/mission-control/projects",
json={"title": ""},
)
assert response.status_code == 422
def test_list_projects_empty(self, client):
"""Test listing projects when none exist."""
response = client.get("/api/mission-control/projects")
assert response.status_code == 200
data = response.json()
assert data["projects"] == []
assert data["count"] == 0
def test_list_projects(self, client):
"""Test listing projects."""
client.post(
"/api/mission-control/projects",
json={"title": "Project A"},
)
client.post(
"/api/mission-control/projects",
json={"title": "Project B"},
)
response = client.get("/api/mission-control/projects")
assert response.status_code == 200
assert response.json()["count"] == 2
def test_list_projects_by_status(self, client):
"""Test filtering projects by status."""
client.post(
"/api/mission-control/projects",
json={"title": "Draft"},
)
# Create another and approve it
create_resp = client.post(
"/api/mission-control/projects",
json={"title": "Approved"},
)
project_id = create_resp.json()["project"]["id"]
client.post(f"/api/mission-control/projects/{project_id}/approve")
response = client.get(
"/api/mission-control/projects",
params={"status": "draft"},
)
assert response.status_code == 200
assert response.json()["count"] == 1
assert response.json()["projects"][0]["title"] == "Draft"
def test_get_project(self, client):
"""Test getting a project with tasks and progress."""
# Create project
create_resp = client.post(
"/api/mission-control/projects",
json={"title": "Detail Project"},
)
project_id = create_resp.json()["project"]["id"]
response = client.get(f"/api/mission-control/projects/{project_id}")
assert response.status_code == 200
data = response.json()
assert data["project"]["title"] == "Detail Project"
assert "tasks" in data
assert "progress" in data
assert data["progress"]["total"] == 0
def test_get_project_not_found(self, client):
"""Test getting a non-existent project."""
response = client.get("/api/mission-control/projects/nonexistent")
assert response.status_code == 404
def test_update_project(self, client):
"""Test updating a project's fields."""
create_resp = client.post(
"/api/mission-control/projects",
json={"title": "Original Title"},
)
project_id = create_resp.json()["project"]["id"]
response = client.patch(
f"/api/mission-control/projects/{project_id}",
json={
"title": "Updated Title",
"description": "New description",
"tags": ["updated"],
},
)
assert response.status_code == 200
data = response.json()
assert data["project"]["title"] == "Updated Title"
assert data["project"]["description"] == "New description"
assert data["project"]["tags"] == ["updated"]
def test_update_project_status(self, client):
"""Test updating a project's status via PATCH."""
create_resp = client.post(
"/api/mission-control/projects",
json={"title": "Status Project"},
)
project_id = create_resp.json()["project"]["id"]
response = client.patch(
f"/api/mission-control/projects/{project_id}",
json={"status": "executing"},
)
assert response.status_code == 200
assert response.json()["project"]["status"] == "executing"
def test_update_project_not_found(self, client):
"""Test updating a non-existent project."""
response = client.patch(
"/api/mission-control/projects/nonexistent",
json={"title": "Nope"},
)
assert response.status_code == 404
def test_delete_project(self, client):
"""Test deleting a project via API."""
create_resp = client.post(
"/api/mission-control/projects",
json={"title": "Delete Me"},
)
project_id = create_resp.json()["project"]["id"]
response = client.delete(f"/api/mission-control/projects/{project_id}")
assert response.status_code == 200
# Verify deleted
get_resp = client.get(f"/api/mission-control/projects/{project_id}")
assert get_resp.status_code == 404
def test_delete_project_not_found(self, client):
"""Test deleting a non-existent project."""
response = client.delete("/api/mission-control/projects/nonexistent")
assert response.status_code == 404
def test_approve_project(self, client):
"""Test approving a project."""
create_resp = client.post(
"/api/mission-control/projects",
json={"title": "Approve Me"},
)
project_id = create_resp.json()["project"]["id"]
response = client.post(f"/api/mission-control/projects/{project_id}/approve")
assert response.status_code == 200
assert response.json()["project"]["status"] == "approved"
def test_approve_project_not_found(self, client):
"""Test approving a non-existent project."""
response = client.post("/api/mission-control/projects/nonexistent/approve")
assert response.status_code == 404
def test_pause_project(self, client):
"""Test pausing a project."""
create_resp = client.post(
"/api/mission-control/projects",
json={"title": "Pause Me"},
)
project_id = create_resp.json()["project"]["id"]
response = client.post(f"/api/mission-control/projects/{project_id}/pause")
assert response.status_code == 200
assert response.json()["project"]["status"] == "paused"
def test_pause_project_not_found(self, client):
"""Test pausing a non-existent project."""
response = client.post("/api/mission-control/projects/nonexistent/pause")
assert response.status_code == 404
def test_resume_project(self, client):
"""Test resuming a project."""
create_resp = client.post(
"/api/mission-control/projects",
json={"title": "Resume Me"},
)
project_id = create_resp.json()["project"]["id"]
# Pause first, then resume
client.post(f"/api/mission-control/projects/{project_id}/pause")
response = client.post(f"/api/mission-control/projects/{project_id}/resume")
assert response.status_code == 200
assert response.json()["project"]["status"] == "executing"
def test_resume_project_not_found(self, client):
"""Test resuming a non-existent project."""
response = client.post("/api/mission-control/projects/nonexistent/resume")
assert response.status_code == 404