kin: KIN-049 Кнопка Deploy на странице задачи после approve. Для каждого проекта настраивается deploy-команда (git push, scp, ssh restart). В Settings проекта.

This commit is contained in:
Gros Frumos 2026-03-16 08:21:13 +02:00
parent 860ef3f6c9
commit d50bd703ae
11 changed files with 517 additions and 61 deletions

View file

@ -939,3 +939,135 @@ def test_patch_task_title_and_brief_text_together(client):
data = r.json()
assert data["title"] == "Совместное"
assert data["brief"]["text"] == "и описание"
# ---------------------------------------------------------------------------
# KIN-049 — Deploy: миграция, PATCH deploy_command, POST /deploy
# ---------------------------------------------------------------------------
def test_deploy_command_column_exists_in_schema(client):
"""Миграция: PRAGMA table_info(projects) подтверждает наличие deploy_command (decision #74)."""
from core.db import init_db
conn = init_db(api_module.DB_PATH)
cols = {row[1] for row in conn.execute("PRAGMA table_info(projects)").fetchall()}
conn.close()
assert "deploy_command" in cols
def test_patch_project_deploy_command_persisted_via_sql(client):
"""PATCH с deploy_command сохраняется в БД — прямой SQL (decision #55)."""
client.patch("/api/projects/p1", json={"deploy_command": "echo hello"})
from core.db import init_db
conn = init_db(api_module.DB_PATH)
row = conn.execute("SELECT deploy_command FROM projects WHERE id = 'p1'").fetchone()
conn.close()
assert row is not None
assert row[0] == "echo hello"
def test_patch_project_deploy_command_returned_in_response(client):
"""После PATCH ответ содержит обновлённый deploy_command."""
r = client.patch("/api/projects/p1", json={"deploy_command": "git push origin main"})
assert r.status_code == 200
assert r.json()["deploy_command"] == "git push origin main"
def test_patch_project_deploy_command_empty_string_clears_to_null(client):
"""PATCH с пустой строкой очищает deploy_command → NULL (decision #68)."""
client.patch("/api/projects/p1", json={"deploy_command": "echo hello"})
client.patch("/api/projects/p1", json={"deploy_command": ""})
from core.db import init_db
conn = init_db(api_module.DB_PATH)
row = conn.execute("SELECT deploy_command FROM projects WHERE id = 'p1'").fetchone()
conn.close()
assert row[0] is None
def test_deploy_project_executes_command_returns_stdout(client):
"""POST /deploy — команда echo → stdout присутствует в ответе."""
from unittest.mock import patch, MagicMock
client.patch("/api/projects/p1", json={"deploy_command": "echo deployed"})
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "deployed\n"
mock_result.stderr = ""
with patch("web.api.subprocess.run", return_value=mock_result):
r = client.post("/api/projects/p1/deploy")
assert r.status_code == 200
data = r.json()
assert data["success"] is True
assert data["exit_code"] == 0
assert "deployed" in data["stdout"]
assert "duration_seconds" in data
def test_deploy_project_without_deploy_command_returns_400(client):
"""POST /deploy для проекта без deploy_command → 400."""
r = client.post("/api/projects/p1/deploy")
assert r.status_code == 400
def test_deploy_project_not_found_returns_404(client):
"""POST /deploy для несуществующего проекта → 404."""
r = client.post("/api/projects/NOPE/deploy")
assert r.status_code == 404
def test_deploy_project_failed_command_returns_success_false(client):
"""POST /deploy — ненулевой exit_code → success=False (команда выполнилась, но упала)."""
from unittest.mock import patch, MagicMock
client.patch("/api/projects/p1", json={"deploy_command": "exit 1"})
mock_result = MagicMock()
mock_result.returncode = 1
mock_result.stdout = ""
mock_result.stderr = "error occurred"
with patch("web.api.subprocess.run", return_value=mock_result):
r = client.post("/api/projects/p1/deploy")
assert r.status_code == 200
data = r.json()
assert data["success"] is False
assert data["exit_code"] == 1
assert "error occurred" in data["stderr"]
def test_deploy_project_timeout_returns_504(client):
"""POST /deploy — timeout → 504."""
from unittest.mock import patch
import subprocess
client.patch("/api/projects/p1", json={"deploy_command": "sleep 100"})
with patch("web.api.subprocess.run", side_effect=subprocess.TimeoutExpired("sleep 100", 60)):
r = client.post("/api/projects/p1/deploy")
assert r.status_code == 504
def test_task_full_includes_project_deploy_command(client):
"""GET /api/tasks/{id}/full включает project_deploy_command из таблицы projects."""
client.patch("/api/projects/p1", json={"deploy_command": "git push"})
r = client.get("/api/tasks/P1-001/full")
assert r.status_code == 200
data = r.json()
assert "project_deploy_command" in data
assert data["project_deploy_command"] == "git push"
def test_task_full_project_deploy_command_none_when_not_set(client):
"""GET /api/tasks/{id}/full возвращает project_deploy_command=None когда не задана."""
r = client.get("/api/tasks/P1-001/full")
assert r.status_code == 200
data = r.json()
assert "project_deploy_command" in data
assert data["project_deploy_command"] is None