163 lines
6.4 KiB
Python
163 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
|