From ac8a0e9fc6357023fc5a14dc6b35790d8ab2c374 Mon Sep 17 00:00:00 2001 From: migatu Date: Tue, 11 Aug 2026 11:26:42 +0200 Subject: [PATCH] =?UTF-8?q?fix(dane):=20trzy=20b=C5=82=C4=99dy=20styku=20r?= =?UTF-8?q?ejestru=20plik=C3=B3w=20z=20reszt=C4=85=20warstwy=20(DAN-27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Po wdrożeniu DAN-27 przestało działać wyszukiwanie (500 z /search) i ekran Ustawienia (502 z /bases). Ekran „Pliki" działał, co dobrze pokazuje, gdzie leżał problem: nie w rejestrze, tylko w JEGO STYKU z kodem, który zastąpił. 1. NameError przy KAŻDYM wyszukiwaniu. Przepisując `_enabled_files` pod rejestr usunąłem lokalny `from app import bases`, a modułowego w tym pliku nigdy nie było. Wołanie bases.disabled_entries() wywracało się natychmiast. 2. KeyError na /bases. Rejestr oddawał `in_use`, a endpoint liczy `b["enabled"]` — tak samo warstwa logiczna i szablon Ustawień (DAN-15/PRE-09). Rejestr wszedł w miejsce starej listy baz, więc musi mówić jej językiem; oddaje teraz oba pola o tej samej wartości. 3. Cache podawany jako baza. `_scan` filtrował tylko nazwę PLIKU, więc zawartość `.cache` wchodziła do rejestru (pliki w środku nie zaczynają się od kropki), a przy pierwszym uruchomieniu była jeszcze przyjmowana jako aktywna. Teraz pomijamy wszystko, co leży w ukrytym KATALOGU. Ten wyszedł dopiero z nowych testów — nie wiedziałem o nim. DLACZEGO TESTY TEGO NIE ZŁAPAŁY. test_files.py sprawdza rejestr w IZOLACJI i był zielony, podczas gdy produkcja leżała. Groźne w takiej podmianie nie jest to, co nowy moduł robi w środku, tylko czy mówi tym samym językiem, co jego odbiorcy. Doszedł więc test_rejestr_integracja.py: wyszukiwanie przez dostawcę (obie gałęzie, także ta z DISABLED_BASES), kontrakt pól listy baz, endpoint /bases przez trasę oraz odstawienie bazy widziane JEDNOCZEŚNIE w wyszukiwaniu i w liczniku. Co-Authored-By: Claude Opus 5 --- services/data/app/files.py | 18 ++- services/data/app/providers/excel_provider.py | 5 +- .../data/tests/test_rejestr_integracja.py | 112 ++++++++++++++++++ 3 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 services/data/tests/test_rejestr_integracja.py diff --git a/services/data/app/files.py b/services/data/app/files.py index 1b97450..204be8f 100644 --- a/services/data/app/files.py +++ b/services/data/app/files.py @@ -198,8 +198,18 @@ def _inspect_workbook(path: Path, required: list[str], min_rows: int) -> list[st # ── rejestr ────────────────────────────────────────────────────────────── def _scan(root: Path) -> list[Path]: + """Pliki na udziale, bez śmieci technicznych. + + Pomijamy nie tylko ukryte PLIKI, ale i wszystko, co leży w ukrytym KATALOGU: + filtr po samej nazwie pliku wciągał do rejestru zawartość `.cache`, bo pliki + w środku nie zaczynają się od kropki. Efekt: cache podawany jako baza, a przy + pierwszym uruchomieniu jeszcze przyjmowany jako aktywny.""" + def ukryta_sciezka(p: Path) -> bool: + return any(part.startswith(".") for part in p.relative_to(root).parts[:-1]) + return [p for p in sorted(root.glob("**/*")) - if p.is_file() and not p.name.startswith((".", "~$"))] + if p.is_file() and not p.name.startswith((".", "~$")) + and not ukryta_sciezka(p)] def _adopt_existing(root: Path) -> dict: @@ -252,6 +262,12 @@ def registry(root: Path | str, *, for_admin: bool = False) -> list[dict]: entry = { "name": p.name, "path": rel, "size_mb": size_mb, "modified": modified, "status": status, "in_use": status in USABLE, + # `enabled` to TA SAMA informacja pod nazwą, której używa reszta + # świata: endpoint /bases, warstwa logiczna i ekran „Ustawienia" + # (DAN-15/PRE-09). Rejestr wszedł w miejsce starej listy baz, więc + # musi mówić jej językiem — inaczej każdy odbiorca dostaje KeyError, + # a to była właśnie awaria /bases po wdrożeniu DAN-27. + "enabled": status in USABLE, "archived_at": row.get("archived_at") or "", "uploaded_at": row.get("uploaded_at") or "", "uploaded_by": row.get("uploaded_by") or "", diff --git a/services/data/app/providers/excel_provider.py b/services/data/app/providers/excel_provider.py index c44e96c..8bb6dec 100644 --- a/services/data/app/providers/excel_provider.py +++ b/services/data/app/providers/excel_provider.py @@ -100,7 +100,10 @@ class ExcelDataProvider(DataProvider): odwrotnie — inaczej ktoś z dostępem do ekranu mógłby włączyć bazę wyłączoną świadomie na poziomie wdrożenia. """ - from app import files + # `bases` MUSI być zaimportowane tutaj — modułowego importu nie ma, + # a przepisując tę funkcję pod rejestr usunąłem lokalny. Efekt: NameError + # przy KAŻDYM wyszukiwaniu, czyli 500 z warstwy danych. + from app import bases, files usable = set(files.usable_paths(self.s.excel_dir)) out = [p for p in paths if p in usable] diff --git a/services/data/tests/test_rejestr_integracja.py b/services/data/tests/test_rejestr_integracja.py new file mode 100644 index 0000000..b56069d --- /dev/null +++ b/services/data/tests/test_rejestr_integracja.py @@ -0,0 +1,112 @@ +"""Rejestr plików a RESZTA warstwy danych — punkty styku (DAN-27). + +DLACZEGO OSOBNY PLIK. test_files.py sprawdza sam rejestr w izolacji i przechodził +na zielono, podczas gdy na produkcji leżało wyszukiwanie (500) i lista baz (502). +Rejestr wszedł w miejsce starego mechanizmu włączania baz, więc groźne jest nie +to, co robi w środku, tylko czy MÓWI TYM SAMYM JĘZYKIEM, co jego odbiorcy. + +Oba tamte błędy były jednolinijkowe i oba niewidoczne dla testów jednostkowych: + * NameError, bo przepisując `_enabled_files` usunąłem lokalny import `bases`, + * KeyError, bo rejestr oddawał `in_use`, a endpoint /bases czytał `enabled`. +""" +import pathlib + +import pytest + + +@pytest.fixture() +def excel_dir(tmp_path, monkeypatch): + monkeypatch.setenv("EXCEL_DIR", str(tmp_path)) + monkeypatch.setenv("CACHE_DIR", str(tmp_path / ".cache")) + monkeypatch.delenv("DISABLED_BASES", raising=False) + monkeypatch.delenv("FILES_STATE", raising=False) + return tmp_path + + +def _xlsx(path, rows=(("Ma Ari", "odważny"), ("Ve Tau", "zgodny"))): + import openpyxl + + wb = openpyxl.Workbook() + ws = wb.active + ws.append(["significator", "effect"]) + for r in rows: + ws.append(list(r)) + wb.save(path) + return path + + +def _provider(excel_dir): + from app.config import Settings + from app.providers.excel_provider import ExcelDataProvider + + return ExcelDataProvider(Settings()) + + +def test_search_does_not_explode_on_the_registry(excel_dir): + """Regresja: `_enabled_files` wołało bases.disabled_entries() bez importu, + więc KAŻDE wyszukiwanie kończyło się NameError → 500 z warstwy danych.""" + from app.models import SearchQuery + + _xlsx(excel_dir / "baza.xlsx") + p = _provider(excel_dir) + p.warmup() + out = p.search(SearchQuery(key="significator", value="Ma Ari", exact=False, limit=10)) + assert out.total >= 1, "zastana baza nie weszła do wyszukiwania" + + +def test_search_still_works_with_disabled_bases_set(excel_dir, monkeypatch): + """DISABLED_BASES zostaje jako awaryjne wyłączenie i ma odsiewać DODATKOWO — + to właśnie ta gałąź kodu wywalała się na braku importu.""" + from app.models import SearchQuery + + _xlsx(excel_dir / "a.xlsx") + _xlsx(excel_dir / "b.xlsx") + monkeypatch.setenv("DISABLED_BASES", "b.xlsx") + p = _provider(excel_dir) + p.warmup() + out = p.search(SearchQuery(key="significator", value="Ma Ari", exact=False, limit=10)) + assert out.total >= 1 + + +def test_registry_speaks_the_language_the_bases_endpoint_reads(excel_dir): + """Regresja: endpoint /bases liczy `b["enabled"]`, rejestr oddawał `in_use`. + KeyError → 500 z danych → 502 z logiki → „Warstwa logiczna niedostępna" + na ekranie Ustawienia.""" + _xlsx(excel_dir / "baza.xlsx") + items = _provider(excel_dir).list_bases() + assert items, "lista baz jest pusta" + for row in items: + for key in ("name", "path", "enabled", "in_use", "size_mb", "modified"): + assert key in row, f"brak pola `{key}` — odbiorca dostanie KeyError" + assert row["enabled"] == row["in_use"], "dwa pola, jedna prawda" + + +def test_bases_endpoint_answers_end_to_end(excel_dir): + """Przez TRASĘ, nie przez dostawcę: to ona wywalała się na produkcji.""" + _xlsx(excel_dir / "baza.xlsx") + import importlib + + from app import main as data_main + + importlib.reload(data_main) + body = data_main.bases() + assert body["total"] == 1 + assert body["enabled"] == 1, "zastana baza powinna być włączona po adopcji" + assert body["bases"][0]["name"] == "baza.xlsx" + + +def test_switching_a_base_off_is_visible_in_both_places(excel_dir): + """Odstawienie bazy ma zniknąć i z wyszukiwania, i z licznika na Ustawieniach.""" + from app import files + from app.models import SearchQuery + + _xlsx(excel_dir / "baza.xlsx") + p = _provider(excel_dir) + p.warmup() + assert p.search(SearchQuery(key="significator", value="Ma Ari", limit=10)).total >= 1 + + files.set_status(excel_dir, "baza.xlsx", files.READY) + p2 = _provider(excel_dir) + p2.warmup() + assert p2.search(SearchQuery(key="significator", value="Ma Ari", limit=10)).total == 0 + assert [b["enabled"] for b in p2.list_bases()] == [False]