fix(dane): bazy zastane na udziale zostają w użyciu po przejściu na rejestr
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m57s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m39s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m44s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 40s
Testy / Kontrola składni wszystkich warstw (push) Successful in 16s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 11m26s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Failing after 3h18m22s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 12m9s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 51s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 16s

Bez tego wdrożenie DAN-27 WYŁĄCZYŁOBY WYSZUKIWANIE. Dotąd bazy działały domyślnie
(wyłączało się je jawnie przez DISABLED_BASES). Po przejściu na rejestr plik bez
wpisu w stanie dostaje `ready`, czyli NIE w użyciu — a stan po wdrożeniu jest
pusty. Efekt: żadna baza nie jest aktywna i program nagle niczego nie znajduje.

Ta cicha zmiana zachowania byłaby gorsza od awarii, bo wygląda jak pusta baza,
a nie jak zepsuty deploy — i szukałoby się jej w warstwie danych albo w indeksie.

Teraz brak PLIKU stanu oznacza pierwsze uruchomienie i bazy zastane są przyjmowane
jako aktywne. Pusty słownik przy ISTNIEJĄCYM pliku to co innego: ktoś świadomie
wszystko odstawił, więc nie wskrzeszamy — osobny test tego pilnuje.

Rozróżnienie jest celowe i też pod testem: `ready` dotyczy plików WGRANYCH przez
ekran (te ktoś musi świadomie włączyć), a nie zastanych przy przejściu na rejestr.
Inaczej nowa baza wchodziłaby do wyników sama, bez niczyjej decyzji.

Przyjęcie działa też na udziale tylko do odczytu: zapis stanu wtedy nie przechodzi,
więc powtórzy się przy każdym uruchomieniu — zachowanie to samo, koszt żaden.

Dwa testy opisujące STARE zachowanie zostały poprawione, bo to one były błędne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 11:59:00 +02:00
parent 6466ab89a9
commit a299cdad3c
2 changed files with 84 additions and 6 deletions
+27 -1
View File
@@ -202,6 +202,30 @@ def _scan(root: Path) -> list[Path]:
if p.is_file() and not p.name.startswith((".", "~$"))] if p.is_file() and not p.name.startswith((".", "~$"))]
def _adopt_existing(root: Path) -> dict:
"""Pierwsze uruchomienie: bazy zastane na udziale są OD RAZU w użyciu.
Bez tego wdrożenie DAN-27 wyłączyłoby wyszukiwanie. Dotąd bazy działały
domyślnie (wyłączało się je jawnie przez DISABLED_BASES); po przejściu na
rejestr plik bez wpisu dostaje `ready`, czyli NIE w użyciu — więc pusty stan
po wdrożeniu oznaczałby, że program nagle niczego nie znajduje. Ta cicha
zmiana zachowania byłaby gorsza od awarii, bo wygląda jak pusta baza.
Rozróżnienie jest celowe: `ready` dotyczy plików WGRANYCH przez ekran (te
ktoś musi świadomie włączyć), a nie zastanych przy przejściu na rejestr.
Zapis stanu może się nie udać (udział read-only) — wtedy trudno, przy każdym
uruchomieniu przyjmiemy je na nowo. Zachowanie jest to samo, koszt żaden."""
files = {str(p.relative_to(root)): {"status": ACTIVE, "adopted_at": _now()}
for p in _scan(root)}
data = {"files": files, "rules": {**DEFAULT_RULES}}
try:
_write_state(root, data)
except OSError:
pass
return data
def registry(root: Path | str, *, for_admin: bool = False) -> list[dict]: def registry(root: Path | str, *, for_admin: bool = False) -> list[dict]:
"""Pliki na udziale wraz ze stanem. `for_admin` odsłania kwarantannę i powody. """Pliki na udziale wraz ze stanem. `for_admin` odsłania kwarantannę i powody.
@@ -209,7 +233,9 @@ def registry(root: Path | str, *, for_admin: bool = False) -> list[dict]:
dochodziły do przeglądarki i były tylko ukrywane stylem, wystarczyłby podgląd dochodziły do przeglądarki i były tylko ukrywane stylem, wystarczyłby podgląd
źródła strony, żeby poznać reguły walidacji.""" źródła strony, żeby poznać reguły walidacji."""
root = Path(root) root = Path(root)
data = _read_state(root) # Brak PLIKU stanu = pierwsze uruchomienie. Pusty słownik przy istniejącym
# pliku to co innego: ktoś świadomie wszystko odstawił, więc nie wskrzeszamy.
data = _read_state(root) if state_path(root).exists() else _adopt_existing(root)
out: list[dict] = [] out: list[dict] = []
for p in _scan(root): for p in _scan(root):
rel = str(p.relative_to(root)) rel = str(p.relative_to(root))
+57 -5
View File
@@ -32,19 +32,24 @@ def _xlsx(path, rows=3, header=("id", "opis")):
# ── rejestr i stany ────────────────────────────────────────────────────── # ── rejestr i stany ──────────────────────────────────────────────────────
def test_new_file_is_visible_but_not_in_use(root): def test_a_file_dropped_on_the_share_is_adopted_as_active(root):
"""Baza położona na udziale poza aplikacją (np. przez NFS) ma działać —
tak było przed DAN-27 i tak ma zostać. Plik WGRANY EKRANEM to inna sprawa:
ten wymaga świadomego włączenia (patrz test niżej)."""
_xlsx(root / "baza.xlsx") _xlsx(root / "baza.xlsx")
entry = files.registry(root)[0] entry = files.registry(root)[0]
assert entry["status"] == files.READY assert entry["status"] == files.ACTIVE
assert entry["in_use"] is False, "nowy plik nie może sam wejść do wyszukiwania" assert entry["in_use"] is True
def test_only_active_files_reach_the_search(root): def test_only_active_files_reach_the_search(root):
_xlsx(root / "a.xlsx") _xlsx(root / "a.xlsx")
_xlsx(root / "b.xlsx") _xlsx(root / "b.xlsx")
assert files.usable_paths(root) == [] files.registry(root) # przyjęcie zastanych
files.set_status(root, "a.xlsx", files.ACTIVE) files.set_status(root, "b.xlsx", files.READY) # świadome odstawienie
assert [pathlib.Path(p).name for p in files.usable_paths(root)] == ["a.xlsx"] assert [pathlib.Path(p).name for p in files.usable_paths(root)] == ["a.xlsx"]
files.set_status(root, "b.xlsx", files.ACTIVE)
assert len(files.usable_paths(root)) == 2
def test_state_survives_a_restart(root): def test_state_survives_a_restart(root):
@@ -160,3 +165,50 @@ def test_delete_removes_the_file_and_its_entry(root):
assert not (root / "a.xlsx").exists() assert not (root / "a.xlsx").exists()
assert files.registry(root, for_admin=True) == [] assert files.registry(root, for_admin=True) == []
assert files.usable_paths(root) == [] assert files.usable_paths(root) == []
# ── przejście na rejestr nie może wyłączyć wyszukiwania ─────────────────
def test_bases_already_on_the_share_stay_in_use_after_the_switch(root):
"""Dotąd bazy działały domyślnie (wyłączało się je przez DISABLED_BASES).
Po przejściu na rejestr pusty stan oznaczałby, że program nagle niczego nie
znajduje — cicha zmiana zachowania gorsza od awarii, bo wygląda jak pusta baza."""
_xlsx(root / "main_base.xlsx")
_xlsx(root / "zodiac_pl.xlsx")
assert len(files.usable_paths(root)) == 2, "zastane bazy wypadły z wyszukiwania"
assert all(e["in_use"] for e in files.registry(root))
def test_adoption_happens_once_and_respects_later_decisions(root):
"""Po przyjęciu stan jest zapisany, więc świadome odstawienie bazy ZOSTAJE —
kolejny odczyt nie może jej wskrzesić."""
_xlsx(root / "a.xlsx")
_xlsx(root / "b.xlsx")
files.registry(root) # przyjęcie
files.set_status(root, "a.xlsx", files.READY) # świadome odstawienie
assert [pathlib.Path(p).name for p in files.usable_paths(root)] == ["b.xlsx"]
files.set_status(root, "b.xlsx", files.READY) # odstawiamy wszystko
assert files.usable_paths(root) == [], "pusty wybór został wskrzeszony"
def test_uploaded_files_still_need_an_explicit_switch_on(root):
"""Przyjęcie dotyczy TYLKO baz zastanych. Plik wgrany ekranem ktoś musi
świadomie włączyć — inaczej nowa baza wchodziłaby do wyników sama."""
_xlsx(root / "zastana.xlsx")
files.registry(root)
out = files.store_upload(root, "nowa.xlsx", _xlsx(root / "tmp.xlsx").read_bytes())
assert out["accepted"] is True
names = [pathlib.Path(p).name for p in files.usable_paths(root)]
assert "nowa.xlsx" not in names, "wgrana baza weszła do wyników bez decyzji"
def test_adoption_survives_a_read_only_share(root, monkeypatch):
"""Na udziale tylko do odczytu stanu nie da się zapisać — zachowanie ma
zostać to samo, tylko przyjęcie powtórzy się przy każdym uruchomieniu."""
_xlsx(root / "a.xlsx")
def boom(*a, **kw):
raise OSError("read-only file system")
monkeypatch.setattr(files, "_write_state", boom)
assert len(files.usable_paths(root)) == 1