test_task_manager.py•16.8 kB
# Aidderall MCP Server - Hierarchical task management for AI assistants
# Copyright (C) 2024 Briam R. <briamr@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import pytest
from src.models import MainTask, SubTask, TaskStatus
from src.task_manager import TaskManager
class TestTaskManager:
def setup_method(self):
self.manager = TaskManager()
def test_zen_state(self):
assert self.manager.is_zen_state
assert self.manager.current_task is None
assert len(self.manager.global_tasks) == 0
def test_create_new_task(self):
task = self.manager.create_new_task("First Task", "Body of first task")
assert isinstance(task, MainTask)
assert task.title == "First Task"
assert task.body == "Body of first task"
assert task.status == TaskStatus.CURRENT
assert not self.manager.is_zen_state
assert self.manager.current_task == task
assert len(self.manager.global_tasks) == 1
def test_extend_current_task_in_zen_state(self):
with pytest.raises(ValueError, match="Cannot extend task in zen state"):
self.manager.extend_current_task("Sub Task", "Sub body")
def test_extend_current_task(self):
main_task = self.manager.create_new_task("Main Task", "Main body")
sub_task = self.manager.extend_current_task("Sub Task", "Sub body")
assert isinstance(sub_task, SubTask)
assert sub_task.title == "Sub Task"
assert sub_task.status == TaskStatus.CURRENT
assert main_task.status == TaskStatus.PENDING
assert self.manager.current_task == sub_task
assert len(main_task.sub_tasks) == 1
def test_complete_current_task_subtask(self):
main_task = self.manager.create_new_task("Main Task", "Main body")
sub_task1 = self.manager.extend_current_task("Sub Task 1", "Sub body 1")
sub_task2 = self.manager.extend_current_task("Sub Task 2", "Sub body 2")
completed = self.manager.complete_current_task()
assert completed == sub_task2
assert completed.status == TaskStatus.COMPLETED
assert completed.completed_at is not None
assert self.manager.current_task == sub_task1
assert sub_task1.status == TaskStatus.CURRENT
assert len(self.manager.completed_tasks) == 1
def test_complete_current_task_main_task(self):
task = self.manager.create_new_task("Only Task", "Body")
completed = self.manager.complete_current_task()
assert completed == task
# Task should stay in structure but be marked completed
assert self.manager.is_zen_state # Single completed task = zen
assert len(self.manager.global_tasks) == 1
assert self.manager.global_tasks[0] == task
assert task.status == TaskStatus.COMPLETED
assert (
self.manager.current_task == task
) # Still returns the task even if completed
assert len(self.manager.completed_tasks) == 1
def test_update_current_task(self):
task = self.manager.create_new_task("Task", "Original body")
updated = self.manager.update_current_task("Updated body")
assert updated == task
assert task.body == "Updated body"
assert task.title == "Task"
def test_update_current_task_no_current(self):
with pytest.raises(ValueError, match="No current task to update"):
self.manager.update_current_task("New body")
def test_get_siblings_to_left(self):
main_task = self.manager.create_new_task("Main", "Main body")
sub1 = self.manager.extend_current_task("Sub 1", "Body 1")
sub2 = self.manager.extend_current_task("Sub 2", "Body 2")
sub3 = self.manager.extend_current_task("Sub 3", "Body 3")
siblings = self.manager.get_siblings_to_left()
assert len(siblings) == 2
assert siblings[0] == sub1
assert siblings[1] == sub2
def test_get_breadcrumb_trail(self):
main1 = self.manager.create_new_task("Main 1", "Body 1")
sub1 = self.manager.extend_current_task("Sub 1", "Sub body 1")
main2 = self.manager.create_new_task("Main 2", "Body 2")
sub2 = self.manager.extend_current_task("Sub 2", "Sub body 2")
trail = self.manager.get_breadcrumb_trail()
assert len(trail) == 2
assert trail[0] == main1
assert trail[1] == main2
def test_get_big_picture(self):
picture = self.manager.get_big_picture()
assert picture == "No tasks (zen state)"
main1 = self.manager.create_new_task("Main Task", "Body")
picture = self.manager.get_big_picture()
assert "Main Task (current) <-- YOU ARE HERE" in picture
sub1 = self.manager.extend_current_task("Sub Task", "Sub body")
picture = self.manager.get_big_picture()
assert "Main Task (pending)" in picture
assert " Sub Task (current) <-- YOU ARE HERE" in picture
def test_get_completed_tasks(self):
task1 = self.manager.create_new_task("Task 1", "Body 1")
self.manager.complete_current_task()
task2 = self.manager.create_new_task("Task 2", "Body 2")
self.manager.complete_current_task()
chrono = self.manager.get_completed_tasks("chronological")
assert len(chrono) == 2
assert chrono[0] == task1
assert chrono[1] == task2
logical = self.manager.get_completed_tasks("logical")
assert len(logical) == 2
assert logical[0] == task2
assert logical[1] == task1
with pytest.raises(ValueError):
self.manager.get_completed_tasks("invalid")
def test_get_stack_overview(self):
overview = self.manager.get_stack_overview()
assert overview["zen_state"] is True
assert overview["stack_depth"] == 0
assert overview["current_task_id"] is None
main = self.manager.create_new_task("Main", "Body")
sub = self.manager.extend_current_task("Sub", "Sub body")
overview = self.manager.get_stack_overview()
assert overview["zen_state"] is False
assert overview["stack_depth"] == 2
assert overview["current_task_id"] == sub.id
assert len(overview["global_tasks"]) == 1
assert len(overview["global_tasks"][0]["sub_tasks"]) == 1
def test_peek_context_subtask(self):
main = self.manager.create_new_task("Main", "Body")
sub1 = self.manager.extend_current_task("Sub 1", "Body 1")
sub2 = self.manager.extend_current_task("Sub 2", "Body 2")
parent, immediate = self.manager.peek_context()
assert parent == main
assert immediate == sub1
def test_peek_context_main_task(self):
main1 = self.manager.create_new_task("Main 1", "Body 1")
sub1 = self.manager.extend_current_task("Sub 1", "Sub body")
main2 = self.manager.create_new_task("Main 2", "Body 2")
parent, immediate = self.manager.peek_context()
assert parent == main1
assert immediate == sub1
def test_list_siblings(self):
main = self.manager.create_new_task("Main", "Body")
siblings = self.manager.list_siblings()
assert len(siblings) == 0
sub1 = self.manager.extend_current_task("Sub 1", "Body 1")
sub2 = self.manager.extend_current_task("Sub 2", "Body 2")
siblings = self.manager.list_siblings()
assert len(siblings) == 1
assert siblings[0] == sub1
def test_complex_scenario(self):
main1 = self.manager.create_new_task("Design Feature", "Design specs")
sub1_1 = self.manager.extend_current_task("Research", "Research notes")
sub1_2 = self.manager.extend_current_task("Interview", "Interview results")
self.manager.complete_current_task()
assert self.manager.current_task == sub1_1
self.manager.complete_current_task()
assert self.manager.current_task == main1
sub1_3 = self.manager.extend_current_task("Specification", "Spec doc")
self.manager.complete_current_task()
self.manager.complete_current_task()
# Tasks should stay in structure
assert self.manager.is_zen_state # All tasks completed = zen
assert len(self.manager.global_tasks) == 1
assert main1.status == TaskStatus.COMPLETED
assert len(self.manager.completed_tasks) == 4
def test_switch_focus_basic(self):
# Create multiple tasks
main1 = self.manager.create_new_task("Main 1", "Body 1")
sub1 = self.manager.extend_current_task("Sub 1", "Sub body 1")
main2 = self.manager.create_new_task("Main 2", "Body 2")
# Current focus should be main2
assert self.manager.current_task == main2
assert main2.status == TaskStatus.CURRENT
assert sub1.status == TaskStatus.PENDING
assert main1.status == TaskStatus.PENDING
# Switch to sub1
result = self.manager.switch_focus(sub1.id)
assert result["success"] is True
assert self.manager.current_task == sub1
assert sub1.status == TaskStatus.CURRENT
assert main2.status == TaskStatus.PENDING
# Switch to main1
result = self.manager.switch_focus(main1.id)
assert result["success"] is True
assert self.manager.current_task == main1
assert main1.status == TaskStatus.CURRENT
assert sub1.status == TaskStatus.PENDING
def test_switch_focus_same_task(self):
task = self.manager.create_new_task("Task", "Body")
result = self.manager.switch_focus(task.id)
assert result["success"] is True
assert result["message"] == "Task is already the current focus"
def test_switch_focus_invalid_task(self):
with pytest.raises(ValueError, match="Task with ID 'invalid-id' not found"):
self.manager.switch_focus("invalid-id")
def test_switch_focus_completed_task(self):
task = self.manager.create_new_task("Task", "Body")
task_id = task.id
self.manager.complete_current_task()
with pytest.raises(ValueError, match="not found or already completed"):
self.manager.switch_focus(task_id)
def test_switch_focus_complex_scenario(self):
# Create a complex task structure
main1 = self.manager.create_new_task("Project A", "Project A details")
sub1_1 = self.manager.extend_current_task("Research", "Research notes")
sub1_2 = self.manager.extend_current_task("Analysis", "Analysis data")
main2 = self.manager.create_new_task("Urgent Bug", "Bug details")
sub2_1 = self.manager.extend_current_task("Debug", "Debug info")
# Current focus is sub2_1
assert self.manager.current_task == sub2_1
# Jump back to sub1_2 without completing anything
result = self.manager.switch_focus(sub1_2.id)
assert self.manager.current_task == sub1_2
assert "Analysis" in result["new_focus_path"]
# All tasks should still be pending except the current one
assert sub1_2.status == TaskStatus.CURRENT
assert sub1_1.status == TaskStatus.PENDING
assert main1.status == TaskStatus.PENDING
assert sub2_1.status == TaskStatus.PENDING
assert main2.status == TaskStatus.PENDING
def test_complete_task_stays_in_structure(self):
# Create tasks
main = self.manager.create_new_task("Main Task", "Body")
sub1 = self.manager.extend_current_task("Sub 1", "Sub body 1")
sub2 = self.manager.extend_current_task("Sub 2", "Sub body 2")
# Complete sub2
completed = self.manager.complete_current_task()
assert completed == sub2
# Task should still be in structure
assert len(main.sub_tasks) == 2
assert main.sub_tasks[1] == sub2
assert sub2.status == TaskStatus.COMPLETED
# Current should be sub1
assert self.manager.current_task == sub1
assert sub1.status == TaskStatus.CURRENT
# Task should also be in completed_tasks
assert len(self.manager.completed_tasks) == 1
assert self.manager.completed_tasks[0] == sub2
def test_remove_task_basic(self):
# Create tasks
main1 = self.manager.create_new_task("Main 1", "Body 1")
sub1 = self.manager.extend_current_task("Sub 1", "Sub body 1")
main2 = self.manager.create_new_task("Main 2", "Body 2")
# Remove sub1
result = self.manager.remove_task(sub1.id)
assert result["success"] is True
assert "Sub 1" in result["message"]
# Sub1 should be gone from structure
assert len(main1.sub_tasks) == 0
# Global tasks should still have both main tasks
assert len(self.manager.global_tasks) == 2
def test_remove_main_task_with_children(self):
# Create tasks
main1 = self.manager.create_new_task("Main 1", "Body 1")
sub1 = self.manager.extend_current_task("Sub 1", "Sub body 1")
sub2 = self.manager.extend_current_task("Sub 2", "Sub body 2")
main2 = self.manager.create_new_task("Main 2", "Body 2")
# Remove main1 (should remove it entirely with children)
result = self.manager.remove_task(main1.id)
assert result["success"] is True
# Only main2 should remain
assert len(self.manager.global_tasks) == 1
assert self.manager.global_tasks[0] == main2
def test_remove_completed_task_stays_in_history(self):
# Create and complete a task
task = self.manager.create_new_task("Task", "Body")
self.manager.complete_current_task()
# Task should be in structure and completed_tasks
assert len(self.manager.global_tasks) == 1
assert len(self.manager.completed_tasks) == 1
# Remove it from structure
result = self.manager.remove_task(task.id)
assert result["was_completed"] is True
# Should be gone from structure but still in completed_tasks
assert len(self.manager.global_tasks) == 0
assert len(self.manager.completed_tasks) == 1
assert self.manager.completed_tasks[0] == task
def test_remove_nonexistent_task(self):
with pytest.raises(ValueError, match="not found in structure"):
self.manager.remove_task("invalid-id")
def test_focus_navigation_with_completed_tasks(self):
# Create structure
main = self.manager.create_new_task("Main", "Body")
sub1 = self.manager.extend_current_task("Sub 1", "Body 1")
sub2 = self.manager.extend_current_task("Sub 2", "Body 2")
sub3 = self.manager.extend_current_task("Sub 3", "Body 3")
# Complete sub3, focus should go to sub2
self.manager.complete_current_task()
assert self.manager.current_task == sub2
# Complete sub2, focus should go to sub1
self.manager.complete_current_task()
assert self.manager.current_task == sub1
# All tasks should still be in structure
assert len(main.sub_tasks) == 3
assert sub3.status == TaskStatus.COMPLETED
assert sub2.status == TaskStatus.COMPLETED
assert sub1.status == TaskStatus.CURRENT
def test_zen_state_with_completed_tasks(self):
# Empty = zen
assert self.manager.is_zen_state
# Create tasks
main = self.manager.create_new_task("Main", "Body")
sub = self.manager.extend_current_task("Sub", "Sub body")
# With incomplete tasks = not zen
assert not self.manager.is_zen_state
# Complete sub
self.manager.complete_current_task()
assert not self.manager.is_zen_state # Main still incomplete
# Complete main
self.manager.complete_current_task()
assert self.manager.is_zen_state # All completed = zen
# Tasks still in structure
assert len(self.manager.global_tasks) == 1
assert len(main.sub_tasks) == 1
# Remove a task
self.manager.remove_task(sub.id)
assert self.manager.is_zen_state # Still zen (all remaining completed)
# Remove main task
self.manager.remove_task(main.id)
assert self.manager.is_zen_state # Empty = zen