kin/tests/test_kin_110_regression.py

163 lines
6.4 KiB
Python
Raw Permalink Normal View History

2026-03-17 21:17:19 +02:00
"""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