kin: KIN-048 Post-pipeline hook: автокоммит после успешного завершения задачи. git add -A && git commit -m 'kin: TASK_ID TITLE'. Срабатывает автоматически как rebuild-frontend.

This commit is contained in:
Gros Frumos 2026-03-16 06:59:46 +02:00
parent 8a6f280cbd
commit ae21e48b65
13 changed files with 1554 additions and 65 deletions

View file

@ -1,7 +1,8 @@
"""
Tests for KIN-012 auto mode features:
Tests for KIN-012/KIN-063 auto mode features:
- TestAutoApprove: pipeline auto-approves (status done) без ручного review
(KIN-063: auto_complete только если последний шаг tester или reviewer)
- TestAutoRerunOnPermissionDenied: runner делает retry при permission error,
останавливается после одного retry (лимит = 1)
- TestAutoFollowup: generate_followups вызывается сразу, без ожидания
@ -75,30 +76,30 @@ class TestAutoApprove:
@patch("agents.runner.run_hooks")
@patch("agents.runner.subprocess.run")
def test_auto_mode_sets_status_done(self, mock_run, mock_hooks, mock_followup, conn):
"""Auto-режим: статус задачи становится 'done', а не 'review'."""
"""Auto-complete режим: статус становится 'done', если последний шаг — tester."""
mock_run.return_value = _mock_success()
mock_hooks.return_value = []
mock_followup.return_value = {"created": [], "pending_actions": []}
models.update_project(conn, "vdol", execution_mode="auto")
steps = [{"role": "debugger", "brief": "find bug"}]
models.update_project(conn, "vdol", execution_mode="auto_complete")
steps = [{"role": "debugger", "brief": "find bug"}, {"role": "tester", "brief": "verify fix"}]
result = run_pipeline(conn, "VDOL-001", steps)
assert result["success"] is True
task = models.get_task(conn, "VDOL-001")
assert task["status"] == "done", "Auto-mode должен auto-approve: status=done"
assert task["status"] == "done", "Auto-complete должен auto-approve: status=done"
@patch("core.followup.generate_followups")
@patch("agents.runner.run_hooks")
@patch("agents.runner.subprocess.run")
def test_auto_mode_fires_task_auto_approved_hook(self, mock_run, mock_hooks, mock_followup, conn):
"""В auto-режиме срабатывает хук task_auto_approved."""
"""В auto_complete-режиме срабатывает хук task_auto_approved (если последний шаг — tester)."""
mock_run.return_value = _mock_success()
mock_hooks.return_value = []
mock_followup.return_value = {"created": [], "pending_actions": []}
models.update_project(conn, "vdol", execution_mode="auto")
steps = [{"role": "debugger", "brief": "find bug"}]
models.update_project(conn, "vdol", execution_mode="auto_complete")
steps = [{"role": "debugger", "brief": "find bug"}, {"role": "tester", "brief": "verify"}]
run_pipeline(conn, "VDOL-001", steps)
events = _get_hook_events(mock_hooks)
@ -140,20 +141,20 @@ class TestAutoApprove:
@patch("agents.runner.run_hooks")
@patch("agents.runner.subprocess.run")
def test_task_level_auto_overrides_project_review(self, mock_run, mock_hooks, mock_followup, conn):
"""Если у задачи execution_mode=auto, pipeline auto-approve, даже если проект в review."""
"""Если у задачи execution_mode=auto_complete, pipeline auto-approve, даже если проект в review."""
mock_run.return_value = _mock_success()
mock_hooks.return_value = []
mock_followup.return_value = {"created": [], "pending_actions": []}
# Проект в review, но задача — auto
models.update_task(conn, "VDOL-001", execution_mode="auto")
# Проект в review, но задача — auto_complete
models.update_task(conn, "VDOL-001", execution_mode="auto_complete")
steps = [{"role": "debugger", "brief": "find"}]
steps = [{"role": "debugger", "brief": "find"}, {"role": "reviewer", "brief": "approve"}]
result = run_pipeline(conn, "VDOL-001", steps)
assert result["success"] is True
task = models.get_task(conn, "VDOL-001")
assert task["status"] == "done", "Task-level auto должен override project review"
assert task["status"] == "done", "Task-level auto_complete должен override project review"
@patch("core.followup.generate_followups")
@patch("agents.runner.run_hooks")
@ -164,11 +165,11 @@ class TestAutoApprove:
mock_hooks.return_value = []
mock_followup.return_value = {"created": [], "pending_actions": []}
models.update_project(conn, "vdol", execution_mode="auto")
steps = [{"role": "debugger", "brief": "find"}]
models.update_project(conn, "vdol", execution_mode="auto_complete")
steps = [{"role": "debugger", "brief": "find"}, {"role": "tester", "brief": "test"}]
result = run_pipeline(conn, "VDOL-001", steps)
assert result.get("mode") == "auto"
assert result.get("mode") == "auto_complete"
# ---------------------------------------------------------------------------
@ -178,10 +179,12 @@ class TestAutoApprove:
class TestAutoRerunOnPermissionDenied:
"""Runner повторяет шаг при permission issues, останавливается по лимиту (1 retry)."""
@patch("agents.runner._run_autocommit")
@patch("agents.runner._run_learning_extraction")
@patch("core.followup.generate_followups")
@patch("agents.runner.run_hooks")
@patch("agents.runner.subprocess.run")
def test_auto_mode_retries_on_permission_error(self, mock_run, mock_hooks, mock_followup, conn):
def test_auto_mode_retries_on_permission_error(self, mock_run, mock_hooks, mock_followup, mock_learn, mock_autocommit, conn):
"""Auto-режим: при permission denied runner делает 1 retry с allow_write=True."""
mock_run.side_effect = [
_mock_permission_denied(), # 1-й вызов: permission error
@ -189,8 +192,9 @@ class TestAutoRerunOnPermissionDenied:
]
mock_hooks.return_value = []
mock_followup.return_value = {"created": [], "pending_actions": []}
mock_learn.return_value = {"added": 0, "skipped": 0}
models.update_project(conn, "vdol", execution_mode="auto")
models.update_project(conn, "vdol", execution_mode="auto_complete")
steps = [{"role": "debugger", "brief": "fix file"}]
result = run_pipeline(conn, "VDOL-001", steps)
@ -209,7 +213,7 @@ class TestAutoRerunOnPermissionDenied:
mock_hooks.return_value = []
mock_followup.return_value = {"created": [], "pending_actions": []}
models.update_project(conn, "vdol", execution_mode="auto")
models.update_project(conn, "vdol", execution_mode="auto_complete")
steps = [{"role": "debugger", "brief": "fix"}]
run_pipeline(conn, "VDOL-001", steps)
@ -229,7 +233,7 @@ class TestAutoRerunOnPermissionDenied:
mock_hooks.return_value = []
mock_followup.return_value = {"created": [], "pending_actions": []}
models.update_project(conn, "vdol", execution_mode="auto")
models.update_project(conn, "vdol", execution_mode="auto_complete")
steps = [{"role": "debugger", "brief": "fix"}]
run_pipeline(conn, "VDOL-001", steps)
@ -248,7 +252,7 @@ class TestAutoRerunOnPermissionDenied:
mock_hooks.return_value = []
mock_followup.return_value = {"created": [], "pending_actions": []}
models.update_project(conn, "vdol", execution_mode="auto")
models.update_project(conn, "vdol", execution_mode="auto_complete")
steps = [{"role": "debugger", "brief": "fix"}]
result = run_pipeline(conn, "VDOL-001", steps)
@ -257,10 +261,12 @@ class TestAutoRerunOnPermissionDenied:
task = models.get_task(conn, "VDOL-001")
assert task["status"] == "blocked"
@patch("agents.runner._run_autocommit")
@patch("agents.runner._run_learning_extraction")
@patch("core.followup.generate_followups")
@patch("agents.runner.run_hooks")
@patch("agents.runner.subprocess.run")
def test_subsequent_steps_use_allow_write_after_retry(self, mock_run, mock_hooks, mock_followup, conn):
def test_subsequent_steps_use_allow_write_after_retry(self, mock_run, mock_hooks, mock_followup, mock_learn, mock_autocommit, conn):
"""После успешного retry все следующие шаги тоже используют allow_write."""
mock_run.side_effect = [
_mock_permission_denied(), # Шаг 1: permission error
@ -269,8 +275,9 @@ class TestAutoRerunOnPermissionDenied:
]
mock_hooks.return_value = []
mock_followup.return_value = {"created": [], "pending_actions": []}
mock_learn.return_value = {"added": 0, "skipped": 0}
models.update_project(conn, "vdol", execution_mode="auto")
models.update_project(conn, "vdol", execution_mode="auto_complete")
steps = [
{"role": "debugger", "brief": "fix"},
{"role": "tester", "brief": "test"},
@ -293,7 +300,7 @@ class TestAutoRerunOnPermissionDenied:
mock_hooks.return_value = []
mock_followup.return_value = {"created": [], "pending_actions": []}
models.update_project(conn, "vdol", execution_mode="auto")
models.update_project(conn, "vdol", execution_mode="auto_complete")
steps = [{"role": "debugger", "brief": "fix"}]
result = run_pipeline(conn, "VDOL-001", steps)
@ -330,13 +337,13 @@ class TestAutoFollowup:
@patch("agents.runner.run_hooks")
@patch("agents.runner.subprocess.run")
def test_auto_followup_triggered_immediately(self, mock_run, mock_hooks, mock_followup, conn):
"""В auto-режиме generate_followups вызывается сразу после pipeline."""
"""В auto_complete-режиме generate_followups вызывается сразу после pipeline (последний шаг — tester)."""
mock_run.return_value = _mock_success()
mock_hooks.return_value = []
mock_followup.return_value = {"created": [], "pending_actions": []}
models.update_project(conn, "vdol", execution_mode="auto")
steps = [{"role": "debugger", "brief": "find"}]
models.update_project(conn, "vdol", execution_mode="auto_complete")
steps = [{"role": "debugger", "brief": "find"}, {"role": "tester", "brief": "test"}]
result = run_pipeline(conn, "VDOL-001", steps)
assert result["success"] is True
@ -357,8 +364,8 @@ class TestAutoFollowup:
mock_followup.return_value = {"created": [], "pending_actions": pending}
mock_resolve.return_value = [{"resolved": "rerun", "result": {}}]
models.update_project(conn, "vdol", execution_mode="auto")
steps = [{"role": "debugger", "brief": "find"}]
models.update_project(conn, "vdol", execution_mode="auto_complete")
steps = [{"role": "debugger", "brief": "find"}, {"role": "tester", "brief": "test"}]
run_pipeline(conn, "VDOL-001", steps)
mock_resolve.assert_called_once_with(conn, "VDOL-001", pending)
@ -392,10 +399,10 @@ class TestAutoFollowup:
mock_hooks.return_value = []
mock_followup.return_value = {"created": [], "pending_actions": []}
models.update_project(conn, "vdol", execution_mode="auto")
models.update_project(conn, "vdol", execution_mode="auto_complete")
models.update_task(conn, "VDOL-001", brief={"source": "followup:VDOL-000"})
steps = [{"role": "debugger", "brief": "find"}]
steps = [{"role": "debugger", "brief": "find"}, {"role": "tester", "brief": "test"}]
result = run_pipeline(conn, "VDOL-001", steps)
assert result["success"] is True
@ -412,8 +419,8 @@ class TestAutoFollowup:
mock_hooks.return_value = []
mock_followup.side_effect = Exception("followup PM crashed")
models.update_project(conn, "vdol", execution_mode="auto")
steps = [{"role": "debugger", "brief": "find"}]
models.update_project(conn, "vdol", execution_mode="auto_complete")
steps = [{"role": "debugger", "brief": "find"}, {"role": "tester", "brief": "test"}]
result = run_pipeline(conn, "VDOL-001", steps)
assert result["success"] is True # Pipeline succeeded, followup failure absorbed
@ -431,8 +438,8 @@ class TestAutoFollowup:
mock_followup.return_value = {"created": [], "pending_actions": []}
mock_resolve.return_value = []
models.update_project(conn, "vdol", execution_mode="auto")
steps = [{"role": "debugger", "brief": "find"}]
models.update_project(conn, "vdol", execution_mode="auto_complete")
steps = [{"role": "debugger", "brief": "find"}, {"role": "tester", "brief": "test"}]
run_pipeline(conn, "VDOL-001", steps)
mock_resolve.assert_not_called()