kin: auto-commit after pipeline

This commit is contained in:
Gros Frumos 2026-03-19 21:56:57 +02:00
parent 3274ca0f98
commit 4c01e0e4ee

View file

@ -89,3 +89,160 @@ class TestKIN122RebuildFrontendNpmInstall:
assert len(build_lines) >= 1, ( assert len(build_lines) >= 1, (
"npm run build должен быть безусловным вызовом" "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})"
)