f24616d342
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m57s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 34s
Testy / Kontrola składni wszystkich warstw (push) Successful in 22s
Ostatnia z osmiu wskazowek partnerow. Nowa usluga services/render sklada raport:
generuje plik posredni .tex i kompiluje go XeLaTeX-em do PDF.
OSOBNY komponent, nie czesc prezentacji — ta sama zasada co przy izolacji
swissepha (LOG-27). TeX Live wazy setki megabajtow; w obrazie produktu
spowalnialby kazdy build, a tak aktualizuje sie niezaleznie i jego awaria nie
kladzie aplikacji, tylko przycisk „Pobierz PDF".
SZYFROWANIE, o ktore prosiles: lacze prezentacja↔render idzie tak samo jak
pozostale, ale z WLASNYM, TRZECIM kluczem (LINK_KEY_PRESENTATION_RENDER). Osobny,
bo tym laczem plynie CALY raport — dane urodzeniowe i opisy z baz — wiec przejecie
go nie moze otwierac lacza do logiki ani do danych. Fail-closed: bez klucza pod
nie wstaje. Test kopii link_crypto obejmuje teraz cztery uslugi.
Uklad PDF wg prosby: imie i nazwisko -> wprowadzone dane -> RYSUNEK kosmogramu
-> interpretacja natalna -> predykcje okresowe. Test pilnuje kolejnosci.
Dwie rzeczy, ktore wyszly dopiero przy skladaniu tego kawalka:
1. KOSMOGRAM NIE NADAWAL SIE DO PDF. Uzywa zmiennych CSS (var(--line)) i klasy
.glyph, ktorej font podaje styles.css — a samodzielny konwerter SVG→PDF nie zna
naszego arkusza. Wyszlyby czarne kreski BEZ SYMBOLI. Stad wariant „print":
konkretne kolory na bialym tle i font glifow wpisany wprost w rysunek. Przy
okazji zalatwia to etap 4 planu kola (wariant do druku).
2. UCIECZKA ZNAKOW LATEXA byla zepsuta — i zlapal to moj wlasny test. Zamiana
„\” na \textbackslash{} szla w tej samej petli co nawiasy, wiec kolejne
podmiany ucieklyby nawiasy dopiero co wstawione: wychodzilo
\textbackslash\{\}. Poprawka: ukosnik chowany pod znacznik zastepczy i
rozwijany na koncu. To nie kosmetyka — niezauwazony „%” komentuje RESZTE LINII,
wiec zdanie od modelu urywaloby sie w polowie, a PDF powstawalby normalnie,
tylko krotszy. Test na wrogim tekscie sprawdza tez, ze \end{document} ani
\input{} nie wyrwa sie z dokumentu.
Usluga nie zapisuje nic poza katalogiem tymczasowym, ktory sprzata po sobie;
w manifescie readOnlyRootFilesystem + emptyDir na /tmp. /health raportuje
obecnosc xelatex i rsvg-convert, zeby zepsuty obraz bylo widac od razu.
Zweryfikowane na zywo (TestClient uslugi render): zadanie bez szyfrowania
z POPRAWNYM tokenem -> 400; obcy klucz -> 400 i tajny opis nie wraca; wlasciwy
klucz -> zadanie dochodzi do aplikacji, tresci baz NIE MA na kablu, odpowiedz
zaszyfrowana. Manifesty przechodza kubectl apply --dry-run=server na zywym
klastrze.
CZEGO NIE SPRAWDZILEM: samej kompilacji PDF. W tym srodowisku nie ma ani TeX
Live, ani dzialajacego runtime'u kontenerow (docker CLI jest, daemon nie) —
probowalem zbudowac obraz i sie nie dalo. Sprawdzone jest wszystko dookola:
generowanie .tex, ucieczka znakow, szyfrowanie, kontrakt API, samowystarczalnosc
SVG. Pierwsze uruchomienie na klastrze trzeba obejrzec — dlatego PRE-24 zostaje
jako „W trakcie", nie „Zrobione".
Instrukcja wdrozenia: docs/wdrozenie-render-pdf.md (klucz, obraz, merge,
weryfikacja, znane ograniczenia).
Testy: 15 nowych (render) + 14 (lacze i wariant druku w prezentacji).
Calosc: prezentacja 131, logika 265/1 skip, render 15.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
131 lines
4.8 KiB
Python
131 lines
4.8 KiB
Python
"""Generowanie źródła LaTeX raportu (PRE-24).
|
||
|
||
Najważniejsza część to UCIECZKA znaków specjalnych. Kompilacji tu nie
|
||
uruchomimy (brak TeX Live w tym środowisku), ale to i tak nie ona jest tu
|
||
najbardziej krucha: błędna ucieczka nie wywala kompilacji, tylko po cichu psuje
|
||
treść — najgorszy możliwy wynik, bo PDF powstaje i wygląda poprawnie.
|
||
"""
|
||
import pytest
|
||
|
||
from app.latex import build, esc
|
||
|
||
|
||
# ─────────────────────────── ucieczka znaków specjalnych ────────────────
|
||
|
||
def test_percent_is_escaped():
|
||
"""NAJGROŹNIEJSZY przypadek: `%` bez ucieczki komentuje resztę linii, więc
|
||
zdanie urywa się w środku, a PDF powstaje normalnie — tylko krótszy."""
|
||
out = esc("wzrost o 50% w tym okresie")
|
||
assert r"50\%" in out
|
||
assert "w tym okresie" in out
|
||
|
||
|
||
def test_backslash_is_escaped_first():
|
||
"""Odwrotny ukośnik musi iść pierwszy, inaczej zepsulibyśmy ucieczki
|
||
wstawione później (np. `\\%` zamieniłoby się w coś innego)."""
|
||
assert esc("a\\b") == r"a\textbackslash{}b"
|
||
|
||
|
||
def test_all_specials_are_escaped():
|
||
for ch in ("$", "&", "#", "_", "%", "{", "}"):
|
||
assert ch not in esc(ch).replace("\\" + ch, ""), f"{ch} nie zostało ucieczone"
|
||
|
||
|
||
def test_tilde_and_caret_use_commands():
|
||
assert esc("~") == r"\textasciitilde{}"
|
||
assert esc("^") == r"\textasciicircum{}"
|
||
|
||
|
||
def test_polish_and_glyphs_pass_through():
|
||
"""XeLaTeX bierze unicode wprost — polskich znaków ani glifów nie ruszamy."""
|
||
text = "Zażółć gęślą jaźń ♄ ♓ ☉"
|
||
assert esc(text) == text
|
||
|
||
|
||
def test_escape_handles_none_and_numbers():
|
||
assert esc(None) == ""
|
||
assert esc(42) == "42"
|
||
|
||
|
||
# ─────────────────────────────── układ dokumentu ─────────────────────────
|
||
|
||
def _report(**over):
|
||
base = {
|
||
"person": "Jan Kowalski",
|
||
"data": {"date": "1984-04-30", "time": "11:20", "tz_offset": 2,
|
||
"lat": 50.0647, "lon": 19.945, "place": "Kraków",
|
||
"house_system": "whole_sign", "zodiac": "tropical"},
|
||
"natal": {"text": "Interpretacja natalna.\n\nDrugi akapit."},
|
||
"predictions": [
|
||
{"from_date": "2026-01-01", "to_date": "2026-03-31", "text": "Pierwszy kwartał."},
|
||
{"from_date": "2026-07-01", "to_date": "2026-09-30", "text": "Trzeci kwartał."},
|
||
],
|
||
}
|
||
base.update(over)
|
||
return base
|
||
|
||
|
||
def test_document_is_complete():
|
||
tex = build(_report())
|
||
assert tex.startswith(r"\documentclass")
|
||
assert tex.rstrip().endswith(r"\end{document}")
|
||
|
||
|
||
def test_uses_unicode_engine_setup():
|
||
"""XeLaTeX + fontspec — bez tego polskie znaki i glify nie wyjdą."""
|
||
tex = build(_report())
|
||
assert r"\usepackage{fontspec}" in tex
|
||
assert r"\setmainfont" in tex
|
||
|
||
|
||
def test_section_order_matches_the_brief():
|
||
"""Prośba partnerów: imię → dane → rysunek → natalna → predykcje."""
|
||
tex = build(_report(), wheel_pdf="wheel.pdf")
|
||
person = tex.index("Jan Kowalski")
|
||
data = tex.index("Data urodzenia")
|
||
wheel = tex.index("includegraphics")
|
||
natal = tex.index("Interpretacja natalna")
|
||
preds = tex.index("Predykcje okresowe")
|
||
assert person < data < wheel < natal < preds
|
||
|
||
|
||
def test_person_name_opens_the_document():
|
||
tex = build(_report())
|
||
body = tex.split(r"\begin{document}")[1]
|
||
assert body.index("Jan Kowalski") < 80, "imię ma być na samym początku"
|
||
|
||
|
||
def test_all_predictions_are_included():
|
||
tex = build(_report())
|
||
assert "2026-01-01" in tex and "2026-07-01" in tex
|
||
assert "Pierwszy kwartał" in tex and "Trzeci kwartał" in tex
|
||
|
||
|
||
def test_paragraphs_are_preserved():
|
||
"""Model oddziela akapity pustą linią — mają zostać akapitami, nie zlepkiem."""
|
||
tex = build(_report())
|
||
assert "Interpretacja natalna." in tex and "Drugi akapit." in tex
|
||
assert "Interpretacja natalna.\n\nDrugi akapit." in tex
|
||
|
||
|
||
def test_hostile_text_cannot_break_the_document():
|
||
"""Tekst od modelu jest wejściem z ZEWNĄTRZ — nie może wstrzyknąć polecenia
|
||
ani urwać dokumentu."""
|
||
nasty = r"100% \end{document} \input{/etc/passwd} $x_1$ #& {}"
|
||
tex = build(_report(natal={"text": nasty}))
|
||
body = tex.split(r"\begin{document}")[1]
|
||
assert body.count(r"\end{document}") == 1, "tekst urwał dokument"
|
||
assert r"\input{" not in body
|
||
|
||
|
||
def test_missing_parts_are_simply_absent():
|
||
"""Niekompletny materiał ma dać krótszy raport, nie wyjątek."""
|
||
tex = build({"person": "", "data": {}})
|
||
assert r"\begin{document}" in tex and r"\end{document}" in tex
|
||
assert "Raport astrologiczny" in tex # zapas, gdy brak imienia
|
||
|
||
|
||
def test_wheel_is_optional():
|
||
assert "includegraphics" not in build(_report())
|
||
assert "includegraphics" in build(_report(), wheel_pdf="wheel.pdf")
|