nightshift/tests/test_tasks.py
K. Hodges c1baf9b7d8 Implement NightShift MVP phases 1-6
Includes starter project generation, validation for configs/tasks/commands, artifact snapshot writing, structured stage results, command output capture, devlogs for phases 1-6, and unit coverage for the implemented MVP layers.
2026-05-17 00:17:13 -07:00

113 lines
2.9 KiB
Python

from pathlib import Path
import tempfile
import unittest
from nightshift.errors import TaskError
from nightshift.tasks import (
parse_task_file,
parse_tasks,
select_next_incomplete_task,
select_task_by_id,
)
TASKS_MD = """# Tasks
- [x] TASK-001: Completed task
Description:
Already done.
Acceptance Criteria:
- It is complete
- [ ] TASK-002: Add artifact directory creation
Description:
Create per-run and per-task artifact directories.
Dependencies:
- TASK-001
Acceptance Criteria:
- Creates `.nightshift/runs/<timestamp>/`
- Creates task-specific folder
- Writes task snapshot
"""
class TaskParserTests(unittest.TestCase):
def test_parse_documented_task_format(self) -> None:
tasks = parse_tasks(TASKS_MD)
self.assertEqual(len(tasks), 2)
self.assertEqual(tasks[1].id, "TASK-002")
self.assertEqual(tasks[1].title, "Add artifact directory creation")
self.assertFalse(tasks[1].completed)
self.assertEqual(
tasks[1].description,
"Create per-run and per-task artifact directories.",
)
self.assertEqual(tasks[1].dependencies, ("TASK-001",))
self.assertEqual(len(tasks[1].acceptance_criteria), 3)
self.assertIn("TASK-002", tasks[1].raw_markdown)
def test_select_next_incomplete_task(self) -> None:
tasks = parse_tasks(TASKS_MD)
selected = select_next_incomplete_task(tasks)
self.assertEqual(selected.id, "TASK-002")
def test_select_task_by_id(self) -> None:
tasks = parse_tasks(TASKS_MD)
selected = select_task_by_id(tasks, "TASK-001")
self.assertTrue(selected.completed)
def test_select_task_by_id_reports_available_tasks(self) -> None:
tasks = parse_tasks(TASKS_MD)
with self.assertRaisesRegex(TaskError, "Available tasks: TASK-001, TASK-002"):
select_task_by_id(tasks, "TASK-999")
def test_parse_task_file_rejects_path_traversal(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
with self.assertRaisesRegex(TaskError, "outside project root"):
parse_task_file(root, "../tasks.md")
def test_malformed_task_header_has_useful_error(self) -> None:
markdown = """# Tasks
- [ ] Add YAML config loading
Acceptance Criteria:
- Loads config
"""
with self.assertRaisesRegex(TaskError, "malformed task header"):
parse_tasks(markdown)
def test_missing_acceptance_criteria_fails(self) -> None:
markdown = """# Tasks
- [ ] TASK-001: Missing criteria
Description:
No acceptance criteria.
"""
with self.assertRaisesRegex(TaskError, "missing Acceptance Criteria"):
parse_tasks(markdown)
def test_no_tasks_fails(self) -> None:
with self.assertRaisesRegex(TaskError, "no tasks found"):
parse_tasks("# Tasks\n\nNothing here.\n")
if __name__ == "__main__":
unittest.main()