4d1daab35a
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m34s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m28s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 18s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 10s
build / build (push) Successful in 26s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m37s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m33s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m27s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 17s
Testy / Kontrola składni wszystkich warstw (push) Successful in 10s
Ostatnie trzy pozycje z treści LOG-05, których wcześniej nie było: - whole_sign_aries — znaki jako domy, ale dom I ZAWSZE na 0° Barana, niezależnie od Ascendentu (tradycja indyjska i część szkół hellenistycznych), - equal_mc — równe domy zakotwiczone na MC: dom X zaczyna się dokładnie na południku, a nie ma go gdzieś w środku, - obrót kosmogramu: Ascendent albo 0° Barana po lewej stronie koła. Zmienia WYŁĄCZNIE rysunek, żadna liczba nie jest przeliczana. Wariant „od Barana" unieruchamia koło względem zodiaku, więc dwa horoskopy da się porównać na oko. Oba nowe systemy zgodne z wyrocznią co do zera od pierwszego uruchomienia. Razem 13 systemów: 2 833 424 porównania w przemiale, zero przekroczeń. PRZY OKAZJI — DWA BŁĘDY, KTÓRE SAM WPROWADZIŁEM I KTÓRYCH TESTY NIE WIDZIAŁY: - linia zbierająca ostrzeżenia o fallbacku trafiła do handlera strony głównej zamiast do compile_pdf: odwoływała się do nieistniejącej zmiennej, czyli 500 na stronie głównej, a do PDF-a ostrzeżenia nie docierały wcale, - compile_build używał parametru formularza, którego nie miał w sygnaturze. Oba przeszły przez komplet zielonych testów, bo żaden nie wywoływał POST-a — testy prezentacji sprawdzały teksty w szablonach i w main.py. Doszły więc testy uderzające w prawdziwe trasy (POST / i POST /compile ze stubowaną logiką), które łapią tę klasę błędu. Z tego samego powodu przepisane dwa testy PDF-a: greppowały z main.py dokładny kształt wywołania render(chart, theme="print") i pękały przy dopisaniu argumentu, mimo poprawnego zachowania. Teraz wołają trasę i sprawdzają, że KAŻDY z czterech rysunków dostaje motyw druku. LOG-05 i PRE-05 → Zrobione w docs/astrololo_wymagania.xlsx. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
227 lines
9.2 KiB
Python
227 lines
9.2 KiB
Python
"""Zakładka „Skompiluj" — zbiorczy raport (PRE-23).
|
|
|
|
Raport składa się z trzech kawałków liczonych w różnych miejscach: horoskopu
|
|
(serwer), interpretacji natalnej i predykcji okresowych (magazyn przeglądarki).
|
|
Pilnujemy, żeby zakładka umiała je zebrać, a przede wszystkim — żeby POWIEDZIAŁA,
|
|
czego brakuje. Niekompletny raport, o którym dowiadujesz się dopiero z PDF-a,
|
|
byłby najgorszym wynikiem.
|
|
"""
|
|
import pathlib
|
|
|
|
from starlette.testclient import TestClient
|
|
|
|
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
|
TPL = (APP / "templates" / "compile.html").read_text(encoding="utf-8")
|
|
BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8")
|
|
JS = (APP / "static" / "compile.js").read_text(encoding="utf-8")
|
|
NATAL_JS = (APP / "static" / "natal.js").read_text(encoding="utf-8")
|
|
INTERPRET = (APP / "templates" / "interpret.html").read_text(encoding="utf-8")
|
|
MAIN = (APP / "main.py").read_text(encoding="utf-8")
|
|
|
|
|
|
def _client():
|
|
import os
|
|
|
|
os.environ.pop("APP_PASSWORD", None) # bez bramki logowania w teście
|
|
from app.main import app
|
|
|
|
return TestClient(app)
|
|
|
|
|
|
# ───────────────────────────────── nawigacja i trasa ─────────────────────
|
|
|
|
def test_tab_is_in_the_menu():
|
|
assert 'href="/compile"' in BASE and "Skompiluj" in BASE
|
|
|
|
|
|
def test_get_renders_empty_form():
|
|
r = _client().get("/compile")
|
|
assert r.status_code == 200
|
|
assert "Złóż podsumowanie" in r.text
|
|
|
|
|
|
def test_routes_exist():
|
|
assert '@app.get("/compile"' in MAIN and '@app.post("/compile"' in MAIN
|
|
|
|
|
|
# ─────────────────────────── kolejność w raporcie ────────────────────────
|
|
|
|
def test_report_order_matches_the_brief():
|
|
"""Prośba partnerów: najpierw imię i nazwisko, potem wprowadzone dane, potem
|
|
RYSUNEK kosmogramu, a dopiero po nim interpretacja natalna i predykcje."""
|
|
person = TPL.index('id="reportPerson"')
|
|
wheel = TPL.index("wheel_svg")
|
|
natal = TPL.index('id="reportNatal"')
|
|
preds = TPL.index('id="reportPredictions"')
|
|
assert person < wheel < natal < preds
|
|
|
|
|
|
def test_person_name_heads_the_report():
|
|
assert 'id="reportPerson"' in TPL
|
|
assert "form.person" in TPL
|
|
|
|
|
|
# ───── wszystkie rysunki w raporcie i w PDF (nie tylko koło) ─────
|
|
# Zasada: co pokazujemy na stronie, ma trafić do podsumowania i do PDF-a.
|
|
|
|
def test_report_shows_every_chart_figure():
|
|
"""Raport «Skompiluj» pokazuje wszystkie rysunki, nie tylko koło."""
|
|
for cls in ("wheel-fig", "aspectarian-fig", "declination-fig", "antiscia-fig"):
|
|
assert cls in TPL, f"brak {cls} w raporcie"
|
|
|
|
|
|
def test_compile_page_renders_all_figures():
|
|
"""Handler /compile liczy wszystkie rysunki do podglądu raportu."""
|
|
for var in ("aspectarian_svg", "declination_svg", "antiscia_svg"):
|
|
assert var in MAIN, f"/compile nie ustawia {var}"
|
|
|
|
|
|
def test_pdf_bundles_all_figures_in_print_theme(monkeypatch):
|
|
"""compile_pdf składa KOMPLET rysunków w motywie DRUKU.
|
|
|
|
Sprawdzamy przez WYWOŁANIE trasy, nie przez szukanie tekstu w main.py:
|
|
poprzednia wersja greppowała `render(chart, theme="print")` i pękała przy
|
|
każdym dopisaniu argumentu, mimo że zachowanie zostawało poprawne."""
|
|
seen: dict[str, str] = {}
|
|
|
|
from app import chartwheel
|
|
from app.clients.render_client import RenderClient
|
|
from app.main import logic
|
|
|
|
for name in ("render", "render_aspectarian", "render_declination", "render_antiscia"):
|
|
original = getattr(chartwheel, name)
|
|
|
|
def spy(chart, theme="screen", _n=name, _o=original, **kw):
|
|
seen[_n] = theme
|
|
return _o(chart, theme=theme, **kw)
|
|
|
|
monkeypatch.setattr(chartwheel, name, spy)
|
|
|
|
monkeypatch.setattr(logic, "positions", lambda **kw: _pdf_sample_chart())
|
|
monkeypatch.setattr(RenderClient, "pdf", lambda self, report: b"%PDF-1.4 stub")
|
|
|
|
r = _client().post("/compile/pdf", json={"person": "Jan", "data": {
|
|
"date": "1984-04-30", "time": "11:20", "tz_offset": 2, "lat": 50.06, "lon": 19.94}})
|
|
assert r.status_code == 200, r.text[:300]
|
|
assert set(seen) == {"render", "render_aspectarian",
|
|
"render_declination", "render_antiscia"}, seen
|
|
assert set(seen.values()) == {"print"}, seen
|
|
|
|
|
|
def _pdf_sample_chart() -> dict:
|
|
cusps = [{"house": i + 1, "sign": "Aries", "in_sign": "0", "decimal": float(i * 30),
|
|
"sign_glyph": "♈"} for i in range(12)]
|
|
ang = {k: {"name": k, "sign": "Aries", "in_sign": "0", "decimal": 0.0,
|
|
"sign_glyph": "♈"} for k in ("Asc", "MC", "Dsc", "IC")}
|
|
return {"engine": "test", "positions": [], "cusps": cusps, "angles": ang,
|
|
"sign_glyphs": [{"sign": "Aries", "glyph": "♈"}], "house_system": "equal"}
|
|
|
|
|
|
# ──────────────────────── zbieranie materiału z magazynów ────────────────
|
|
|
|
def test_reads_stores_through_their_api_not_raw_storage():
|
|
"""Format danych ma jednego właściciela — moduł, który je zapisuje.
|
|
Sięganie tu wprost do localStorage rozjechałoby się przy pierwszej zmianie."""
|
|
assert "window.astrololoNatal" in JS
|
|
assert "window.astrololoPredictions" in JS
|
|
# chodzi o brak BEZPOŚREDNIEGO dostępu, nie o samo słowo (pada w komentarzu)
|
|
assert "localStorage.getItem" not in JS
|
|
assert "localStorage.setItem" not in JS
|
|
|
|
|
|
def test_stores_are_loaded_on_the_tab():
|
|
for src in ("natal.js", "predictions.js", "compile.js"):
|
|
assert src in TPL, f"brak {src} na zakładce"
|
|
|
|
|
|
def test_stores_load_before_the_assembler():
|
|
"""Szukamy TAGÓW skryptów, nie samych nazw — te padają też w komentarzach.
|
|
Statyka jedzie przez helper cache-bustingu (PRE-26): src="{{ static('X') }}"."""
|
|
def tag(name):
|
|
return TPL.index("static('" + name + "')")
|
|
|
|
assert tag("natal.js") < tag("compile.js")
|
|
assert tag("predictions.js") < tag("compile.js")
|
|
|
|
|
|
# ─────────────────────────── mówi, czego brakuje ─────────────────────────
|
|
|
|
def test_shows_what_is_missing():
|
|
assert 'id="readiness"' in TPL
|
|
assert "rdy-missing" in JS
|
|
for hint in ("Interpretacje", "Kalendarz"):
|
|
assert hint in JS, f"brak podpowiedzi, gdzie uzupełnić: {hint}"
|
|
|
|
|
|
def test_all_three_parts_are_checked():
|
|
for label in ("Policzony horoskop", "Interpretacja natalna", "Predykcje okresowe"):
|
|
assert label in JS
|
|
|
|
|
|
# ───────────────────────────── natalna zapamiętywana ─────────────────────
|
|
|
|
def test_natal_is_captured_on_interpret_tab():
|
|
assert "natal.js" in INTERPRET
|
|
assert 'id="natalNote"' in INTERPRET
|
|
|
|
|
|
def test_natal_store_keeps_a_single_entry():
|
|
"""Natalna jest JEDNA — dotyczy momentu urodzenia, nie okresu. Ponowne
|
|
wygenerowanie ma podmienić, nie dokładać."""
|
|
assert "setItem" in NATAL_JS and "push" not in NATAL_JS
|
|
|
|
|
|
def test_natal_only_stores_natal_profile():
|
|
assert "'natal'" in NATAL_JS
|
|
|
|
|
|
def test_full_storage_is_reported():
|
|
assert "pełny" in NATAL_JS
|
|
|
|
|
|
# ─────────────────────────────── bezpieczeństwo ──────────────────────────
|
|
|
|
def test_ai_text_is_escaped_before_injection():
|
|
"""Treść od modelu wstawiamy do DOM — bez ucieczki byłby to wektor wstrzyknięcia."""
|
|
assert "function esc" in JS
|
|
assert "replace(/</g, '<')" in JS
|
|
|
|
|
|
def test_does_nothing_on_other_tabs():
|
|
assert "if (!document.getElementById('readiness')) return;" in JS
|
|
|
|
|
|
# ─────── podsumowanie NIE gubi opcji policzonych przy horoskopie ──────────
|
|
# Regresja 2026-07-28: „Skompiluj" przeliczało horoskop bez stacji/tabel/
|
|
# porównania domów, więc podsumowanie miało braki względem tego, co policzono.
|
|
|
|
def test_compile_form_carries_the_options():
|
|
"""Te same opcje co „Horoskop" — z WSPÓLNEGO pliku (formsync je synchronizuje)."""
|
|
assert '{% include "_form_options.html" %}' in TPL
|
|
opts = (APP / "templates" / "_form_options.html").read_text(encoding="utf-8")
|
|
for f in ('name="stations"', 'name="tables"', 'name="house_systems"'):
|
|
assert f in opts, f"brak opcji we wspólnym pliku: {f}"
|
|
|
|
|
|
def test_compile_handler_recomputes_with_all_options():
|
|
assert "house_systems: list[str] = Form([])" in MAIN
|
|
assert "stations: bool = Form(False)" in MAIN
|
|
assert "house_systems=house_systems" in MAIN # przekazane do logiki
|
|
assert "stations=stations, zodiac=zodiac, tables=tables" in MAIN
|
|
|
|
|
|
def test_compile_pdf_recomputes_with_all_options():
|
|
assert 'stations=bool(data.get("stations"))' in MAIN
|
|
assert 'house_systems=[s for s in (data.get("house_systems")' in MAIN
|
|
|
|
|
|
def test_compile_pdf_payload_includes_options():
|
|
assert "house_systems: multi('house_systems')" in JS
|
|
assert "stations: checked('stations')" in JS
|
|
|
|
|
|
def test_summary_uses_shared_result_tables():
|
|
"""Podsumowanie pokazuje te same tabele co horoskop — jeden wspólny plik,
|
|
żeby nie mogły znowu się rozjechać."""
|
|
assert '{% include "_result_tables.html" %}' in TPL
|