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:
parent
a80679ae72
commit
bfc8f1c0bb
18 changed files with 1390 additions and 57 deletions
|
|
@ -540,6 +540,7 @@ class TestKIN052RebuildFrontendCommand:
|
|||
"""Хук должен сохраняться в файловой БД и быть доступен после пересоздания соединения.
|
||||
|
||||
Симулирует рестарт: создаём хук, закрываем соединение, открываем новое — хук на месте.
|
||||
Используем проект НЕ 'kin', чтобы _seed_default_hooks не мигрировал хук.
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
|
|
@ -547,16 +548,17 @@ class TestKIN052RebuildFrontendCommand:
|
|||
# Первое соединение — создаём проект и хук
|
||||
conn1 = init_db(db_path)
|
||||
from core import models as _models
|
||||
_models.create_project(conn1, "kin", "Kin", "/projects/kin", tech_stack=["vue3"])
|
||||
cmd = "cd /Users/grosfrumos/projects/kin/web/frontend && npm run build"
|
||||
hook = create_hook(conn1, "kin", "rebuild-frontend", "pipeline_completed", cmd,
|
||||
_models.create_project(conn1, "kin-test", "KinTest", "/projects/kin-test",
|
||||
tech_stack=["vue3"])
|
||||
cmd = "cd /projects/kin-test/web/frontend && npm run build"
|
||||
hook = create_hook(conn1, "kin-test", "rebuild-frontend", "pipeline_completed", cmd,
|
||||
trigger_module_path=None)
|
||||
hook_id = hook["id"]
|
||||
conn1.close()
|
||||
|
||||
# Второе соединение — «рестарт», хук должен быть на месте
|
||||
conn2 = init_db(db_path)
|
||||
hooks = get_hooks(conn2, "kin", event="pipeline_completed", enabled_only=True)
|
||||
hooks = get_hooks(conn2, "kin-test", event="pipeline_completed", enabled_only=True)
|
||||
conn2.close()
|
||||
|
||||
assert len(hooks) == 1, "После пересоздания соединения хук должен оставаться в БД"
|
||||
|
|
@ -595,6 +597,7 @@ class TestKIN053SeedDefaultHooks:
|
|||
"""_seed_default_hooks создаёт rebuild-frontend хук при наличии проекта 'kin'.
|
||||
|
||||
Порядок: init_db → create_project('kin') → повторный init_db → хук есть.
|
||||
KIN-003: команда теперь scripts/rebuild-frontend.sh, не cd && npm run build.
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
|
|
@ -609,28 +612,33 @@ class TestKIN053SeedDefaultHooks:
|
|||
|
||||
assert len(hooks) == 1
|
||||
assert hooks[0]["name"] == "rebuild-frontend"
|
||||
assert "npm run build" in hooks[0]["command"]
|
||||
assert "web/frontend" in hooks[0]["command"]
|
||||
assert "rebuild-frontend.sh" in hooks[0]["command"]
|
||||
finally:
|
||||
os.unlink(db_path)
|
||||
|
||||
def test_seed_hook_has_correct_command(self):
|
||||
"""Команда хука — точная строка с cd && npm run build."""
|
||||
"""Команда хука использует динамический путь из projects.path (KIN-BIZ-004).
|
||||
|
||||
KIN-003: хук мигрирован на скрипт scripts/rebuild-frontend.sh
|
||||
с trigger_module_path='web/frontend/*' для точного git-фильтра.
|
||||
KIN-BIZ-004: путь берётся из projects.path, не захардкожен.
|
||||
"""
|
||||
project_path = "/projects/kin"
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
conn1 = init_db(db_path)
|
||||
models.create_project(conn1, "kin", "Kin", "/projects/kin")
|
||||
models.create_project(conn1, "kin", "Kin", project_path)
|
||||
conn1.close()
|
||||
|
||||
conn2 = init_db(db_path)
|
||||
hooks = get_hooks(conn2, "kin", event="pipeline_completed", enabled_only=False)
|
||||
conn2.close()
|
||||
|
||||
assert hooks[0]["command"] == (
|
||||
"cd /Users/grosfrumos/projects/kin/web/frontend && npm run build"
|
||||
)
|
||||
assert hooks[0]["trigger_module_path"] is None
|
||||
assert hooks[0]["command"] == f"{project_path}/scripts/rebuild-frontend.sh"
|
||||
assert hooks[0]["trigger_module_path"] == "web/frontend/*"
|
||||
assert hooks[0]["working_dir"] == project_path
|
||||
assert hooks[0]["timeout_seconds"] == 300
|
||||
finally:
|
||||
os.unlink(db_path)
|
||||
|
||||
|
|
@ -672,3 +680,225 @@ class TestKIN053SeedDefaultHooks:
|
|||
assert other_hooks == []
|
||||
finally:
|
||||
os.unlink(db_path)
|
||||
|
||||
def test_seed_hook_migration_updates_existing_hook(self):
|
||||
"""_seed_default_hooks мигрирует существующий хук используя динамический путь (KIN-BIZ-004).
|
||||
|
||||
Если rebuild-frontend уже существует со старой командой (cd && npm run build),
|
||||
повторный init_db должен обновить его на scripts/rebuild-frontend.sh
|
||||
с путём из projects.path.
|
||||
"""
|
||||
project_path = "/projects/kin"
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
conn1 = init_db(db_path)
|
||||
models.create_project(conn1, "kin", "Kin", project_path)
|
||||
# Вставляем старый хук вручную (имитация состояния до KIN-003)
|
||||
old_cmd = f"cd {project_path}/web/frontend && npm run build"
|
||||
conn1.execute(
|
||||
"""INSERT INTO hooks (project_id, name, event, trigger_module_path, command,
|
||||
working_dir, timeout_seconds, enabled)
|
||||
VALUES ('kin', 'rebuild-frontend', 'pipeline_completed',
|
||||
NULL, ?, NULL, 120, 1)""",
|
||||
(old_cmd,),
|
||||
)
|
||||
conn1.commit()
|
||||
conn1.close()
|
||||
|
||||
# Повторный init_db запускает _seed_default_hooks с миграцией
|
||||
conn2 = init_db(db_path)
|
||||
hooks = get_hooks(conn2, "kin", event="pipeline_completed", enabled_only=False)
|
||||
conn2.close()
|
||||
|
||||
assert len(hooks) == 1
|
||||
assert hooks[0]["command"] == f"{project_path}/scripts/rebuild-frontend.sh"
|
||||
assert hooks[0]["trigger_module_path"] == "web/frontend/*"
|
||||
assert hooks[0]["working_dir"] == project_path
|
||||
assert hooks[0]["timeout_seconds"] == 300
|
||||
finally:
|
||||
os.unlink(db_path)
|
||||
|
||||
def test_seed_hook_uses_dynamic_path_not_hardcoded(self):
|
||||
"""Команда хука содержит путь из projects.path, а не захардкоженный /Users/grosfrumos/... (KIN-BIZ-004).
|
||||
|
||||
Создаём проект с нестандартным путём и проверяем,
|
||||
что хук использует именно этот путь.
|
||||
"""
|
||||
custom_path = "/srv/custom/kin-deployment"
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
try:
|
||||
conn1 = init_db(db_path)
|
||||
models.create_project(conn1, "kin", "Kin", custom_path)
|
||||
conn1.close()
|
||||
|
||||
conn2 = init_db(db_path)
|
||||
hooks = get_hooks(conn2, "kin", event="pipeline_completed", enabled_only=False)
|
||||
conn2.close()
|
||||
|
||||
assert len(hooks) == 1
|
||||
assert hooks[0]["command"] == f"{custom_path}/scripts/rebuild-frontend.sh", (
|
||||
"Команда должна использовать путь из projects.path, не захардкоженный"
|
||||
)
|
||||
assert hooks[0]["working_dir"] == custom_path, (
|
||||
"working_dir должен совпадать с projects.path"
|
||||
)
|
||||
assert "/Users/grosfrumos" not in hooks[0]["command"], (
|
||||
"Захардкоженный путь /Users/grosfrumos не должен присутствовать в команде"
|
||||
)
|
||||
finally:
|
||||
os.unlink(db_path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# KIN-003: changed_files — точный git-фильтр для trigger_module_path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestChangedFilesMatching:
|
||||
"""Тесты для нового параметра changed_files в run_hooks() (KIN-003).
|
||||
|
||||
Когда changed_files передан — trigger_module_path матчится по реальным
|
||||
git-изменённым файлам, а не по task_modules из БД.
|
||||
"""
|
||||
|
||||
def _make_proc(self, returncode=0, stdout="ok", stderr=""):
|
||||
m = MagicMock()
|
||||
m.returncode = returncode
|
||||
m.stdout = stdout
|
||||
m.stderr = stderr
|
||||
return m
|
||||
|
||||
@pytest.fixture
|
||||
def frontend_trigger_hook(self, conn):
|
||||
"""Хук с trigger_module_path='web/frontend/*'."""
|
||||
return create_hook(
|
||||
conn, "vdol", "rebuild-frontend", "pipeline_completed",
|
||||
"scripts/rebuild-frontend.sh",
|
||||
trigger_module_path="web/frontend/*",
|
||||
working_dir="/tmp",
|
||||
)
|
||||
|
||||
@patch("core.hooks.subprocess.run")
|
||||
def test_hook_fires_when_frontend_file_in_changed_files(
|
||||
self, mock_run, conn, frontend_trigger_hook
|
||||
):
|
||||
"""Хук срабатывает, если среди changed_files есть файл в web/frontend/."""
|
||||
mock_run.return_value = self._make_proc()
|
||||
results = run_hooks(
|
||||
conn, "vdol", "VDOL-001",
|
||||
event="pipeline_completed",
|
||||
task_modules=[],
|
||||
changed_files=["web/frontend/App.vue", "core/models.py"],
|
||||
)
|
||||
assert len(results) == 1
|
||||
assert results[0].name == "rebuild-frontend"
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch("core.hooks.subprocess.run")
|
||||
def test_hook_skipped_when_no_frontend_file_in_changed_files(
|
||||
self, mock_run, conn, frontend_trigger_hook
|
||||
):
|
||||
"""Хук НЕ срабатывает, если changed_files не содержит web/frontend/* файлов."""
|
||||
mock_run.return_value = self._make_proc()
|
||||
results = run_hooks(
|
||||
conn, "vdol", "VDOL-001",
|
||||
event="pipeline_completed",
|
||||
task_modules=[],
|
||||
changed_files=["core/models.py", "web/api.py", "agents/runner.py"],
|
||||
)
|
||||
assert len(results) == 0
|
||||
mock_run.assert_not_called()
|
||||
|
||||
@patch("core.hooks.subprocess.run")
|
||||
def test_hook_skipped_when_changed_files_is_empty_list(
|
||||
self, mock_run, conn, frontend_trigger_hook
|
||||
):
|
||||
"""Пустой changed_files [] — хук с trigger_module_path не срабатывает."""
|
||||
mock_run.return_value = self._make_proc()
|
||||
results = run_hooks(
|
||||
conn, "vdol", "VDOL-001",
|
||||
event="pipeline_completed",
|
||||
task_modules=[{"path": "web/frontend/App.vue", "name": "App"}],
|
||||
changed_files=[], # git говорит: ничего не изменилось
|
||||
)
|
||||
assert len(results) == 0
|
||||
mock_run.assert_not_called()
|
||||
|
||||
@patch("core.hooks.subprocess.run")
|
||||
def test_changed_files_overrides_task_modules_match(
|
||||
self, mock_run, conn, frontend_trigger_hook
|
||||
):
|
||||
"""Если changed_files передан, task_modules игнорируется для фильтрации.
|
||||
|
||||
task_modules содержит frontend-файл, но changed_files — нет.
|
||||
Хук не должен сработать: changed_files имеет приоритет.
|
||||
"""
|
||||
mock_run.return_value = self._make_proc()
|
||||
results = run_hooks(
|
||||
conn, "vdol", "VDOL-001",
|
||||
event="pipeline_completed",
|
||||
task_modules=[{"path": "web/frontend/App.vue", "name": "App"}],
|
||||
changed_files=["core/models.py"], # нет frontend-файлов
|
||||
)
|
||||
assert len(results) == 0, (
|
||||
"changed_files должен иметь приоритет над task_modules"
|
||||
)
|
||||
mock_run.assert_not_called()
|
||||
|
||||
@patch("core.hooks.subprocess.run")
|
||||
def test_fallback_to_task_modules_when_changed_files_is_none(
|
||||
self, mock_run, conn, frontend_trigger_hook
|
||||
):
|
||||
"""Если changed_files=None — используется старое поведение через task_modules."""
|
||||
mock_run.return_value = self._make_proc()
|
||||
results = run_hooks(
|
||||
conn, "vdol", "VDOL-001",
|
||||
event="pipeline_completed",
|
||||
task_modules=[{"path": "web/frontend/App.vue", "name": "App"}],
|
||||
changed_files=None, # не передан — fallback
|
||||
)
|
||||
assert len(results) == 1
|
||||
assert results[0].name == "rebuild-frontend"
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch("core.hooks.subprocess.run")
|
||||
def test_hook_without_trigger_fires_regardless_of_changed_files(
|
||||
self, mock_run, conn
|
||||
):
|
||||
"""Хук без trigger_module_path всегда срабатывает, даже если changed_files=[].
|
||||
|
||||
Используется для хуков, которые должны запускаться после каждого pipeline.
|
||||
"""
|
||||
mock_run.return_value = self._make_proc()
|
||||
create_hook(
|
||||
conn, "vdol", "always-run", "pipeline_completed",
|
||||
"echo always",
|
||||
trigger_module_path=None,
|
||||
working_dir="/tmp",
|
||||
)
|
||||
results = run_hooks(
|
||||
conn, "vdol", "VDOL-001",
|
||||
event="pipeline_completed",
|
||||
task_modules=[],
|
||||
changed_files=[], # пусто — но хук без фильтра всегда запустится
|
||||
)
|
||||
assert len(results) == 1
|
||||
assert results[0].name == "always-run"
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch("core.hooks.subprocess.run")
|
||||
def test_deep_frontend_path_matches_glob(
|
||||
self, mock_run, conn, frontend_trigger_hook
|
||||
):
|
||||
"""Вложенные пути web/frontend/src/components/Foo.vue матчатся по 'web/frontend/*'."""
|
||||
mock_run.return_value = self._make_proc()
|
||||
results = run_hooks(
|
||||
conn, "vdol", "VDOL-001",
|
||||
event="pipeline_completed",
|
||||
task_modules=[],
|
||||
changed_files=["web/frontend/src/components/TaskCard.vue"],
|
||||
)
|
||||
assert len(results) == 1, (
|
||||
"fnmatch должен рекурсивно матчить 'web/frontend/*' на вложенные пути"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue