kin/tests/test_kin_biz_002.py

201 lines
8 KiB
Python
Raw Normal View History

"""Regression tests for KIN-BIZ-002.
Проблема: approve через /tasks/{id}/approve не продвигал phase state machine.
Фикс: в approve_task() добавлен блок, вызывающий approve_phase() из core.phases
если задача принадлежит активной фазе.
В approve_phase() endpoint добавлена синхронизация task.status='done'.
Тесты покрывают:
1. POST /tasks/{id}/approve для phase-задачи phase.status=done, следующая фаза active
2. Изменения в БД персистентны после approve
3. POST /tasks/{id}/approve для обычной задачи не ломает ничего, phase=None
4. POST /phases/{id}/approve task.status синхронизируется в done
"""
import pytest
from fastapi.testclient import TestClient
import web.api as api_module
@pytest.fixture
def client(tmp_path):
"""Изолированная временная БД для каждого теста."""
db_path = tmp_path / "test_biz002.db"
api_module.DB_PATH = db_path
from web.api import app
return TestClient(app)
def _create_project_with_phases(client, project_id: str = "proj_biz002") -> dict:
"""Вспомогательная: создаёт проект с двумя researcher-фазами + architect."""
r = client.post("/api/projects/new", json={
"id": project_id,
"name": "BIZ-002 Test Project",
"path": f"/tmp/{project_id}",
"description": "Тест регрессии KIN-BIZ-002",
"roles": ["business_analyst", "tech_researcher"],
})
assert r.status_code == 200, r.json()
return r.json()
def _get_active_phase(client, project_id: str) -> dict:
"""Вспомогательная: возвращает первую активную фазу."""
phases = client.get(f"/api/projects/{project_id}/phases").json()
active = next(ph for ph in phases if ph["status"] == "active")
return active
# ---------------------------------------------------------------------------
# KIN-BIZ-002 — регрессионные тесты
# ---------------------------------------------------------------------------
def test_KIN_BIZ_002_approve_task_advances_phase_state_machine(client):
"""KIN-BIZ-002: POST /tasks/{id}/approve для phase-задачи продвигает state machine.
Ожидаем: phase.status=approved, next_phase активирован.
"""
_create_project_with_phases(client)
active_phase = _get_active_phase(client, "proj_biz002")
task_id = active_phase["task_id"]
r = client.post(f"/api/tasks/{task_id}/approve", json={})
assert r.status_code == 200
data = r.json()
assert data["status"] == "done"
# Ключ phase должен присутствовать и содержать результат
assert "phase" in data
assert data["phase"] is not None
# Одобренная фаза имеет status=approved
assert data["phase"]["phase"]["status"] == "approved"
# Следующая фаза была активирована
assert data["phase"]["next_phase"] is not None
assert data["phase"]["next_phase"]["status"] == "active"
def test_KIN_BIZ_002_approve_task_phase_status_persists_in_db(client):
"""KIN-BIZ-002: после approve через /tasks/{id}/approve статусы фаз корректны в БД.
Первая фаза approved, вторая фаза active.
"""
data = _create_project_with_phases(client)
# Три фазы: business_analyst, tech_researcher, architect
assert len(data["phases"]) == 3
active_phase = _get_active_phase(client, "proj_biz002")
task_id = active_phase["task_id"]
client.post(f"/api/tasks/{task_id}/approve", json={})
# Перечитываем фазы из БД
phases = client.get("/api/projects/proj_biz002/phases").json()
statuses = {ph["role"]: ph["status"] for ph in phases}
assert statuses["business_analyst"] == "approved"
assert statuses["tech_researcher"] == "active"
assert statuses["architect"] == "pending"
def test_KIN_BIZ_002_approve_task_task_status_is_done(client):
"""KIN-BIZ-002: сама задача должна иметь status=done после approve."""
_create_project_with_phases(client)
active_phase = _get_active_phase(client, "proj_biz002")
task_id = active_phase["task_id"]
client.post(f"/api/tasks/{task_id}/approve", json={})
task = client.get(f"/api/tasks/{task_id}").json()
assert task["status"] == "done"
def test_KIN_BIZ_002_approve_regular_task_does_not_affect_phases(client):
"""KIN-BIZ-002: approve обычной задачи (без фазы) не ломает ничего, phase=None."""
# Создаём обычный проект без фаз
client.post("/api/projects", json={
"id": "plain_proj",
"name": "Plain Project",
"path": "/tmp/plain_proj",
})
r_task = client.post("/api/tasks", json={
"project_id": "plain_proj",
"title": "Обычная задача без фазы",
})
assert r_task.status_code == 200
task_id = r_task.json()["id"]
r = client.post(f"/api/tasks/{task_id}/approve", json={})
assert r.status_code == 200
data = r.json()
assert data["status"] == "done"
# phase должен быть None — нет связанной фазы
assert data["phase"] is None
def test_KIN_BIZ_002_approve_regular_task_sets_status_done(client):
"""KIN-BIZ-002: approve обычной задачи корректно устанавливает status=done."""
client.post("/api/projects", json={
"id": "plain2",
"name": "Plain2",
"path": "/tmp/plain2",
})
r_task = client.post("/api/tasks", json={
"project_id": "plain2",
"title": "Задача без фазы",
})
task_id = r_task.json()["id"]
client.post(f"/api/tasks/{task_id}/approve", json={})
task = client.get(f"/api/tasks/{task_id}").json()
assert task["status"] == "done"
def test_KIN_BIZ_002_approve_phase_endpoint_syncs_task_status_to_done(client):
"""KIN-BIZ-002: POST /phases/{id}/approve синхронизирует task.status=done.
Гарантируем консистентность обоих путей одобрения фазы.
"""
_create_project_with_phases(client)
active_phase = _get_active_phase(client, "proj_biz002")
phase_id = active_phase["id"]
task_id = active_phase["task_id"]
r = client.post(f"/api/phases/{phase_id}/approve", json={})
assert r.status_code == 200
# Задача, связанная с фазой, должна иметь status=done
task = client.get(f"/api/tasks/{task_id}").json()
assert task["status"] == "done"
def test_KIN_BIZ_002_full_phase_chain_two_approves_completes_workflow(client):
"""KIN-BIZ-002: последовательный approve через /tasks/{id}/approve проходит весь chain.
business_analyst approved tech_researcher approved architect approved.
"""
_create_project_with_phases(client)
phases_init = client.get("/api/projects/proj_biz002/phases").json()
assert len(phases_init) == 3
# Апруваем каждую фазу последовательно через task-endpoint
for _ in range(3):
phases = client.get("/api/projects/proj_biz002/phases").json()
active = next((ph for ph in phases if ph["status"] == "active"), None)
if active is None:
break
task_id = active["task_id"]
r = client.post(f"/api/tasks/{task_id}/approve", json={})
assert r.status_code == 200
assert r.json()["status"] == "done"
# После всех approve все фазы должны быть approved
final_phases = client.get("/api/projects/proj_biz002/phases").json()
for ph in final_phases:
assert ph["status"] == "approved", (
f"Ожидали approved для {ph['role']}, получили {ph['status']}"
)