"""Regress automatic decomposition against existing execution history.""" from __future__ import annotations from hermes_cli import kanban_db from hermes_cli import kanban_decompose from hermes_execution_regression_support import ExecutionSafetyTestCase class DecompositionSafetyTests(ExecutionSafetyTestCase): """Verify only unexecuted triage roots may decompose automatically.""" def test_automatic_decomposer_skips_executed_triage_task(self) -> None: task_id = self._route_executed_task_to_triage() completions = self._client( {"fanout": False, "title": "must not run", "body": "must not run"} ) before = len(kanban_db.list_tasks(self.connection)) outcome = kanban_decompose.decompose_task(task_id, automatic=True) self.assertFalse(outcome.ok) self.assertIn("execution history", outcome.reason) self.assertEqual(completions.calls, 0) self.assertEqual(self._status(task_id), "triage") self.assertEqual(len(kanban_db.list_tasks(self.connection)), before) def test_manual_decomposition_remains_available_after_execution(self) -> None: task_id = self._route_executed_task_to_triage() completions = self._client( { "fanout": False, "title": "operator-approved retry", "body": "Retain one bounded task.", "assignee": "default", } ) outcome = kanban_decompose.decompose_task(task_id, author="operator") self.assertTrue(outcome.ok) self.assertFalse(outcome.fanout) self.assertEqual(completions.calls, 1) self.assertEqual(self._status(task_id), "ready") def test_fresh_triage_task_still_auto_promotes(self) -> None: task_id = kanban_db.create_task( self.connection, title="fresh objective", triage=True, ) completions = self._client( { "fanout": False, "title": "specified objective", "body": "One bounded task.", "assignee": "default", } ) outcome = kanban_decompose.decompose_task(task_id, automatic=True) self.assertTrue(outcome.ok) self.assertEqual(completions.calls, 1) self.assertEqual(self._status(task_id), "ready") def test_execution_history_gained_during_single_llm_call_blocks_commit(self) -> None: task_id = kanban_db.create_task( self.connection, title="concurrent single objective", triage=True, ) completions = self._client( { "fanout": False, "title": "redundant rewrite", "body": "must not be committed", "assignee": "default", }, before_response=lambda: self._insert_run(task_id), ) outcome = kanban_decompose.decompose_task(task_id, automatic=True) self.assertFalse(outcome.ok) self.assertIn("gained execution history", outcome.reason) self.assertEqual(completions.calls, 1) self.assertEqual(self._status(task_id), "triage") def test_execution_history_gained_during_fanout_llm_call_blocks_commit(self) -> None: task_id = kanban_db.create_task( self.connection, title="concurrent triage objective", triage=True, ) completions = self._client( { "fanout": True, "tasks": [ { "title": "redundant child", "body": "must not be created", "assignee": "default", "parents": [], } ], }, before_response=lambda: self._insert_run(task_id), ) before = len(kanban_db.list_tasks(self.connection)) outcome = kanban_decompose.decompose_task(task_id, automatic=True) self.assertFalse(outcome.ok) self.assertIn("gained execution history", outcome.reason) self.assertEqual(completions.calls, 1) self.assertEqual(self._status(task_id), "triage") self.assertEqual(len(kanban_db.list_tasks(self.connection)), before)