fix(dane): trzy błędy styku rejestru plików z resztą warstwy (DAN-27)
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m29s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Failing after 4m53s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m27s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 7s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
build / build (push) Successful in 6s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m28s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 4m52s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m24s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 7s
Testy / Kontrola składni wszystkich warstw (push) Successful in 5s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m29s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Failing after 4m53s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m27s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 7s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
build / build (push) Successful in 6s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m28s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 4m52s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m24s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 7s
Testy / Kontrola składni wszystkich warstw (push) Successful in 5s
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 <noreply@anthropic.com>
This commit was merged in pull request #72.
This commit is contained in:
@@ -198,8 +198,18 @@ def _inspect_workbook(path: Path, required: list[str], min_rows: int) -> list[st
|
|||||||
# ── rejestr ──────────────────────────────────────────────────────────────
|
# ── rejestr ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _scan(root: Path) -> list[Path]:
|
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("**/*"))
|
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:
|
def _adopt_existing(root: Path) -> dict:
|
||||||
@@ -252,6 +262,12 @@ def registry(root: Path | str, *, for_admin: bool = False) -> list[dict]:
|
|||||||
entry = {
|
entry = {
|
||||||
"name": p.name, "path": rel, "size_mb": size_mb, "modified": modified,
|
"name": p.name, "path": rel, "size_mb": size_mb, "modified": modified,
|
||||||
"status": status, "in_use": status in USABLE,
|
"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 "",
|
"archived_at": row.get("archived_at") or "",
|
||||||
"uploaded_at": row.get("uploaded_at") or "",
|
"uploaded_at": row.get("uploaded_at") or "",
|
||||||
"uploaded_by": row.get("uploaded_by") or "",
|
"uploaded_by": row.get("uploaded_by") or "",
|
||||||
|
|||||||
@@ -100,7 +100,10 @@ class ExcelDataProvider(DataProvider):
|
|||||||
odwrotnie — inaczej ktoś z dostępem do ekranu mógłby włączyć bazę
|
odwrotnie — inaczej ktoś z dostępem do ekranu mógłby włączyć bazę
|
||||||
wyłączoną świadomie na poziomie wdrożenia.
|
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))
|
usable = set(files.usable_paths(self.s.excel_dir))
|
||||||
out = [p for p in paths if p in usable]
|
out = [p for p in paths if p in usable]
|
||||||
|
|||||||
@@ -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]
|
||||||
Reference in New Issue
Block a user