kin: KIN-083 Healthcheck claude CLI auth: перед запуском pipeline проверять что claude залогинен (быстрый claude -p 'ok' --output-format json, проверить is_error и 'Not logged in'). Если не залогинен — не запускать pipeline, а показать ошибку 'Claude CLI requires login' в GUI с инструкцией.

This commit is contained in:
Gros Frumos 2026-03-16 15:48:09 +02:00
parent a80679ae72
commit bfc8f1c0bb
18 changed files with 1390 additions and 57 deletions

View file

@ -2,11 +2,13 @@
import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
# Patch DB_PATH before importing app
import web.api as api_module
@pytest.fixture
def client(tmp_path):
db_path = tmp_path / "test.db"
@ -224,6 +226,30 @@ def test_run_not_found(client):
assert r.status_code == 404
def test_run_returns_503_when_claude_not_authenticated(client):
"""KIN-083: /run возвращает 503 с claude_auth_required если claude не залогинен."""
from agents.runner import ClaudeAuthError
with patch("agents.runner.check_claude_auth", side_effect=ClaudeAuthError("Claude CLI requires login. Run: claude login")):
r = client.post("/api/tasks/P1-001/run")
assert r.status_code == 503
body = r.json()
assert body["detail"]["error"] == "claude_auth_required"
assert body["detail"]["instructions"] == "Run: claude login"
assert "login" in body["detail"]["message"].lower()
def test_start_phase_returns_503_when_claude_not_authenticated(client):
"""KIN-083: /phases/start возвращает 503 с claude_auth_required если claude не залогинен."""
from agents.runner import ClaudeAuthError
with patch("agents.runner.check_claude_auth", side_effect=ClaudeAuthError("Claude CLI requires login. Run: claude login")):
r = client.post("/api/projects/p1/phases/start")
assert r.status_code == 503
body = r.json()
assert body["detail"]["error"] == "claude_auth_required"
assert body["detail"]["instructions"] == "Run: claude login"
assert "login" in body["detail"]["message"].lower()
def test_run_kin_038_without_allow_write(client):
"""Регрессионный тест KIN-038: allow_write удалён из схемы,
эндпоинт принимает запросы с пустым телом без этого параметра."""
@ -1583,3 +1609,89 @@ def test_kin_arch_003_deploy_operations_project_null_path_uses_cwd_none(client):
assert call_kwargs.get("cwd") is None, (
"KIN-ARCH-003: для operations-проектов без path, cwd должен быть None"
)
# ---------------------------------------------------------------------------
# Bootstrap endpoint — KIN-081
# ---------------------------------------------------------------------------
@pytest.fixture
def bootstrap_client(tmp_path):
"""TestClient без seed-данных, с отдельным DB_PATH."""
db_path = tmp_path / "bs_test.db"
api_module.DB_PATH = db_path
from web.api import app
return TestClient(app), tmp_path
def test_bootstrap_endpoint_invalid_path_returns_400(bootstrap_client):
"""KIN-081: bootstrap возвращает 400 если путь не существует."""
client, _ = bootstrap_client
r = client.post("/api/bootstrap", json={
"id": "newproj", "name": "New Project", "path": "/nonexistent/path/that/does/not/exist"
})
assert r.status_code == 400
assert "not a directory" in r.json()["detail"].lower()
def test_bootstrap_endpoint_duplicate_id_returns_409(bootstrap_client, tmp_path):
"""KIN-081: bootstrap возвращает 409 если проект с таким ID уже существует."""
client, _ = bootstrap_client
proj_dir = tmp_path / "myproj"
proj_dir.mkdir()
# Create project first
client.post("/api/projects", json={"id": "existing", "name": "Existing", "path": str(proj_dir)})
# Try bootstrap with same ID
r = client.post("/api/bootstrap", json={
"id": "existing", "name": "Same ID", "path": str(proj_dir)
})
assert r.status_code == 409
assert "already exists" in r.json()["detail"]
def test_bootstrap_endpoint_rollback_on_save_error(bootstrap_client, tmp_path):
"""KIN-081: при ошибке в save_to_db проект удаляется (rollback), возвращается 500."""
client, _ = bootstrap_client
proj_dir = tmp_path / "rollbackproj"
proj_dir.mkdir()
from core.db import init_db
from core import models as _models
def _save_create_then_fail(conn, project_id, name, path, *args, **kwargs):
# Simulate partial write: project row created, then error
_models.create_project(conn, project_id, name, path)
raise RuntimeError("simulated DB error after project created")
with patch("web.api.save_to_db", side_effect=_save_create_then_fail):
r = client.post("/api/bootstrap", json={
"id": "rollbackproj", "name": "Rollback Test", "path": str(proj_dir)
})
assert r.status_code == 500
assert "Bootstrap failed" in r.json()["detail"]
# Project must NOT remain in DB (rollback was executed)
conn = init_db(api_module.DB_PATH)
assert _models.get_project(conn, "rollbackproj") is None
conn.close()
def test_bootstrap_endpoint_success(bootstrap_client, tmp_path):
"""KIN-081: успешный bootstrap возвращает 200 с project и counts."""
client, _ = bootstrap_client
proj_dir = tmp_path / "goodproj"
proj_dir.mkdir()
(proj_dir / "requirements.txt").write_text("fastapi\n")
with patch("web.api.find_vault_root", return_value=None):
r = client.post("/api/bootstrap", json={
"id": "goodproj", "name": "Good Project", "path": str(proj_dir)
})
assert r.status_code == 200
data = r.json()
assert data["project"]["id"] == "goodproj"
assert "modules_count" in data
assert "decisions_count" in data
assert "tasks_count" in data