From e12211de618891e15bb2bbaab147492f17ffc37c Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Thu, 19 Mar 2026 13:28:30 +0200 Subject: [PATCH] kin: auto-commit after pipeline --- tests/test_kin_fix_024_regression.py | 154 +++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 tests/test_kin_fix_024_regression.py diff --git a/tests/test_kin_fix_024_regression.py b/tests/test_kin_fix_024_regression.py new file mode 100644 index 0000000..e604a8f --- /dev/null +++ b/tests/test_kin_fix_024_regression.py @@ -0,0 +1,154 @@ +"""Regression tests for KIN-FIX-024 — dist из worktree вручную скопирован в основной проект. + +Acceptance criteria: +- после `git pull` + `npm run build` в main — деплой работает без ручного копирования dist/ + +Проверяем: +(1) DIST path в api.py указывает на web/frontend/dist (относительно api.py) +(2) В проекте нет dist/ в корне (stray artifact) +(3) В web/ нет dist/ вне web/frontend/dist/ (stray artifact) +(4) tsconfig.app.json исключает *.test.ts из сборки +(5) tsconfig.app.json исключает *.spec.ts из сборки +(6) tsconfig.app.json исключает __tests__/ директории +(7) GET / → возвращает index.html из dist/ (SPA root) +(8) GET /some/nested/route → SPA fallback на index.html +(9) GET /favicon.ico → конкретный файл из dist/ (не index.html) +""" + +import json +import re +import pytest +from pathlib import Path + +PROJECT_ROOT = Path(__file__).parent.parent +WEB_API_FILE = PROJECT_ROOT / "web" / "api.py" +TSCONFIG_APP = PROJECT_ROOT / "web" / "frontend" / "tsconfig.app.json" + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture +def spa_client(tmp_path): + """TestClient с изолированной БД и фейковым dist/ для тестов SPA endpoint.""" + import web.api as api_module + + db_path = tmp_path / "test.db" + api_module.DB_PATH = db_path + + # Создаём фейковый dist/ + dist_dir = tmp_path / "dist" + dist_dir.mkdir() + (dist_dir / "index.html").write_text("SPA") + (dist_dir / "favicon.ico").write_bytes(b"ICON_DATA") + + original_dist = api_module.DIST + api_module.DIST = dist_dir + + from web.api import app + from fastapi.testclient import TestClient + client = TestClient(app) + yield client + + api_module.DIST = original_dist + + +# --------------------------------------------------------------------------- +# (1–3) Проверка расположения dist/ в проекте +# --------------------------------------------------------------------------- + +def test_dist_path_points_to_web_frontend_dist(): + """api.py DIST должен резолвиться в web/frontend/dist относительно api.py.""" + import web.api as api_module + expected = (WEB_API_FILE.parent / "frontend" / "dist").resolve() + actual = api_module.DIST.resolve() + assert actual == expected, ( + f"DIST указывает на {actual}, ожидалось {expected}. " + "Возможна ошибка пути — деплой без ручного копирования не будет работать." + ) + + +def test_no_stray_dist_in_project_root(): + """dist/ не должен существовать в корне проекта (признак ручного копирования).""" + root_dist = PROJECT_ROOT / "dist" + assert not root_dist.exists(), ( + f"Обнаружен stray dist/ в корне проекта: {root_dist}. " + "Это признак ручного копирования из worktree." + ) + + +def test_no_stray_dist_in_web_root(): + """dist/ не должен существовать в web/ (только в web/frontend/dist/).""" + web_dist = PROJECT_ROOT / "web" / "dist" + assert not web_dist.exists(), ( + f"Обнаружен stray dist/ в web/: {web_dist}. " + "Правильное место: web/frontend/dist/" + ) + + +# --------------------------------------------------------------------------- +# (4–6) tsconfig.app.json — exclude для тест-файлов +# --------------------------------------------------------------------------- + +def _load_tsconfig_exclude() -> list: + """Парсим tsconfig.app.json как JSONC (может содержать /* */ и // комментарии).""" + content = TSCONFIG_APP.read_text() + # Стрипаем блочные комментарии /* ... */ + content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL) + # Стрипаем строчные комментарии // ... + content = re.sub(r"//[^\n]*", "", content) + data = json.loads(content) + return data.get("exclude", []) + + +def test_tsconfig_app_excludes_test_ts(): + """tsconfig.app.json должен исключать src/**/*.test.ts из сборки.""" + exclude = _load_tsconfig_exclude() + assert any("test.ts" in e for e in exclude), ( + f"tsconfig.app.json не исключает *.test.ts — тест-файлы попадут в сборку. " + f"exclude={exclude}" + ) + + +def test_tsconfig_app_excludes_spec_ts(): + """tsconfig.app.json должен исключать src/**/*.spec.ts из сборки.""" + exclude = _load_tsconfig_exclude() + assert any("spec.ts" in e for e in exclude), ( + f"tsconfig.app.json не исключает *.spec.ts — тест-файлы попадут в сборку. " + f"exclude={exclude}" + ) + + +def test_tsconfig_app_excludes_tests_directory(): + """tsconfig.app.json должен исключать src/**/__tests__/ из сборки.""" + exclude = _load_tsconfig_exclude() + assert any("__tests__" in e for e in exclude), ( + f"tsconfig.app.json не исключает __tests__/ директории. " + f"exclude={exclude}" + ) + + +# --------------------------------------------------------------------------- +# (7–9) SPA endpoint — serve_spa обслуживает из корректного dist/ +# --------------------------------------------------------------------------- + +def test_serve_spa_root_returns_index_html(spa_client): + """GET / → index.html из dist/ (SPA entry point).""" + r = spa_client.get("/") + assert r.status_code == 200 + assert "SPA" in r.text + + +def test_serve_spa_unknown_route_returns_index_html(spa_client): + """GET /projects/kin/tasks → SPA fallback (index.html), не 404.""" + r = spa_client.get("/projects/kin/tasks") + assert r.status_code == 200 + assert "SPA" in r.text + + +def test_serve_spa_existing_file_returned_directly(spa_client): + """GET /favicon.ico → конкретный файл из dist/, а не index.html.""" + r = spa_client.get("/favicon.ico") + assert r.status_code == 200 + assert r.content == b"ICON_DATA"