From 4c01e0e4eedfd9d0adf8f3cf68ae3c8d056eee5a Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Thu, 19 Mar 2026 21:56:57 +0200 Subject: [PATCH] kin: auto-commit after pipeline --- tests/test_kin_122_regression.py | 157 +++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/tests/test_kin_122_regression.py b/tests/test_kin_122_regression.py index a3352cc..acf3507 100644 --- a/tests/test_kin_122_regression.py +++ b/tests/test_kin_122_regression.py @@ -89,3 +89,160 @@ class TestKIN122RebuildFrontendNpmInstall: assert len(build_lines) >= 1, ( "npm run build должен быть безусловным вызовом" ) + + +# --------------------------------------------------------------------------- +# Функциональные тесты: реальный запуск скрипта с mock npm +# Покрывают acceptance criteria KIN-122: +# 1. npm install срабатывает автоматически если package.json изменился +# 2. npm install НЕ запускается если package.json не менялся +# --------------------------------------------------------------------------- + +@pytest.fixture() +def fake_frontend_env(tmp_path): + """ + Создаёт временную структуру директорий, копирует скрипт туда, + подкладывает mock npm который пишет свои вызовы в лог-файл. + + Структура: + tmp_path/ + scripts/rebuild-frontend.sh ← копия скрипта + web/frontend/ ← FRONTEND_DIR + bin/npm ← mock npm (логирует вызовы) + npm_calls.log ← лог вызовов (создаётся mock npm) + """ + scripts_dir = tmp_path / "scripts" + scripts_dir.mkdir() + + frontend_dir = tmp_path / "web" / "frontend" + frontend_dir.mkdir(parents=True) + + (frontend_dir / "package.json").write_text('{"name": "test-kin-122", "scripts": {"build": "echo build"}}') + + # Копируем скрипт в temp-окружение + script_copy = scripts_dir / "rebuild-frontend.sh" + shutil.copy(SCRIPT_PATH, script_copy) + script_copy.chmod(0o755) + + # Mock npm: записывает все свои аргументы в лог-файл + calls_log = tmp_path / "npm_calls.log" + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + mock_npm = bin_dir / "npm" + mock_npm.write_text( + f'#!/bin/bash\necho "$@" >> "{calls_log}"\n' + ) + mock_npm.chmod(0o755) + + return { + "script": script_copy, + "frontend_dir": frontend_dir, + "bin_dir": bin_dir, + "calls_log": calls_log, + } + + +def _run_script(env_data): + """Запускает rebuild-frontend.sh с mock npm в PATH.""" + result = subprocess.run( + [str(env_data["script"])], + env={**os.environ, "PATH": f"{env_data['bin_dir']}:{os.environ.get('PATH', '')}"}, + capture_output=True, + text=True, + ) + return result + + +def _get_npm_calls(env_data): + """Возвращает список аргументов из всех вызовов mock npm.""" + log = env_data["calls_log"] + if not log.exists(): + return [] + return log.read_text().strip().splitlines() + + +class TestKIN122NpmInstallBehaviorFunctional: + """ + Функциональные тесты: реальный запуск скрипта в изолированном окружении. + Acceptance criteria: хук срабатывает автоматически без ручного вмешательства, + не замедляет pipeline если package.json не менялся. + """ + + def test_npm_install_runs_when_node_modules_missing(self, fake_frontend_env): + """AC-1: npm install вызывается автоматически если node_modules отсутствует.""" + # node_modules не создан — условие ! -d node_modules истинно + _run_script(fake_frontend_env) + calls = _get_npm_calls(fake_frontend_env) + assert any("install" in c for c in calls), ( + f"npm install должен вызваться при отсутствии node_modules. Вызовы: {calls}" + ) + + def test_npm_install_runs_when_package_json_newer_than_node_modules(self, fake_frontend_env): + """AC-1: npm install вызывается если package.json новее node_modules (изменились зависимости).""" + frontend = fake_frontend_env["frontend_dir"] + + # Создаём node_modules со старым mtime + node_modules = frontend / "node_modules" + node_modules.mkdir() + old_time = time.time() - 200 + os.utime(node_modules, (old_time, old_time)) + + # package.json уже имеет текущий mtime — он новее node_modules + _run_script(fake_frontend_env) + calls = _get_npm_calls(fake_frontend_env) + assert any("install" in c for c in calls), ( + f"npm install должен вызваться если package.json новее node_modules. Вызовы: {calls}" + ) + + def test_npm_install_skipped_when_node_modules_up_to_date(self, fake_frontend_env): + """AC-2: npm install НЕ вызывается если node_modules актуален (package.json не менялся).""" + frontend = fake_frontend_env["frontend_dir"] + + # Создаём node_modules, затем делаем package.json старше + node_modules = frontend / "node_modules" + node_modules.mkdir() + + # node_modules имеет текущее время, package.json — старое + old_time = time.time() - 200 + package_json = frontend / "package.json" + os.utime(package_json, (old_time, old_time)) + + _run_script(fake_frontend_env) + calls = _get_npm_calls(fake_frontend_env) + assert not any("install" in c for c in calls), ( + f"npm install НЕ должен вызываться если node_modules актуален. Вызовы: {calls}" + ) + + def test_npm_run_build_always_runs_regardless_of_install(self, fake_frontend_env): + """AC-2: npm run build всегда вызывается — pipeline не блокируется если npm install пропущен.""" + frontend = fake_frontend_env["frontend_dir"] + + # Сценарий: node_modules актуален, npm install не нужен + node_modules = frontend / "node_modules" + node_modules.mkdir() + old_time = time.time() - 200 + os.utime(frontend / "package.json", (old_time, old_time)) + + _run_script(fake_frontend_env) + calls = _get_npm_calls(fake_frontend_env) + assert any("run" in c and "build" in c for c in calls), ( + f"npm run build должен вызываться всегда, даже без npm install. Вызовы: {calls}" + ) + + def test_npm_run_build_runs_after_npm_install(self, fake_frontend_env): + """AC-1: npm run build запускается после npm install (оба вызова в правильном порядке).""" + # node_modules отсутствует — сработают оба: npm install и npm run build + _run_script(fake_frontend_env) + calls = _get_npm_calls(fake_frontend_env) + + install_calls = [c for c in calls if "install" in c] + build_calls = [c for c in calls if "run" in c and "build" in c] + + assert install_calls, f"npm install не найден в вызовах: {calls}" + assert build_calls, f"npm run build не найден в вызовах: {calls}" + + install_idx = calls.index(install_calls[0]) + build_idx = calls.index(build_calls[0]) + assert install_idx < build_idx, ( + f"npm install (pos {install_idx}) должен стоять перед npm run build (pos {build_idx})" + )