Files
pocketpaw/tests/test_skip_task.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

266 lines
9.3 KiB
Python

# Tests for SKIPPED task status — scheduler, manager progress, API
# Created: 2026-02-12
#
# Covers:
# - SKIPPED status unblocks dependent tasks (same as DONE)
# - SKIPPED tasks count in project completion check
# - Manager progress includes skipped in percent numerator
# - API skip endpoint sets status, cascades, returns progress
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
import pytest
from pocketpaw.deep_work.models import Project, ProjectStatus
from pocketpaw.deep_work.scheduler import DependencyScheduler
from pocketpaw.mission_control import (
FileMissionControlStore,
MissionControlManager,
reset_mission_control_manager,
reset_mission_control_store,
)
from pocketpaw.mission_control.models import Task, TaskStatus
# ============================================================================
# Fixtures — Scheduler (mocked)
# ============================================================================
@pytest.fixture
def mock_manager():
"""Mock MissionControlManager for scheduler tests."""
manager = AsyncMock()
manager.list_tasks = AsyncMock(return_value=[])
manager.get_project_tasks = AsyncMock(return_value=[])
manager.get_task = AsyncMock(return_value=None)
manager.get_project = AsyncMock(return_value=None)
manager.update_project = AsyncMock()
return manager
@pytest.fixture
def mock_executor():
"""Mock MCTaskExecutor."""
executor = AsyncMock()
executor.execute_task_background = AsyncMock()
executor.is_task_running = MagicMock(return_value=False)
return executor
@pytest.fixture
def mock_human_router():
"""Mock HumanTaskRouter."""
router = AsyncMock()
router.notify_human_task = AsyncMock()
router.notify_review_task = AsyncMock()
return router
@pytest.fixture
def scheduler(mock_manager, mock_executor, mock_human_router):
"""DependencyScheduler with mocked deps."""
return DependencyScheduler(mock_manager, mock_executor, mock_human_router)
def _make_task(
task_id: str,
status: TaskStatus = TaskStatus.INBOX,
project_id: str = "proj-1",
blocked_by: list[str] | None = None,
task_type: str = "agent",
assignee_ids: list[str] | None = None,
title: str = "",
) -> Task:
"""Helper to create a Task with specific fields."""
return Task(
id=task_id,
title=title or f"Task {task_id}",
status=status,
project_id=project_id,
blocked_by=blocked_by or [],
task_type=task_type,
assignee_ids=assignee_ids or [],
)
# ============================================================================
# Scheduler: SKIPPED unblocks dependents
# ============================================================================
class TestSkippedUnblocksDependents:
async def test_skipped_blocker_unblocks_dependent(self, scheduler, mock_manager):
"""A SKIPPED blocker should satisfy the blocked_by check."""
tasks = [
_make_task("t1", status=TaskStatus.SKIPPED),
_make_task("t2", status=TaskStatus.INBOX, blocked_by=["t1"]),
]
mock_manager.get_project_tasks.return_value = tasks
ready = await scheduler.get_ready_tasks("proj-1")
assert len(ready) == 1
assert ready[0].id == "t2"
async def test_mixed_done_and_skipped_unblocks(self, scheduler, mock_manager):
"""When blockers are a mix of DONE and SKIPPED, dependents should be ready."""
tasks = [
_make_task("t1", status=TaskStatus.DONE),
_make_task("t2", status=TaskStatus.SKIPPED),
_make_task("t3", status=TaskStatus.INBOX, blocked_by=["t1", "t2"]),
]
mock_manager.get_project_tasks.return_value = tasks
ready = await scheduler.get_ready_tasks("proj-1")
assert len(ready) == 1
assert ready[0].id == "t3"
async def test_partial_skip_partial_pending_blocks(self, scheduler, mock_manager):
"""If one blocker is SKIPPED but another is IN_PROGRESS, task is NOT ready."""
tasks = [
_make_task("t1", status=TaskStatus.SKIPPED),
_make_task("t2", status=TaskStatus.IN_PROGRESS),
_make_task("t3", status=TaskStatus.INBOX, blocked_by=["t1", "t2"]),
]
mock_manager.get_project_tasks.return_value = tasks
ready = await scheduler.get_ready_tasks("proj-1")
assert len(ready) == 0
# ============================================================================
# Scheduler: SKIPPED counts toward project completion
# ============================================================================
class TestSkippedProjectCompletion:
async def test_all_skipped_completes_project(self, scheduler, mock_manager):
"""Project with all tasks SKIPPED should be marked COMPLETED."""
project = Project(id="proj-1", title="All Skipped", status=ProjectStatus.EXECUTING)
tasks = [
_make_task("t1", status=TaskStatus.SKIPPED),
_make_task("t2", status=TaskStatus.SKIPPED),
]
mock_manager.get_task.return_value = tasks[0]
mock_manager.get_project_tasks.return_value = tasks
mock_manager.get_project.return_value = project
await scheduler.on_task_completed("t1")
mock_manager.update_project.assert_awaited_once()
updated = mock_manager.update_project.call_args[0][0]
assert updated.status == ProjectStatus.COMPLETED
async def test_mixed_done_skipped_completes_project(self, scheduler, mock_manager):
"""Project with DONE + SKIPPED tasks should be marked COMPLETED."""
project = Project(id="proj-1", title="Mixed", status=ProjectStatus.EXECUTING)
tasks = [
_make_task("t1", status=TaskStatus.DONE),
_make_task("t2", status=TaskStatus.SKIPPED),
]
mock_manager.get_task.return_value = tasks[0]
mock_manager.get_project_tasks.return_value = tasks
mock_manager.get_project.return_value = project
await scheduler.on_task_completed("t1")
mock_manager.update_project.assert_awaited_once()
updated = mock_manager.update_project.call_args[0][0]
assert updated.status == ProjectStatus.COMPLETED
# ============================================================================
# Manager: Progress includes skipped
# ============================================================================
# Fixtures for real store/manager
@pytest.fixture
def temp_store_path():
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
@pytest.fixture
def real_manager(temp_store_path):
"""Create a real manager with file store for progress tests."""
reset_mission_control_store()
reset_mission_control_manager()
store = FileMissionControlStore(temp_store_path)
return MissionControlManager(store)
class TestProgressWithSkipped:
@pytest.mark.asyncio
async def test_skipped_in_progress_count(self, real_manager):
"""Skipped tasks appear in progress.skipped and contribute to percent."""
project = await real_manager.create_project(title="Progress Test")
# 1: done agent
t1 = await real_manager.create_task(title="Done Task")
t1.project_id = project.id
t1.status = TaskStatus.DONE
await real_manager._store.save_task(t1)
# 2: skipped agent
t2 = await real_manager.create_task(title="Skipped Task")
t2.project_id = project.id
t2.status = TaskStatus.SKIPPED
await real_manager._store.save_task(t2)
# 3: inbox agent
t3 = await real_manager.create_task(title="Pending Task")
t3.project_id = project.id
t3.status = TaskStatus.INBOX
await real_manager._store.save_task(t3)
progress = await real_manager.get_project_progress(project.id)
assert progress["total"] == 3
assert progress["completed"] == 1
assert progress["skipped"] == 1
assert progress["percent"] == pytest.approx(66.7, abs=0.1) # (1+1)/3 * 100
@pytest.mark.asyncio
async def test_skipped_human_not_in_pending(self, real_manager):
"""A skipped human task should NOT count in human_pending."""
project = await real_manager.create_project(title="Human Skip Test")
t1 = await real_manager.create_task(title="Human Skipped")
t1.project_id = project.id
t1.task_type = "human"
t1.status = TaskStatus.SKIPPED
await real_manager._store.save_task(t1)
progress = await real_manager.get_project_progress(project.id)
assert progress["human_pending"] == 0
assert progress["skipped"] == 1
# ============================================================================
# TaskStatus enum: SKIPPED value
# ============================================================================
class TestSkippedEnum:
def test_skipped_status_exists(self):
"""TaskStatus.SKIPPED should have value 'skipped'."""
assert TaskStatus.SKIPPED.value == "skipped"
def test_skipped_round_trip(self):
"""Task with SKIPPED status should serialize and deserialize correctly."""
task = Task(id="skip-test", title="Skip Me", status=TaskStatus.SKIPPED)
data = task.to_dict()
assert data["status"] == "skipped"
restored = Task.from_dict(data)
assert restored.status == TaskStatus.SKIPPED