kin: KIN-080 Разобраться с KIN-FIX-003 и KIN-FIX-004, одна из задач уже выполнена, вторая берется в работу (руками завершаю) но в задаче не меняется текущий статус

This commit is contained in:
Gros Frumos 2026-03-16 17:30:31 +02:00
parent bfc8f1c0bb
commit c67fa379b3
2 changed files with 125 additions and 5 deletions

View file

@ -76,7 +76,7 @@ class ClaudeAuthError(Exception):
def check_claude_auth(timeout: int = 10) -> None: def check_claude_auth(timeout: int = 10) -> None:
"""Check that claude CLI is authenticated before running a pipeline. """Check that claude CLI is authenticated before running a pipeline.
Runs: claude -p 'ok' --output-format json --no-verbose with timeout. Runs: claude -p 'ok' --output-format json with timeout.
Returns None if auth is confirmed. Returns None if auth is confirmed.
Raises ClaudeAuthError if: Raises ClaudeAuthError if:
- claude CLI not found in PATH (FileNotFoundError) - claude CLI not found in PATH (FileNotFoundError)
@ -89,7 +89,7 @@ def check_claude_auth(timeout: int = 10) -> None:
env = _build_claude_env() env = _build_claude_env()
try: try:
proc = subprocess.run( proc = subprocess.run(
[claude_cmd, "-p", "ok", "--output-format", "json", "--no-verbose"], [claude_cmd, "-p", "ok", "--output-format", "json"],
capture_output=True, capture_output=True,
text=True, text=True,
timeout=timeout, timeout=timeout,
@ -1084,7 +1084,13 @@ def run_pipeline(
last_role = steps[-1].get("role", "") if steps else "" last_role = steps[-1].get("role", "") if steps else ""
auto_eligible = last_role in {"tester", "reviewer"} auto_eligible = last_role in {"tester", "reviewer"}
if mode == "auto_complete" and auto_eligible: # Guard: re-fetch current status — user may have manually changed it while pipeline ran
current_task = models.get_task(conn, task_id)
current_status = current_task.get("status") if current_task else None
if current_status in ("done", "cancelled"):
pass # User finished manually — don't overwrite
elif mode == "auto_complete" and auto_eligible:
# Auto-complete mode: last step is tester/reviewer — skip review, approve immediately # Auto-complete mode: last step is tester/reviewer — skip review, approve immediately
models.update_task(conn, task_id, status="done") models.update_task(conn, task_id, status="done")
try: try:
@ -1114,8 +1120,8 @@ def run_pipeline(
except Exception: except Exception:
pass pass
else: else:
# Review mode: wait for manual approval # Review mode: wait for manual approval (don't overwrite execution_mode)
models.update_task(conn, task_id, status="review", execution_mode="review") models.update_task(conn, task_id, status="review")
# Run post-pipeline hooks (failures don't affect pipeline status) # Run post-pipeline hooks (failures don't affect pipeline status)
try: try:

View file

@ -394,6 +394,120 @@ class TestAutoMode:
mock_resolve.assert_called_once_with(conn, "VDOL-001", pending) mock_resolve.assert_called_once_with(conn, "VDOL-001", pending)
# ---------------------------------------------------------------------------
# KIN-080: Guard — не перезаписывать статус, если пользователь изменил вручную
# ---------------------------------------------------------------------------
class TestPipelineStatusGuard:
"""Тесты guard-check: pipeline не должен перезаписывать статус задачи,
если пользователь вручную изменил его на 'done' или 'cancelled' пока
pipeline выполнялся."""
@patch("agents.runner._run_autocommit")
@patch("agents.runner._run_learning_extraction")
@patch("agents.runner._get_changed_files")
@patch("agents.runner.run_hooks")
@patch("agents.runner.subprocess.run")
def test_pipeline_preserves_done_status_set_during_execution(
self, mock_run, mock_hooks, mock_get_files, mock_learn, mock_autocommit, conn
):
"""Guard: если пользователь вручную поставил 'done' пока pipeline работал —
итоговый статус должен остаться 'done', а не перезаписаться в 'review'."""
def side_effect(*args, **kwargs):
# Имитируем ручную смену статуса во время выполнения агента
models.update_task(conn, "VDOL-001", status="done")
return _mock_claude_success({"result": "done"})
mock_run.side_effect = side_effect
mock_hooks.return_value = []
mock_get_files.return_value = []
mock_learn.return_value = {"added": 0, "skipped": 0}
steps = [{"role": "debugger", "brief": "find"}]
result = run_pipeline(conn, "VDOL-001", steps)
assert result["success"] is True
task = models.get_task(conn, "VDOL-001")
assert task["status"] == "done" # guard НЕ перезаписал в "review"
@patch("agents.runner._run_autocommit")
@patch("agents.runner._run_learning_extraction")
@patch("agents.runner._get_changed_files")
@patch("agents.runner.run_hooks")
@patch("agents.runner.subprocess.run")
def test_pipeline_preserves_cancelled_status_set_during_execution(
self, mock_run, mock_hooks, mock_get_files, mock_learn, mock_autocommit, conn
):
"""Guard: если пользователь вручную поставил 'cancelled' пока pipeline работал —
итоговый статус должен остаться 'cancelled'."""
def side_effect(*args, **kwargs):
models.update_task(conn, "VDOL-001", status="cancelled")
return _mock_claude_success({"result": "done"})
mock_run.side_effect = side_effect
mock_hooks.return_value = []
mock_get_files.return_value = []
mock_learn.return_value = {"added": 0, "skipped": 0}
steps = [{"role": "debugger", "brief": "find"}]
result = run_pipeline(conn, "VDOL-001", steps)
assert result["success"] is True
task = models.get_task(conn, "VDOL-001")
assert task["status"] == "cancelled" # guard НЕ перезаписал в "review"
@patch("agents.runner._run_autocommit")
@patch("agents.runner._run_learning_extraction")
@patch("agents.runner._get_changed_files")
@patch("agents.runner.run_hooks")
@patch("agents.runner.subprocess.run")
def test_pipeline_sets_review_when_no_manual_override(
self, mock_run, mock_hooks, mock_get_files, mock_learn, mock_autocommit, conn
):
"""Нормальный случай: задача в in_progress, пользователь не трогал статус —
после pipeline устанавливается 'review'."""
mock_run.return_value = _mock_claude_success({"result": "done"})
mock_hooks.return_value = []
mock_get_files.return_value = []
mock_learn.return_value = {"added": 0, "skipped": 0}
steps = [{"role": "debugger", "brief": "find"}]
result = run_pipeline(conn, "VDOL-001", steps)
assert result["success"] is True
task = models.get_task(conn, "VDOL-001")
assert task["status"] == "review"
@patch("agents.runner._run_autocommit")
@patch("agents.runner._run_learning_extraction")
@patch("agents.runner._get_changed_files")
@patch("core.followup.generate_followups")
@patch("agents.runner.run_hooks")
@patch("agents.runner.subprocess.run")
def test_auto_mode_preserves_done_status_set_during_execution(
self, mock_run, mock_hooks, mock_followup, mock_get_files, mock_learn, mock_autocommit, conn
):
"""Guard в auto_complete mode: если пользователь вручную поставил 'done'
пока pipeline работал guard пропускает обновление (уже done)."""
def side_effect(*args, **kwargs):
models.update_task(conn, "VDOL-001", status="done")
return _mock_claude_success({"result": "done"})
mock_run.side_effect = side_effect
mock_hooks.return_value = []
mock_followup.return_value = {"created": [], "pending_actions": []}
mock_get_files.return_value = []
mock_learn.return_value = {"added": 0, "skipped": 0}
models.update_project(conn, "vdol", execution_mode="auto_complete")
steps = [{"role": "tester", "brief": "verify"}]
result = run_pipeline(conn, "VDOL-001", steps)
assert result["success"] is True
task = models.get_task(conn, "VDOL-001")
assert task["status"] == "done"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Retry on permission error # Retry on permission error
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------