W lutym napisałem, że flaky test to rak toczący zaufanie do CI/CD. Że stabilność jest ważniejsza niż pokrycie. Łatwo napisać manifest. Trudniej podeprzeć go kodem.

Więc zbudowałem dwa narzędzia QA od zera, w odstępie trzech dni. Robią różne rzeczy. Łączy je jeden wzorzec i to wzorzec jest tu pointą: deterministyczny rdzeń, który robi realną robotę, plus warstwa AI, którą można zdjąć i nic się nie sypie.

Nie “AI pisze raport”. Raport jest gotowy bez niej. AI to opcjonalne doradztwo na wierzchu - a bajty to udowadniają.

Wzorzec

Oba narzędzia mają te same trzy etapy: deterministyczny rdzeń, opcjonalny adapter AI, renderer. Rdzeń parsuje, liczy, klasyfikuje. Idzie pierwszy i idzie zawsze. Warstwa AI rusza tylko jeśli ją włączysz, a kiedy się wywali, rzuci wyjątkiem albo zmyśli liczbę - narzędzie oddaje wynik deterministyczny bez zmian.

Pod spodem leży jedna zasada: liczby z kodu, proza z AI. Model nie jest właścicielem żadnej liczby. Bramka po stronie kodu zbiera wszystkie prawdziwe liczby z danych i odrzuca prozę, w której pojawia się jakakolwiek liczba spoza tego zbioru. Liczba niezakotwiczona w danych - doradztwo wylatuje, zostaje raport deterministyczny. Bez wyjątków, bo to regex i kod wyjścia, nie dobre chęci.

Domyślnie wyłączone. Ścieżka bez AI to nie wyjątek doklejony dla bezpieczeństwa - to najczęściej przerabiana ścieżka w testach. W pierwszym narzędziu jest to realny provider-pustka, który akurat jest domyślny. W drugim AI to sesja, która po prostu może się nie pojawić.

Narzędzie pierwsze: qa-report-lake

Zaczęło się od pytania kolegi na LinkedIn: repo puchnie historią raportów, a nic użytecznego z tego nie wychodzi.

qa-report-lake bierze surowe wyniki testów - Playwright, JUnit, Allure albo CTRF wprost - sprowadza je do CTRF i renderuje jeden statyczny raport HTML z historią, trendami i rozróżnieniem flaky kontra nowe. Robi też regresję wizualną: diff pikseli z modelem baseline w stylu reg-suit, triptyk expected/actual/diff, werdykt. Dwa mechanizmy, jeden pipeline.

Inwariant jest testowany na poziomie bajtów. Wyrenderuj raport z AI wyłączonym: 3319 bajtów. Z włączonym: 3498. Pliki się różnią - tą różnicą jest blok advisory, opakowany w markery. Wytnij blok, a to co zostaje jest co do bajtu identyczne z raportem bez AI. Test sprawdza dokładnie to. Część niosąca poprawność nie drgnie, kiedy warstwa AI się rusza.

Bramka pilnująca liczb przechodzi po każdej liczbie w prozie AI i dopasowuje ją do zbioru liczb realnie obecnych w danych, z małą tolerancją - więc 90 i 90,0 to ta sama liczba, ale zmyślone 91 już nie. Anonimizacja czyści terminy klienta zanim jakiekolwiek dane trafią do repo marki, a mapa terminów nigdy nie wchodzi do gita. Ścieżka z AI, jeśli jej chcesz, chodzi na lokalnym modelu przez Ollamę, więc nic nie wychodzi poza maszynę.

Na realnych, zanonimizowanych danych pokazał pass-rate z dziesięciu runów: 79, 81, 31, 77, 45, 56, 79, 79, 87, a na końcu 0 procent - ostatni run to pełna awaria, którą raport wyłapał bez jednego słowa AI. 25 testów, wszystkie zielone. AGPL-3.0.

Narzędzie drugie: flaky-analytics

Trzy dni później, ten sam wzorzec, ostrzejszy.

flaky-analytics czyta historię runów z CI i odpowiada na jedno pytanie: które testy migoczą i co z nimi zrobić. Ściąga ostatnie runy z GitHub Actions, normalizuje je i liczy zdarzenia flaky - retry, który przeskakuje na pass, jeden commit wracający i czerwony, i zielony. Klasyfikuje każdy test jako chroniczny, sporadyczny, izolowany albo zawsze-padający i wypisuje plan kwarantanny z liczbowym dowodem za każdą decyzją.

To skill do Claude Code, nie plugin. I tutaj warstwa AI jest dosłowna: to Claude, w sesji, czyta analizę i pisze hipotezy o przyczynach. Nie ma żadnego procesu modelu do odpalenia. Skill mówi to wprost - jesteś wycinalną warstwą AI, raport jest gotowy bez ciebie. Szkic przechodzi przez bramkę liczb; jeśli cytuje liczbę spoza analizy, leci odrzut, poprawka, ponowne sprawdzenie, a po dwóch nieudanych próbach raport renderuje się bez niego.

Inwariant jest tu najostrzejszą wersją tej samej idei. Test bierze raport z AI, wycina blok advisory i sprawdza, że jest bajtowo równy raportowi bez AI. Plan kwarantanny jest bajtowo identyczny z AI włączonym i wyłączonym. Złote fixtures zgadzają się z wzorcem co do bajtu. Ten sam dowód co w pierwszym narzędziu, o jedno oczko ciaśniej. Anonimizacja idzie krok dalej: czyści, potem przechodzi pliki jeszcze raz i wywala run, jeśli przetrwał choć jeden zakazany termin.

Całość to dziewięć skryptów Node, zero zależności, czyste wbudowane API - chodzi w CI bez niczego do instalacji. 54 testy, wszystkie zielone. To lokalna, audytowalna, otwarta odpowiedź na płatne SaaS-y do flaky w stylu Datadog Test Optimization czy Trunk. AGPL-3.0.

Co wspólne, co się zaostrzyło

Wspólny szkielet: trzy etapy, liczby z kodu, wycinalne AI domyślnie wyłączone, fail-closed wszędzie, bramka liczb po stronie kodu, anonimizacja zanim dane wylądują, CTRF jako wspólny format, inwariant testowany jednostkowo bez sieci.

flaky-analytics mówi o swoim rodowodzie wprost we własnym kodzie: odziedziczone po qa-report-lake. Zmieniła się krawędź. AI przeszło z zewnętrznego lokalnego modelu do samej sesji. Zależności zeszły do zera. Zakres zwęził się z dwóch mechanizmów do jednego. Dystrybucja przeszła z repo z CLI na skill. I oba dowodzą inwariantu co do bajtu - to przestało być różnicą, a stało się standardem.

Po co ten zachód

Od 15.06 programmatic LLM w pipelinie billuje się po pełnej stawce API, osobno od użycia interaktywnego. Deterministyczny rdzeń, który liczy bez tokenów, przestaje być fanaberią, a staje się linijką, za którą nie płacisz. AI idzie na wierzch, opcjonalnie, tam gdzie realnie zarabia na swoje miejsce.

To teza, do której wracam: context before LLM. Najpierw zbuduj czysty, policzony kontekst. Model dołóż drugi, jeśli w ogóle. Kiedy podłoga jest deterministyczna, a AI to warstwa, którą zdejmujesz i dowodzisz, że jest identycznie - masz doradztwo bez stawiania raportu na szali.

Oba narzędzia są na AGPL. Wzorzec to ta część, którą warto ukraść - przenosi się na każdy pipeline tonący w raportach albo we flaky bez właściciela.

Repo