kin: KIN-016 Агенты должны уметь говорить 'не могу'. Если агент не может выполнить задачу (нет доступа, не понимает, выходит за компетенцию) — он должен вернуть status: blocked с причиной, а не пытаться угадывать. PM при получении blocked от агента — эскалирует к человеку через GUI (уведомление) и Telegram (когда будет).

This commit is contained in:
Gros Frumos 2026-03-16 09:13:34 +02:00
parent a605e9d110
commit d9172fc17c
35 changed files with 2375 additions and 23 deletions

View file

@ -1132,4 +1132,136 @@ def test_sync_obsidian_after_patch_returns_sync_result_fields(client, tmp_path):
assert r.status_code == 200
data = r.json()
assert "exported_decisions" in data
assert "tasks_updated" in data
# ---------------------------------------------------------------------------
# KIN-016 — GET /api/notifications — эскалации от заблокированных агентов
# ---------------------------------------------------------------------------
def test_kin016_notifications_empty_when_no_blocked_tasks(client):
"""KIN-016: GET /api/notifications возвращает [] когда нет заблокированных задач."""
r = client.get("/api/notifications")
assert r.status_code == 200
assert r.json() == []
def test_kin016_notifications_returns_blocked_task_as_escalation(client):
"""KIN-016: заблокированная задача появляется в /api/notifications с корректными полями."""
from core.db import init_db
from core import models
conn = init_db(api_module.DB_PATH)
models.update_task(
conn, "P1-001",
status="blocked",
blocked_reason="cannot access external API",
blocked_at="2026-03-16T10:00:00",
blocked_agent_role="debugger",
blocked_pipeline_step="1",
)
conn.close()
r = client.get("/api/notifications")
assert r.status_code == 200
items = r.json()
assert len(items) == 1
item = items[0]
assert item["task_id"] == "P1-001"
assert item["agent_role"] == "debugger"
assert item["reason"] == "cannot access external API"
assert item["pipeline_step"] == "1"
assert item["blocked_at"] == "2026-03-16T10:00:00"
def test_kin016_notifications_contains_project_id_and_title(client):
"""KIN-016: уведомление содержит project_id и title задачи."""
from core.db import init_db
from core import models
conn = init_db(api_module.DB_PATH)
models.update_task(conn, "P1-001", status="blocked",
blocked_reason="out of scope",
blocked_agent_role="architect")
conn.close()
r = client.get("/api/notifications")
assert r.status_code == 200
item = r.json()[0]
assert item["project_id"] == "p1"
assert item["title"] == "Fix bug"
def test_kin016_notifications_filters_by_project_id(client):
"""KIN-016: ?project_id= фильтрует уведомления по проекту."""
from core.db import init_db
from core import models
conn = init_db(api_module.DB_PATH)
# Создаём второй проект с заблокированной задачей
models.create_project(conn, "p2", "P2", "/p2")
models.create_task(conn, "P2-001", "p2", "Another task")
models.update_task(conn, "P1-001", status="blocked",
blocked_reason="reason A", blocked_agent_role="debugger")
models.update_task(conn, "P2-001", status="blocked",
blocked_reason="reason B", blocked_agent_role="tester")
conn.close()
r = client.get("/api/notifications?project_id=p1")
assert r.status_code == 200
items = r.json()
assert all(i["project_id"] == "p1" for i in items)
assert len(items) == 1
assert items[0]["task_id"] == "P1-001"
def test_kin016_notifications_only_returns_blocked_status(client):
"""KIN-016: задачи в статусе pending/review/done НЕ попадают в уведомления."""
from core.db import init_db
from core import models
conn = init_db(api_module.DB_PATH)
# Задача остаётся в pending (дефолт)
assert models.get_task(conn, "P1-001")["status"] == "pending"
conn.close()
r = client.get("/api/notifications")
assert r.status_code == 200
assert r.json() == []
def test_kin016_pipeline_blocked_agent_stops_next_steps_integration(client):
"""KIN-016: после blocked пайплайна задача блокируется, /api/notifications показывает её.
Интеграционный тест: pipeline blocked /api/notifications содержит task.
"""
import json
from unittest.mock import patch, MagicMock
blocked_output = json.dumps({
"result": json.dumps({"status": "blocked", "reason": "no repo access"}),
})
mock_proc = MagicMock()
mock_proc.pid = 123
with patch("web.api.subprocess.Popen") as mock_popen:
mock_popen.return_value = mock_proc
r = client.post("/api/tasks/P1-001/run")
assert r.status_code == 202
# Вручную помечаем задачу blocked (имитируем результат пайплайна)
from core.db import init_db
from core import models
conn = init_db(api_module.DB_PATH)
models.update_task(
conn, "P1-001",
status="blocked",
blocked_reason="no repo access",
blocked_agent_role="debugger",
blocked_pipeline_step="1",
)
conn.close()
r = client.get("/api/notifications")
assert r.status_code == 200
items = r.json()
assert len(items) == 1
assert items[0]["task_id"] == "P1-001"
assert items[0]["reason"] == "no repo access"
assert items[0]["agent_role"] == "debugger"