kin: KIN-016 Агенты должны уметь говорить 'не могу'. Если агент не может выполнить задачу (нет доступа, не понимает, выходит за компетенцию) — он должен вернуть status: blocked с причиной, а не пытаться угадывать. PM при получении blocked от агента — эскалирует к человеку через GUI (уведомление) и Telegram (когда будет).
This commit is contained in:
parent
a605e9d110
commit
d9172fc17c
35 changed files with 2375 additions and 23 deletions
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue