111 lines
4.0 KiB
Python
111 lines
4.0 KiB
Python
|
|
"""Regression tests for Hermes Kanban blocked-task scheduling semantics."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import os
|
||
|
|
import tempfile
|
||
|
|
import unittest
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
|
||
|
|
_HERMES_HOME = tempfile.TemporaryDirectory(prefix="hermes-kanban-test-home-")
|
||
|
|
os.environ["HERMES_HOME"] = _HERMES_HOME.name
|
||
|
|
|
||
|
|
from hermes_cli import kanban_db # noqa: E402
|
||
|
|
|
||
|
|
|
||
|
|
class BlockedTaskSchedulingTests(unittest.TestCase):
|
||
|
|
"""Exercise the real upstream database API against isolated databases."""
|
||
|
|
|
||
|
|
def setUp(self) -> None:
|
||
|
|
self._database_dir = tempfile.TemporaryDirectory(
|
||
|
|
prefix="hermes-kanban-test-db-"
|
||
|
|
)
|
||
|
|
self.connection = kanban_db.connect(
|
||
|
|
Path(self._database_dir.name) / "kanban.db"
|
||
|
|
)
|
||
|
|
|
||
|
|
def tearDown(self) -> None:
|
||
|
|
self.connection.close()
|
||
|
|
self._database_dir.cleanup()
|
||
|
|
|
||
|
|
def create_task(self, title: str, **kwargs: object) -> str:
|
||
|
|
return kanban_db.create_task(self.connection, title=title, **kwargs)
|
||
|
|
|
||
|
|
def status(self, task_id: str) -> str:
|
||
|
|
task = kanban_db.get_task(self.connection, task_id)
|
||
|
|
self.assertIsNotNone(task)
|
||
|
|
return task.status
|
||
|
|
|
||
|
|
def test_initial_block_without_parents_is_sticky(self) -> None:
|
||
|
|
task_id = self.create_task("operator parked", initial_status="blocked")
|
||
|
|
|
||
|
|
self.assertEqual(kanban_db.recompute_ready(self.connection), 0)
|
||
|
|
self.assertEqual(self.status(task_id), "blocked")
|
||
|
|
|
||
|
|
def test_initial_block_with_complete_parents_is_sticky(self) -> None:
|
||
|
|
parent_id = self.create_task("completed prerequisite")
|
||
|
|
self.assertTrue(kanban_db.complete_task(self.connection, parent_id))
|
||
|
|
task_id = self.create_task(
|
||
|
|
"operator parked after prerequisite",
|
||
|
|
initial_status="blocked",
|
||
|
|
parents=[parent_id],
|
||
|
|
)
|
||
|
|
|
||
|
|
self.assertEqual(kanban_db.recompute_ready(self.connection), 0)
|
||
|
|
self.assertEqual(self.status(task_id), "blocked")
|
||
|
|
|
||
|
|
def test_initial_block_is_released_by_an_explicit_unblock(self) -> None:
|
||
|
|
task_id = self.create_task("operator parked", initial_status="blocked")
|
||
|
|
|
||
|
|
self.assertTrue(kanban_db.unblock_task(self.connection, task_id))
|
||
|
|
self.assertEqual(self.status(task_id), "ready")
|
||
|
|
|
||
|
|
def test_dependency_block_promotes_after_parent_completes(self) -> None:
|
||
|
|
parent_id = self.create_task("incomplete prerequisite")
|
||
|
|
task_id = self.create_task("dependency gated", parents=[parent_id])
|
||
|
|
self.assertEqual(self.status(task_id), "todo")
|
||
|
|
|
||
|
|
self.assertTrue(kanban_db.complete_task(self.connection, parent_id))
|
||
|
|
self.assertEqual(self.status(task_id), "ready")
|
||
|
|
|
||
|
|
def test_explicit_block_and_unblock_remain_sticky_and_reversible(self) -> None:
|
||
|
|
task_id = self.create_task("worker handoff")
|
||
|
|
|
||
|
|
self.assertTrue(
|
||
|
|
kanban_db.block_task(
|
||
|
|
self.connection,
|
||
|
|
task_id,
|
||
|
|
reason="human review required",
|
||
|
|
)
|
||
|
|
)
|
||
|
|
self.assertEqual(kanban_db.recompute_ready(self.connection), 0)
|
||
|
|
self.assertEqual(self.status(task_id), "blocked")
|
||
|
|
self.assertTrue(kanban_db.unblock_task(self.connection, task_id))
|
||
|
|
self.assertEqual(self.status(task_id), "ready")
|
||
|
|
|
||
|
|
def test_circuit_breaker_block_is_not_auto_promoted(self) -> None:
|
||
|
|
task_id = self.create_task("repeated worker failure")
|
||
|
|
self.assertIsNotNone(kanban_db.claim_task(self.connection, task_id))
|
||
|
|
|
||
|
|
# Upstream has no public circuit-breaker entry point; exercise the
|
||
|
|
# dispatcher helper that owns its persisted failure-limit semantics.
|
||
|
|
self.assertTrue(
|
||
|
|
kanban_db._record_spawn_failure(
|
||
|
|
self.connection,
|
||
|
|
task_id,
|
||
|
|
"worker failed",
|
||
|
|
failure_limit=1,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
self.assertEqual(self.status(task_id), "blocked")
|
||
|
|
self.assertEqual(
|
||
|
|
kanban_db.recompute_ready(self.connection, failure_limit=1),
|
||
|
|
0,
|
||
|
|
)
|
||
|
|
self.assertEqual(self.status(task_id), "blocked")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main(verbosity=2)
|