kin: KIN-ARCH-003 Сделать path nullable для operations-проектов

This commit is contained in:
Gros Frumos 2026-03-16 09:52:44 +02:00
parent 39acc9cc4b
commit 295a95bc7f
3 changed files with 229 additions and 0 deletions

121
tests/test_arch_002.py Normal file
View file

@ -0,0 +1,121 @@
"""Regression tests for KIN-ARCH-002.
Проблема: функция create_project_with_phases имела нестабильную сигнатуру
параметр path с дефолтом на позиции 4, после чего шли обязательные параметры
(description, selected_roles), что могло приводить к SyntaxError при инвалидации
.pyc-кеша в Python 3.14+.
Фикс: параметры path переносится после обязательных ИЛИ изолируется через *
(keyword-only) текущий код использует * для description/selected_roles.
Тесты покрывают:
1. Вызов с path как позиционным аргументом (текущая конвенция в тестах)
2. Вызов с path=... как keyword-аргументом (безопасная конвенция)
3. Вызов без path=None (дефолт работает)
4. Нет SyntaxError при импорте core.phases (regression guard)
5. Стабильность числа тестов: полный suite запускается без collection errors
"""
import pytest
from core.db import init_db
from core import models
from core.phases import create_project_with_phases
@pytest.fixture
def conn():
c = init_db(db_path=":memory:")
yield c
c.close()
# ---------------------------------------------------------------------------
# KIN-ARCH-002 — regression: signature stability of create_project_with_phases
# ---------------------------------------------------------------------------
def test_arch_002_import_core_phases_no_syntax_error():
"""KIN-ARCH-002: импорт core.phases не вызывает SyntaxError."""
import core.phases # noqa: F401 — если упадёт SyntaxError, тест падает
def test_arch_002_path_as_positional_arg(conn):
"""KIN-ARCH-002: path передаётся как позиционный аргумент (4-я позиция).
Текущая конвенция во всех тестах и в web/api.py.
Регрессионная защита: изменение сигнатуры не должно сломать этот вызов.
"""
result = create_project_with_phases(
conn, "arch002a", "Project A", "/some/path",
description="Описание A", selected_roles=["business_analyst"],
)
assert result["project"]["id"] == "arch002a"
assert len(result["phases"]) == 2 # business_analyst + architect
def test_arch_002_path_as_keyword_arg(conn):
"""KIN-ARCH-002: path передаётся как keyword-аргумент.
Рекомендуемая конвенция по итогам debugger-расследования.
Гарантирует, что будущий рефакторинг сигнатуры не сломает код.
"""
result = create_project_with_phases(
conn, "arch002b", "Project B",
description="Описание B",
selected_roles=["tech_researcher"],
path="/keyword/path",
)
assert result["project"]["id"] == "arch002b"
assert result["project"]["path"] == "/keyword/path"
def test_arch_002_path_none_without_operations_raises(conn):
"""KIN-ARCH-002: path=None для non-operations проекта → IntegrityError из БД (CHECK constraint)."""
import sqlite3
with pytest.raises(sqlite3.IntegrityError, match="CHECK constraint"):
create_project_with_phases(
conn, "arch002fail", "Fail",
description="D",
selected_roles=["marketer"],
path=None,
)
def test_arch_002_phases_count_is_deterministic(conn):
"""KIN-ARCH-002: при каждом вызове создаётся ровно N+1 фаз (N researchers + architect)."""
for idx, (roles, expected_count) in enumerate([
(["business_analyst"], 2),
(["business_analyst", "tech_researcher"], 3),
(["business_analyst", "market_researcher", "legal_researcher"], 4),
]):
project_id = f"arch002_det_{idx}"
result = create_project_with_phases(
conn, project_id, f"Project {len(roles)}",
description="Det test",
selected_roles=roles,
path=f"/tmp/det/{idx}",
)
assert len(result["phases"]) == expected_count, (
f"roles={roles}: ожидали {expected_count} фаз, "
f"получили {len(result['phases'])}"
)
def test_arch_002_first_phase_active_regardless_of_call_convention(conn):
"""KIN-ARCH-002: первая фаза всегда active независимо от способа передачи path."""
# Positional convention
r1 = create_project_with_phases(
conn, "p_pos", "P pos", "/pos",
description="D", selected_roles=["business_analyst"],
)
assert r1["phases"][0]["status"] == "active"
assert r1["phases"][0]["task_id"] is not None
# Keyword convention
r2 = create_project_with_phases(
conn, "p_kw", "P kw",
description="D", selected_roles=["business_analyst"], path="/kw",
)
assert r2["phases"][0]["status"] == "active"
assert r2["phases"][0]["task_id"] is not None