kin: KIN-ARCH-003 Сделать path nullable для operations-проектов
This commit is contained in:
parent
39acc9cc4b
commit
295a95bc7f
3 changed files with 229 additions and 0 deletions
|
|
@ -1504,3 +1504,79 @@ def test_create_research_project_type_accepted(client):
|
||||||
})
|
})
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert r.json()["project_type"] == "research"
|
assert r.json()["project_type"] == "research"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# KIN-ARCH-003 — path nullable для operations-проектов
|
||||||
|
# Исправляет баг: workaround с пустой строкой ("") для operations-проектов
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_kin_arch_003_operations_project_without_path_returns_200(client):
|
||||||
|
"""KIN-ARCH-003: POST /api/projects с project_type='operations' без path → 200.
|
||||||
|
|
||||||
|
До фикса: path="" передавался как workaround для NOT NULL constraint.
|
||||||
|
После фикса: path не передаётся вовсе, сохраняется как NULL.
|
||||||
|
"""
|
||||||
|
r = client.post("/api/projects", json={
|
||||||
|
"id": "ops_null_path",
|
||||||
|
"name": "Ops Null Path",
|
||||||
|
"project_type": "operations",
|
||||||
|
"ssh_host": "10.0.0.1",
|
||||||
|
})
|
||||||
|
assert r.status_code == 200
|
||||||
|
data = r.json()
|
||||||
|
assert data["path"] is None, (
|
||||||
|
"KIN-ARCH-003 регрессия: path должен быть NULL, а не пустой строкой"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_kin_arch_003_development_project_without_path_returns_422(client):
|
||||||
|
"""KIN-ARCH-003: POST /api/projects с project_type='development' без path → 422.
|
||||||
|
|
||||||
|
Pydantic validate_fields: path обязателен для non-operations проектов.
|
||||||
|
"""
|
||||||
|
r = client.post("/api/projects", json={
|
||||||
|
"id": "dev_no_path",
|
||||||
|
"name": "Dev No Path",
|
||||||
|
"project_type": "development",
|
||||||
|
})
|
||||||
|
assert r.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
def test_kin_arch_003_development_without_path_error_mentions_path(client):
|
||||||
|
"""KIN-ARCH-003: тело 422-ответа содержит упоминание об обязательности path."""
|
||||||
|
r = client.post("/api/projects", json={
|
||||||
|
"id": "dev_no_path_msg",
|
||||||
|
"name": "Dev No Path Msg",
|
||||||
|
"project_type": "development",
|
||||||
|
})
|
||||||
|
assert r.status_code == 422
|
||||||
|
detail_str = str(r.json())
|
||||||
|
assert "path" in detail_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_kin_arch_003_deploy_operations_project_null_path_uses_cwd_none(client):
|
||||||
|
"""KIN-ARCH-003: deploy_project для operations-проекта с path=NULL
|
||||||
|
не вызывает Path.exists() — передаёт cwd=None в subprocess.run."""
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
client.post("/api/projects", json={
|
||||||
|
"id": "ops_deploy_null",
|
||||||
|
"name": "Ops Deploy Null Path",
|
||||||
|
"project_type": "operations",
|
||||||
|
"ssh_host": "10.0.0.1",
|
||||||
|
})
|
||||||
|
client.patch("/api/projects/ops_deploy_null", json={"deploy_command": "echo ok"})
|
||||||
|
|
||||||
|
mock_result = MagicMock()
|
||||||
|
mock_result.returncode = 0
|
||||||
|
mock_result.stdout = "ok\n"
|
||||||
|
mock_result.stderr = ""
|
||||||
|
|
||||||
|
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||||
|
r = client.post("/api/projects/ops_deploy_null/deploy")
|
||||||
|
|
||||||
|
assert r.status_code == 200
|
||||||
|
call_kwargs = mock_run.call_args.kwargs
|
||||||
|
assert call_kwargs.get("cwd") is None, (
|
||||||
|
"KIN-ARCH-003: для operations-проектов без path, cwd должен быть None"
|
||||||
|
)
|
||||||
|
|
|
||||||
121
tests/test_arch_002.py
Normal file
121
tests/test_arch_002.py
Normal 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
|
||||||
|
|
@ -89,6 +89,38 @@ def test_update_project_ssh_fields(conn):
|
||||||
assert updated["ssh_user"] == "pelmen"
|
assert updated["ssh_user"] == "pelmen"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# KIN-ARCH-003 — path nullable для operations-проектов
|
||||||
|
# Исправляет баг: workaround с пустой строкой ("") для operations-проектов
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_kin_arch_003_operations_project_without_path_stores_null(conn):
|
||||||
|
"""KIN-ARCH-003: operations-проект без path сохраняется с path=NULL, не пустой строкой.
|
||||||
|
|
||||||
|
До фикса: workaround — передавать path='' чтобы обойти NOT NULL constraint.
|
||||||
|
После фикса: path=None (NULL в БД) допустим для operations-проектов.
|
||||||
|
"""
|
||||||
|
p = models.create_project(
|
||||||
|
conn, "ops_null", "Ops Null Path",
|
||||||
|
project_type="operations",
|
||||||
|
ssh_host="10.0.0.1",
|
||||||
|
)
|
||||||
|
assert p["path"] is None, (
|
||||||
|
"KIN-ARCH-003 регрессия: path должен быть NULL, а не пустой строкой"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_kin_arch_003_check_constraint_rejects_null_path_for_development(conn):
|
||||||
|
"""KIN-ARCH-003: CHECK constraint (path IS NOT NULL OR project_type='operations')
|
||||||
|
отклоняет path=NULL для development-проектов."""
|
||||||
|
import sqlite3 as _sqlite3
|
||||||
|
with pytest.raises(_sqlite3.IntegrityError):
|
||||||
|
models.create_project(
|
||||||
|
conn, "dev_no_path", "Dev No Path",
|
||||||
|
path=None, project_type="development",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -- validate_completion_mode (KIN-063) --
|
# -- validate_completion_mode (KIN-063) --
|
||||||
|
|
||||||
def test_validate_completion_mode_valid_auto_complete():
|
def test_validate_completion_mode_valid_auto_complete():
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue