Files
astrololo/services/presentation/tests/test_predictions.py
T
gitea d094a145b1
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4m44s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 6s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4m43s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
astrololo: generowanie tekstu jako moduł odłączalny (2/5)
Drugi z pięciu kroków budowy trzech produktów. Czysty refaktor — zachowanie
aplikacji się nie zmienia, liczba testów rośnie tylko o nowe.

DLACZEGO WARUNEK W SZABLONIE NIE WYSTARCZA. Dotąd generowanie było chowane przez
`{% if can(request, 'ai') %}`. To sprawia, że funkcji nie WIDAĆ, ale nie że jej
NIE MA: plik szablonu dalej leży w obrazie i dalej zawiera jej nazwy, więc `grep`
po kontenerze pokazuje wszystko, czego warunek nie pokazał na ekranie. Astroklient
ma nie mieć śladu, nie mieć wyłączoną funkcję — więc granica musi być KATALOGIEM.

CO SIĘ PRZENIOSŁO do app/dodatki/: dwa szablony, pięć plików statycznych, trasa
strumienia, cztery metody klienta warstwy logicznej, wpis w katalogu funkcji,
wpisy tras i zasobów, akcje formularza i reguły CSS nazywające funkcję
(`textarea.prompt`).

JAK APLIKACJA O TO PYTA. Neutralny most `app/rozszerzenia.py` odpowiada wyłącznie
na pytanie „czy coś jest podpięte i co wnosi". Sam nie wymienia ani jednej nazwy —
pilnuje tego osobny test, bo most jest w KAŻDYM obrazie. Dlatego też katalog
nazywa się `dodatki`, a nie `ai`: nazwa w instrukcji importu byłaby dokładnie tym
śladem, którego wydzielanie ma się pozbyć.

TRZY MIEJSCA, KTÓRE OKAZAŁY SIĘ TRUDNE:

1. Pola formularza. FastAPI czyta je z SYGNATURY, a wspólny handler nie może
   wymieniać `prompt_budget` ani `llm_provider`. Rozwiązane zależnością: moduł
   deklaruje własne pola u siebie, handler wie tylko, że dostaje słownik.
2. Klient warstwy logicznej jest wspólny, więc metody `prompt`/`horoscope`/
   `llm_models` musiały z niego wyjść. Wspólny klient daje teraz samą drogę
   w dół (`wywolaj`, `pobierz`, `strumien`) — z szyfrowaniem łącza i tokenem
   międzywarstwowym; co nią pojedzie, jest sprawą modułu.
3. Podtytuł ekranu Skompiluj miał wariant „z AI" i wariant bez. Zamiast warunku
   jedno zdanie prawdziwe niezależnie od tego, jakie moduły są w obrazie.

DOCKERFILE. `COPY . .` wnosiło do obrazu także testy — a plik testowy nazywa
funkcje wprost. Teraz wchodzi wyłącznie `app/`.

TEST GRANICY. `test_ai_tylko_w_module.py` przechodzi po wszystkim poza modułem
i szuka siedemnastu słów. Znalazł dwanaście resztek, których nie widziałem:
komentarze w main.py i security.py, przykład `models.js` w komentarzu features.py,
komentarze w compile.js i w trzech arkuszach, oraz regułę `textarea.prompt`,
która przy rozbijaniu CSS trafiła do arkuszy ekranów zamiast do modułu.

Ma też kontrolę pozytywną (te słowa MAJĄ padać w module — inaczej test
przechodziłby, gdyby funkcję wydrążono) i test odłączalności mostu.

SPRAWDZONE NA KOPII BEZ MODUŁU: aplikacja startuje, wszystkie ekrany oddają 200,
katalog ma 13 funkcji zamiast 14, trasy strumienia nie ma, spreparowane
`action=prompt` wraca do akcji domyślnej bez śladu, a `grep` po drzewie nie
znajduje ani jednego ze słów.

Testy: presentation 362, logic 342, data 37, render 41.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:23:08 +02:00

96 lines
3.6 KiB
Python

"""Zapamiętane predykcje okresowe (PRE-22).
Magazyn żyje w JS (localStorage), więc znów pilnujemy go strukturalnie. Sedno:
predykcje mają PRZEŻYĆ przejście na inną zakładkę i trafić do raportu, a lista
nie może puchnąć od powtórzonych prób na tym samym okresie.
"""
import pathlib
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
# Skrypty generowania mieszkają w module `app/ai/` — katalogu, którego
# w obrazie bez tej funkcji po prostu nie ma.
JS = (APP / "dodatki" / "static" / "predictions.js").read_text(encoding="utf-8")
PROGRESS = (APP / "dodatki" / "static" / "progress.js").read_text(encoding="utf-8")
from tests.skladanie import zloz
TIMELINE = zloz("timeline.html")
# ─────────────────────────────── podpięcie ───────────────────────────────
def test_script_loaded_on_calendar_tab():
assert "predictions.js" in TIMELINE
def test_loaded_after_progress_script():
"""Nasłuchuje zdarzenia, które wysyła progress.js — musi być załadowany po nim,
inaczej przegapi pierwsze wywołanie."""
assert TIMELINE.index("progress.js") < TIMELINE.index("predictions.js")
def test_calendar_has_container_for_the_list():
assert 'id="savedPredictions"' in TIMELINE
def test_progress_announces_finished_horoscope():
"""Okno postępu ogłasza gotowy horoskop zdarzeniem, zamiast samo go zapisywać
— to okno postępu, a nie magazyn."""
assert "astrololo:horoscope" in PROGRESS
assert "profile" in PROGRESS.split("astrololo:horoscope")[1][:200]
# ─────────────────────────────── zachowanie ──────────────────────────────
def test_only_period_horoscopes_are_stored():
"""Interpretacja natalna ma NIE trafiać na listę predykcji okresowych."""
assert "'period'" in JS
def test_same_period_replaces_instead_of_duplicating():
"""Ponowne policzenie tego samego zakresu podmienia wpis. Inaczej lista
puchłaby przy każdej próbie z innym modelem."""
assert "function upsert" in JS
assert "p.from_date === entry.from_date" in JS
assert "p.to_date === entry.to_date" in JS
def test_entry_carries_period_and_text():
for field in ("from_date", "to_date", "text", "saved_at"):
assert field in JS, f"wpis nie niesie pola {field}"
def test_incomplete_result_is_not_stored():
"""Bez tekstu albo bez dat nie ma czego zapamiętać — nie zapisujemy śmieci."""
assert "if (!text || !from || !to) return null;" in JS
def test_list_can_be_pruned():
assert "function remove" in JS
assert "data-drop" in JS
def test_full_storage_is_reported_not_swallowed():
"""Horoskopy bywają długie. Gdy magazyn się przepełni, użytkownik musi się
o tym dowiedzieć, zamiast po cichu stracić wynik."""
assert "pełny" in JS or "pelny" in JS
assert "return false;" in JS
def test_corrupt_storage_does_not_break_the_page():
assert "catch" in JS and "Array.isArray" in JS
def test_storage_is_exposed_for_the_compile_tab():
"""Zakładka „Skompiluj" (PRE-23) ma czytać to samo miejsce — jedno źródło prawdy."""
assert "window.astrololoPredictions" in JS
def test_does_nothing_on_other_tabs():
assert "if (!document.getElementById('savedPredictions')) return;" in JS
def test_reload_does_not_duplicate():
"""Wariant bez strumienia zapisuje przy wczytaniu strony — klucz to okres,
więc odświeżenie nie mnoży wpisów."""
assert "id: from + '..' + to" in JS