162 lines
6.4 KiB
Python
162 lines
6.4 KiB
Python
"""Regression tests for KIN-110 — Followup button for blocked tasks.
|
||
|
||
Verifies:
|
||
1. POST /api/tasks/{id}/followup endpoint exists and responds for blocked task
|
||
2. Endpoint returns 404 for non-existent task
|
||
3. Endpoint works for any task status (not just blocked)
|
||
4. Response contains expected fields: created, pending_actions, needs_decision
|
||
"""
|
||
|
||
import json
|
||
import pytest
|
||
from unittest.mock import patch, MagicMock
|
||
|
||
import web.api as api_module
|
||
|
||
|
||
@pytest.fixture
|
||
def client(tmp_path):
|
||
db_path = tmp_path / "test.db"
|
||
api_module.DB_PATH = db_path
|
||
from web.api import app
|
||
from fastapi.testclient import TestClient
|
||
c = TestClient(app)
|
||
c.post("/api/projects", json={"id": "p1", "name": "P1", "path": "/p1"})
|
||
c.post("/api/tasks", json={"project_id": "p1", "title": "Fix auth"})
|
||
return c
|
||
|
||
|
||
@pytest.fixture
|
||
def blocked_client(tmp_path):
|
||
"""Client with a blocked task seeded."""
|
||
db_path = tmp_path / "test.db"
|
||
api_module.DB_PATH = db_path
|
||
from web.api import app
|
||
from fastapi.testclient import TestClient
|
||
from core.db import init_db
|
||
from core import models
|
||
c = TestClient(app)
|
||
c.post("/api/projects", json={"id": "p1", "name": "P1", "path": "/p1"})
|
||
c.post("/api/tasks", json={"project_id": "p1", "title": "Blocked task"})
|
||
# Set task to blocked status
|
||
conn = init_db(db_path)
|
||
models.update_task(conn, "P1-001", status="blocked")
|
||
conn.close()
|
||
return c
|
||
|
||
|
||
class TestFollowupEndpoint:
|
||
"""Tests for POST /api/tasks/{id}/followup endpoint."""
|
||
|
||
@patch("agents.runner._run_claude")
|
||
def test_followup_blocked_task_returns_200(self, mock_claude, blocked_client):
|
||
"""POST /followup на blocked-задаче должен вернуть 200."""
|
||
mock_claude.return_value = {
|
||
"output": json.dumps([
|
||
{"title": "Fix dependency", "type": "hotfix", "priority": 2,
|
||
"brief": "Add missing dep"},
|
||
]),
|
||
"returncode": 0,
|
||
}
|
||
r = blocked_client.post("/api/tasks/P1-001/followup", json={})
|
||
assert r.status_code == 200
|
||
|
||
@patch("agents.runner._run_claude")
|
||
def test_followup_returns_created_tasks(self, mock_claude, blocked_client):
|
||
"""Ответ /followup содержит список created с созданными задачами."""
|
||
mock_claude.return_value = {
|
||
"output": json.dumps([
|
||
{"title": "Fix dependency", "type": "hotfix", "priority": 2,
|
||
"brief": "Add missing dep"},
|
||
]),
|
||
"returncode": 0,
|
||
}
|
||
r = blocked_client.post("/api/tasks/P1-001/followup", json={})
|
||
assert r.status_code == 200
|
||
data = r.json()
|
||
assert "created" in data
|
||
assert len(data["created"]) == 1
|
||
assert data["created"][0]["title"] == "Fix dependency"
|
||
|
||
@patch("agents.runner._run_claude")
|
||
def test_followup_response_has_required_fields(self, mock_claude, blocked_client):
|
||
"""Ответ /followup содержит поля created, pending_actions, needs_decision."""
|
||
mock_claude.return_value = {"output": "[]", "returncode": 0}
|
||
r = blocked_client.post("/api/tasks/P1-001/followup", json={})
|
||
assert r.status_code == 200
|
||
data = r.json()
|
||
assert "created" in data
|
||
assert "pending_actions" in data
|
||
assert "needs_decision" in data
|
||
|
||
def test_followup_nonexistent_task_returns_404(self, blocked_client):
|
||
"""POST /followup на несуществующую задачу → 404."""
|
||
r = blocked_client.post("/api/tasks/NOPE/followup", json={})
|
||
assert r.status_code == 404
|
||
|
||
@patch("agents.runner._run_claude")
|
||
def test_followup_dry_run_does_not_create_tasks(self, mock_claude, blocked_client):
|
||
"""dry_run=true не создаёт задачи в БД."""
|
||
from core.db import init_db
|
||
from core import models
|
||
mock_claude.return_value = {
|
||
"output": json.dumps([
|
||
{"title": "Dry run task", "type": "hotfix", "priority": 3,
|
||
"brief": "should not be created"},
|
||
]),
|
||
"returncode": 0,
|
||
}
|
||
r = blocked_client.post("/api/tasks/P1-001/followup", json={"dry_run": True})
|
||
assert r.status_code == 200
|
||
|
||
# No tasks should be created in DB (only original P1-001 remains)
|
||
conn = init_db(api_module.DB_PATH)
|
||
tasks = models.list_tasks(conn, project_id="p1")
|
||
conn.close()
|
||
assert len(tasks) == 1 # Only P1-001, no new tasks
|
||
|
||
@patch("agents.runner._run_claude")
|
||
def test_followup_creates_child_tasks_in_db(self, mock_claude, blocked_client):
|
||
"""Созданные followup-задачи сохраняются в БД с parent_task_id."""
|
||
from core.db import init_db
|
||
from core import models
|
||
mock_claude.return_value = {
|
||
"output": json.dumps([
|
||
{"title": "New dep task", "type": "feature", "priority": 4,
|
||
"brief": "Install dependency"},
|
||
]),
|
||
"returncode": 0,
|
||
}
|
||
r = blocked_client.post("/api/tasks/P1-001/followup", json={})
|
||
assert r.status_code == 200
|
||
|
||
conn = init_db(api_module.DB_PATH)
|
||
tasks = models.list_tasks(conn, project_id="p1")
|
||
conn.close()
|
||
# Original task + 1 followup
|
||
assert len(tasks) == 2
|
||
followup = next((t for t in tasks if t["id"] != "P1-001"), None)
|
||
assert followup is not None
|
||
assert followup["parent_task_id"] == "P1-001"
|
||
|
||
@patch("agents.runner._run_claude")
|
||
def test_followup_pending_actions_for_permission_blocked_items(
|
||
self, mock_claude, blocked_client
|
||
):
|
||
"""Элементы с permission-блокером попадают в pending_actions, не в created."""
|
||
mock_claude.return_value = {
|
||
"output": json.dumps([
|
||
{"title": "Normal task", "type": "hotfix", "priority": 2,
|
||
"brief": "Just a fix"},
|
||
{"title": "Ручное применение .env",
|
||
"type": "hotfix", "priority": 3,
|
||
"brief": "Не получили разрешение на запись"},
|
||
]),
|
||
"returncode": 0,
|
||
}
|
||
r = blocked_client.post("/api/tasks/P1-001/followup", json={})
|
||
assert r.status_code == 200
|
||
data = r.json()
|
||
assert len(data["created"]) == 1
|
||
assert len(data["pending_actions"]) == 1
|
||
assert data["needs_decision"] is True
|