"""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']}" )