From cfc4a6ba7da4076615f4d6b31a77c4ea5ad31d78 Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Wed, 18 Mar 2026 21:46:41 +0200 Subject: [PATCH] kin: auto-commit after pipeline --- tests/test_models.py | 77 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/test_models.py b/tests/test_models.py index 2da8ea2..3c7ede5 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -932,3 +932,80 @@ def test_get_pipeline_logs_ordered_asc(pipeline_conn): logs = models.get_pipeline_logs(db, pid) ids = [log["id"] for log in logs] assert ids == sorted(ids) + + +# --------------------------------------------------------------------------- +# KIN-UI-018: Защита от circular references в has_open_children / +# _check_parent_completion (decision #816, #817) +# --------------------------------------------------------------------------- + +def test_circular_reference_protection_has_open_children_returns_false(conn): + """KIN-UI-018 (decision #816): has_open_children возвращает False при циклической ссылке A→B→A. + + Задачи A и B создаются напрямую в БД с взаимными parent_task_id. + Ожидаемый результат: False (не True, не RecursionError). + """ + models.create_project(conn, "p1", "P1", "/p1") + # Создаём задачи без parent сначала + models.create_task(conn, "P1-CYC-A", "p1", "Task A") + models.create_task(conn, "P1-CYC-B", "p1", "Task B") + # Устанавливаем цикл напрямую в БД, минуя валидацию API + conn.execute("UPDATE tasks SET parent_task_id = 'P1-CYC-B' WHERE id = 'P1-CYC-A'") + conn.execute("UPDATE tasks SET parent_task_id = 'P1-CYC-A' WHERE id = 'P1-CYC-B'") + conn.commit() + + result_a = models.has_open_children(conn, "P1-CYC-A") + result_b = models.has_open_children(conn, "P1-CYC-B") + + assert result_a is False + assert result_b is False + + +def test_circular_reference_protection_check_parent_completion_returns_without_error(conn): + """KIN-UI-018 (decision #817): _check_parent_completion не падает и не зависает при цикле A→B→A. + + Задачи A и B в статусе 'revising' с взаимными parent_task_id. + Ожидаемый результат: возврат без RecursionError, статус задач не изменился. + """ + models.create_project(conn, "p1", "P1", "/p1") + models.create_task(conn, "P1-CPC-A", "p1", "Task A", status="revising") + models.create_task(conn, "P1-CPC-B", "p1", "Task B", status="revising") + # Устанавливаем цикл напрямую в БД + conn.execute("UPDATE tasks SET parent_task_id = 'P1-CPC-B' WHERE id = 'P1-CPC-A'") + conn.execute("UPDATE tasks SET parent_task_id = 'P1-CPC-A' WHERE id = 'P1-CPC-B'") + conn.commit() + + # Не должно бросить RecursionError или зависнуть + models._check_parent_completion(conn, "P1-CPC-A") + models._check_parent_completion(conn, "P1-CPC-B") + + # Статусы не изменились — цикл обнаружен и прерван + task_a = models.get_task(conn, "P1-CPC-A") + task_b = models.get_task(conn, "P1-CPC-B") + assert task_a["status"] == "revising" + assert task_b["status"] == "revising" + + +# --------------------------------------------------------------------------- +# KIN-UI-020: VALID_TASK_STATUSES — frozenset membership checks +# --------------------------------------------------------------------------- + +def test_valid_task_statuses_is_frozenset(): + """KIN-UI-020: VALID_TASK_STATUSES должен быть frozenset, не list.""" + assert isinstance(models.VALID_TASK_STATUSES, frozenset) + + +@pytest.mark.parametrize("status", [ + "pending", "in_progress", "review", "done", + "blocked", "decomposed", "cancelled", "revising", +]) +def test_valid_task_statuses_membership(status): + """KIN-UI-020: каждый валидный статус присутствует в VALID_TASK_STATUSES (membership check).""" + assert status in models.VALID_TASK_STATUSES + + +def test_invalid_status_not_in_valid_task_statuses(): + """KIN-UI-020: невалидный статус отсутствует в VALID_TASK_STATUSES.""" + assert "invalid_status" not in models.VALID_TASK_STATUSES + assert "" not in models.VALID_TASK_STATUSES + assert "active" not in models.VALID_TASK_STATUSES