Wszystkie wpisy

AI szkicuje raport z nightly - człowiek publikuje (październik 2024)AI szkicuje raport z nightly - człowiek publikuje (październik 2024)

English

AI szkicuje raport z nightly - człowiek publikuje (październik 2024)

Październik 2024: GPT-4o mini składa draft z logów nightly. Quality gate zostaje przy teście, nie przy modelu. Bez o1 jako pinu.

AutomatyzacjaQA

Akapit z nocy, nie nowy portal

Jest 15 października, wtorek. Nocny przebieg ruszył z harmonogramu o czwartej naszego czasu, bo cron 0 2 * * * liczy w UTC, i skończył się przed piątą. Na kanale zespołu czeka automatyczna wiadomość z liczbami i linkami, a w artefaktach przebiegu leżą trzy raporty HTML Playwrighta, po jednym na każdy z trzech jobów nocnej macierzy. Razem dwanaście czerwonych testów.

Wiem, jak wygląda dalsza część poranka, bo robię to od lat. Otwieram pierwszy raport, przewijam, otwieram drugi, porównuję nazwy z tym, co pamiętam z wczoraj, i po kilkunastu minutach mam w głowie obraz nocy, którego poza mną nikt nie widzi. Zespół widzi czerwoną wiadomość i tyle. Tę lukę chcę zamknąć, ale nie nowym portalem z wykresami. Chcę akapitu dla siebie i dla zespołu: ile padło, co jest nowe, co już znamy i od czego zaczynam.

Moja teza na październik 2024 jest wąska. GPT-4o mini jest na tyle tani, że szkic takiego akapitu z wyciągu z plików JUnit kosztuje mnie ułamek centa, i na tyle sprawny, że porządkuje dwanaście porażek szybciej, niż ja otworzę drugi raport. Bramka jakości i publikacja raportu zostają przy mnie: kolor ustawia test, a o tym, co z tym kolorem zrobić i co trafi do zespołu, decyduję ja. Model pisze szkic. Ja go sprawdzam i wklejam do wątku albo do zgłoszenia, albo skreślam w całości.

To jest ciąg dalszy triage’u z lipca 2023, ale nie jego powtórka. Tam miałem jeden czerwony test i prosiłem o trzy hipotezy, które potem sprawdzałem odtworzeniem. Tu mam cały przebieg i o hipotezy nie proszę wcale, tylko o porządek. Tamten wpis był mikroskopem przyłożonym do jednego padnięcia. Ten jest mapą nocy, z której dopiero wybieram, gdzie przyłożę mikroskop.

Stan warsztatu, żeby za rok było wiadomo, o czym mówię. Runner to Playwright 1.48.0 z 8 października, czyli sprzed tygodnia. Szkic pisze gpt-4o-mini przypięty do snapshotu gpt-4o-mini-2024-07-18, z dnia premiery, w cenie 15 centów za milion tokenów wejścia i 60 centów za milion tokenów wyjścia. GPT-4o z 13 maja mam w czacie i do tej roboty go nie biorę: zadanie polega na przepisaniu i pogrupowaniu krótkiego wyciągu, a większy model kosztuje na token mniej więcej rząd wielkości więcej. o1-preview jest od 12 września i zostaje opcją, nie pinem. To preview, w Plus ma tygodniowy limit wiadomości, odpowiada wolno, a przez API jest na token sto razy droższy od mini, nie licząc tokenów rozumowania, za które też się płaci. Claude 3.5 Sonnet z 21 czerwca jest realną konkurencją, ale wybór dostawcy nie zmienia niczego w tym, jak sprawdzam szkic.

Czego tu nie będzie. Model w tym wpisie nie ma władzy nad niczym: nie dotyka bramki, zgłoszeń ani kodu testów i nie dostaje ani jednego tokena dostępu. Nie będzie nazwy klienta ani prawdziwego zrzutu z nocnego przebiegu, bo nocne logi niosą dane, które nie są moje. Nie będzie też przeglądu gotowych paneli, które obiecują to samo za abonament. Będzie syntetyczny nocny przebieg z mojego repozytorium z przykładami do bloga: dwanaście porażek, w tym trzy timeouty o podobnych nazwach i jedna nowa asercja na logowaniu, a do tego prompt, szkic i trzy skreślenia. Aplikacja pod spodem jest wymyślona, a porażki, komunikaty i liczby są takie, jakie wypisał runner 1.48.0.

Wejście, nie pamięć modelu

Model nie zna mojego pipeline’u i nie pozna go z jednego zdania w prompcie. Dostaje artefakt z przebiegu, a nie prośbę, żeby przypomniał sobie, jak to u nas wygląda.

Nocny job to macierz trzech plasterków z fail-fast: false, zbudowana tak samo jak w październiku 2021, z tą różnicą, że pliki między plasterki rozdziela dziś sam Playwright przez --shard. Każdy plasterek zostawia dwa artefakty: raport HTML i plik z wbudowanego reportera JUnit. Gdybym zbierał raporty blob, npx playwright merge-reports skleiłby z nich jeden raport HTML, ale sklejony raport to nadal lista do przeklikania, a nie akapit. Rano pobieram więc same pliki JUnit i przepuszczam je przez skrypt:

gh run download <id-przebiegu> --pattern 'junit-*' --dir nightly/2024-10-15
python3 tools/nightly_digest.py nightly/znane.txt nightly/2024-10-15

Skrypt ma 26 linii i nie woła żadnego modelu:

import sys
import xml.etree.ElementTree as ET
from pathlib import Path

known = set(Path(sys.argv[1]).read_text(encoding="utf-8").splitlines())
reports = sorted(Path(sys.argv[2]).glob("junit-*/junit.xml"))
rows = []
for report in reports:
    shard = report.parent.name.removeprefix("junit-")
    for case in ET.parse(report).iter("testcase"):
        failure = case.find("failure")
        if failure is None:
            continue
        name = f'{case.get("classname")} › {case.get("name")}'
        lines = [line.strip() for line in (failure.text or "").splitlines()]
        error = next((line for line in lines if "Error" in line), "?")
        diff = [line for line in lines if line.startswith(("Expected", "Received"))]
        rows.append(("ZNANY" if name in known else "NOWY", shard, name, error, diff[:2]))

new = sum(1 for row in rows if row[0] == "NOWY")
print(f"porażki: {len(rows)}, nowe: {new}, znane: {len(rows) - new}, plasterki: {len(reports)}")
for label, shard, name, error, diff in rows:
    print(f"{label:<5} {shard}/{len(reports)} {name}")
    print(f"      {error}")
    if diff:
        print(f"      {' | '.join(diff)}")

Robi trzy rzeczy. Z każdego pliku bierze tylko te przypadki, które mają element failure. Z treści porażki zostawia pierwszą linię ze słowem Error i parę linii Expected i Received, jeśli runner je wypisał. Pełną nazwę testu porównuje z listą w nightly/znane.txt i na tej podstawie dopisuje etykietę NOWY albo ZNANY. Tę listę prowadzę ręcznie i każda nazwa ma na niej za sobą zgłoszenie albo zaplanowany triage. To, co znane, jest moją pamięcią, nie pamięcią modelu, i dlatego leży w pliku obok skryptu.

Wyciąg z nocy 15 października wygląda tak:

porażki: 12, nowe: 1, znane: 11, plasterki: 3
ZNANY 1/3 account/orders.spec.ts › zamówienia › lista ostatnich
      Error: Timed out 5000ms waiting for expect(locator).toHaveCount(expected)
      Expected: 3 | Received: 0
ZNANY 1/3 account/orders.spec.ts › zamówienia › filtr po statusie
      Error: Timed out 5000ms waiting for expect(locator).toHaveCount(expected)
      Expected: 1 | Received: 0
ZNANY 1/3 account/orders.spec.ts › zamówienia › szczegóły zamówienia
      Error: Timed out 5000ms waiting for expect(locator).toBeVisible()
      Expected: visible | Received: <element(s) not found>
ZNANY 1/3 account/orders.spec.ts › zamówienia › ponów zamówienie
      Error: Timed out 5000ms waiting for expect(locator).toBeVisible()
      Expected: visible | Received: <element(s) not found>
ZNANY 1/3 account/returns.spec.ts › zwroty › formularz zwrotu
      Error: Timed out 5000ms waiting for expect(locator).toBeVisible()
      Expected: visible | Received: <element(s) not found>
ZNANY 1/3 account/returns.spec.ts › zwroty › status zwrotu
      Error: Timed out 5000ms waiting for expect(locator).toHaveCount(expected)
      Expected: 1 | Received: 0
ZNANY 2/3 account/reviews.spec.ts › opinie › wystaw opinię
      Error: Timed out 5000ms waiting for expect(locator).toBeVisible()
      Expected: visible | Received: <element(s) not found>
ZNANY 2/3 account/reviews.spec.ts › opinie › lista opinii
      Error: Timed out 5000ms waiting for expect(locator).toHaveCount(expected)
      Expected: 2 | Received: 0
NOWY  2/3 auth/login.spec.ts › logowanie › nieistniejący e-mail daje wspólny komunikat
      Error: Timed out 5000ms waiting for expect(locator).toHaveText(expected)
      Expected string: "Nieprawidłowy e-mail lub hasło" | Received string: "Nie znaleziono konta"
ZNANY 3/3 checkout/delivery.spec.ts › dostawa › kurier
      TimeoutError: locator.check: Timeout 15000ms exceeded.
ZNANY 3/3 checkout/delivery.spec.ts › dostawa › paczkomat
      TimeoutError: locator.check: Timeout 15000ms exceeded.
ZNANY 3/3 checkout/delivery.spec.ts › dostawa › odbiór w punkcie
      TimeoutError: locator.check: Timeout 15000ms exceeded.

Trzy pliki XML z tej nocy mają razem około 17 kilobajtów, a same treści porażek to 270 niepustych linii. Wyciąg ma 34. Reszta to call log, który przy asercji na logowaniu powtarza tę samą parę linii dziewięć razy, ramki kodu i ścieżka z agenta przy każdej porażce.

Tu jest też odpowiedź na redakcję, którą w lipcu zeszłego roku robiłem nożyczkami. Wycinałem wtedy z logu tokeny, nagłówki Authorization, adresy wewnętrznych usług i wszystko, co mogło wskazać, czyj to projekt. Dziś większość tych rzeczy nie ma którędy wejść, bo skrypt czyta trzy pola i nic poza nimi. Nie czyta system-out ani system-err, do których trafia wszystko, co test albo aplikacja wypisały na konsolę. Nie czyta call logu z adresami i selektorami. Nie czyta ścieżki z agenta, która na runnerze hostowanym przez GitHub zaczyna się od /home/runner/work/ i ma dalej nazwę repozytorium. Redakcja przeszła z nożyczek na konstrukcję: czego skrypt nie przeczyta, tego nie ma w wyciągu.

Konstrukcja nie zwalnia mnie jednak z czytania. Linie Expected i Received niosą to, co było na stronie, a na stronie bywa adres e-mail, numer zamówienia albo nazwisko z danych testowych. Wyciąg ma 34 linie, więc czytam go cały, zanim cokolwiek wyślę. W cudzym projekcie obowiązuje zasada z triage’u bez jednej poprawki: wklejenie cudzego artefaktu do zewnętrznej usługi jest decyzją jego właściciela, nie moją, i wyciąg idzie do modelu na kluczu, który ten właściciel kontroluje.

Do wyciągu dochodzi jedno zdanie, którego nie ma w żadnym pliku JUnit: skąd przyszedł przebieg. We wrześniu 2022 rozdzieliłem testy na trzy koszyki i ten podział robi tu całą robotę. Czerwony test na pull requeście to sprawa autora zmiany, zanim zrobi merdż. Czerwony test w nocy to sprawa właściciela nocnego przebiegu, który ma go rano rozpoznać. Te same dwanaście porażek znaczy w obu miejscach co innego, a model bez tego zdania zgaduje. W lipcu zeszłego roku zgadywał nawet z tym zdaniem i dostałem hipotezę o bramce przy teście, który na bramce w ogóle nie chodzi. Dlatego w prompcie piszę wprost: harmonogram, nie pull request, trzy plasterki i żadnych innych jobów.

Co ma być w szkicu

Format szkicu jest najważniejszą decyzją w całym tym ćwiczeniu, bo to jedyne miejsce, w którym mogę modelowi cokolwiek narzucić.

Na górze jest liczba: porażki, nowe, znane. Idzie pierwsza, bo najłatwiej ją sprawdzić mechanicznie, i dlatego model ma ją przepisać ze skryptu, a nie liczyć. Potem nowe, każde osobno, nawet jeśli jest jedno. Potem znane, jedna linia na grupę, bo szczegóły są w zgłoszeniach. Potem najwyżej trzy klastry i najwyżej trzy rzeczy do sprawdzenia rano, bo z listy dziesięciu nikt niczego nie sprawdzi. Na końcu sekcja, której w lipcu zeszłego roku brakowało mi najbardziej: czego nie wiem. Model nie mówił wtedy “nie wiem”, a w formacie odpowiedzi nie było na to miejsca. Kiedy sekcja jest w szablonie, model ją wypełnia.

Klastry mają trzy nazwy i każda opisuje objaw, a nie przyczynę. Timeout to porażka, w której runner nie doczekał się elementu, żeby wykonać na nim akcję. Dane to porażka, w której nie ma czego sprawdzić, bo lista jest pusta albo elementu nie ma na stronie; nazwa brzmi jak przyczyna, ale w szkicu znaczy tylko tyle. Asercja to porażka, w której element jest, tylko ma inną treść, niż test oczekuje. W lipcu 2023 dzieliłem awarie na cztery koszyki przyczyn: test, dane, infrastruktura, produkt. Tamten podział jest dobry przy jednym padnięciu, które zaraz odtworzę. Przy mapie nocy dzielę po tym, co widział runner, bo to da się sprawdzić w XML. Dlatego w szkicu nie ma koszyka “produkt”: produkt to werdykt, a werdykt wymaga odtworzenia. Nie ma też werdyktu w drugą stronę, bo “wszystko to środowisko” jest takim samym werdyktem, tylko tańszym.

Prompt trzymam w docs/nightly-prompt.md, obok docs/triage-prompt.md z lipca zeszłego roku, i wysyłam go jako wiadomość systemową. Wyciąg idzie jako druga wiadomość:

To jest wyciąg z nocnego przebiegu z harmonogramu, nie z pull requesta.
Runner: Playwright 1.48.0. Nocny job ma trzy plasterki: nightly (1),
nightly (2) i nightly (3). Innych jobów w tym przebiegu nie ma.
Liczby w nagłówku i etykiety NOWY / ZNANY policzył skrypt.
Przepisz je, nie licz od nowa.

Napisz szkic raportu po polsku, w tej kolejności:
1. Liczba: porażki, nowe, znane.
2. Nowe: każdy test z etykietą NOWY osobno, nawet jeśli jest jeden.
3. Znane: jedna linia na grupę.
4. Klastry, najwyżej trzy, z liczbą testów i wspólnym komunikatem:
   timeout - runner nie doczekał się elementu do akcji,
   dane - nie ma czego sprawdzić, lista jest pusta albo elementu brak,
   asercja - element jest, ale ma inną treść.
5. Do sprawdzenia rano: najwyżej trzy punkty.
6. Czego nie wiem: czego nie da się ustalić z tego wyciągu.

Nie pisz, że coś jest błędem produktu. Nie proponuj poprawek
ani ponownego uruchomienia. Nie wymieniaj jobów, plików ani testów,
których nie ma w wyciągu.

Szkic z nocy 15 października, bez żadnej mojej poprawki:

Nightly 15.10, przebieg z harmonogramu (nie PR), Playwright 1.48.0

Liczba: 12 porażek, nowe: 1, znane: 11.

Nowe:
- auth/login.spec.ts › logowanie › nieistniejący e-mail daje wspólny
  komunikat: zamiast oczekiwanego "Nieprawidłowy e-mail lub hasło"
  strona pokazuje "Nie znaleziono konta".

Znane:
- 8 testów konta (zamówienia, zwroty, opinie): puste listy albo brak elementu.
- 3 testy dostawy (kurier, paczkomat, odbiór w punkcie): timeout przy wyborze opcji.

Klastry:
1. Dane (8): Received: 0 albo <element(s) not found> na stronach konta,
   plasterki 1/3 i 2/3.
2. Timeout (3): locator.check przekracza 15000 ms na opcjach dostawy,
   plasterek 3/3. To typowa niestabilność WebDrivera przy wolno
   ładujących się elementach; pomoże zwiększenie implicit wait.
3. Asercja (1): inny tekst komunikatu na logowaniu, plasterek 2/3.

Job nightly-api zakończył się bez błędów, więc problem nie dotyczy
warstwy API.

Do sprawdzenia rano:
- czy konto testowe ma zamówienia, zwroty i opinie,
- trace jednego z timeoutów dostawy,
- skąd zmiana komunikatu na logowaniu.

Czego nie wiem: czy timeouty wynikają z aplikacji, czy z agenta CI,
i czy zmiana komunikatu była zamierzona.

Podsumowanie: noc wygląda na niestabilność środowiska testowego.
Po uzupełnieniu danych warto ponowić cały przebieg.

Zacznę od tego, co w tym szkicu jest dobre, bo jest tego więcej niż złego. Mini złożył osiem porażek z trzech plików, dwóch plasterków i dwóch różnych asercji w jeden klaster i nazwał go po objawie, a nie po pliku. Nowy test wyciągnął na górę, osobno, ze zdaniem, które mówi dokładnie tyle, ile widać w wyciągu. Lista do sprawdzenia rano to moja lista, w mojej kolejności. Sekcja “czego nie wiem” zadaje dwa pytania, na które naprawdę nie umiem odpowiedzieć z samego wyciągu. Przeklikanie tego samego przez trzy raporty HTML zajmuje mi kilkanaście minut, a szkic jest gotowy, zanim zamknę pierwszy raport.

Trzy zdania z tego szkicu skreślam i każde rozbieram niżej, w części o tym, jak model kłamie. Sam szkic jest dla mnie dokumentem roboczym i nie wychodzi poza moje okno. To, co wychodzi na kanał, ma się zmieścić w wątku na Slacku bez przewijania.

Gdzie człowiek publikuje

Po skreśleniach zostaje szkic, który jest poprawny i niekompletny. Dopisuję do niego dwie rzeczy, których nie ma w żadnym wyciągu.

Pierwsza to historia main. Wczoraj po południu weszła tam zmiana komunikatów na formularzu logowania. Nieistniejący adres nie jest scenariuszem bramki pull requesta, tylko chodzi w nocy razem z całym E2E, więc zmiana przeszła na zielono i dopiero noc ją zobaczyła. Dokładnie po to jest trzeci koszyk.

Druga to kryterium, którego ten test pilnuje. To historyjka logowania, do której w styczniu zeszłego roku szkicowałem przypadki z ChatGPT, zanim powstał do niej zestaw testów. Kryterium mówi, że niepoprawne dane pokazują jeden komunikat, bez wskazania, które pole jest złe, a na liście przypadków, którą wtedy zostawiłem, stoi wprost: e-mail nieistniejący w systemie dostaje ten sam komunikat, bez ujawniania, czy konto istnieje. “Nie znaleziono konta” ujawnia dokładnie to. Czy to błąd, czy świadoma zmiana kryterium, nie rozstrzyga ani model, ani ja. Rozstrzyga autor zmiany razem z właścicielem historyjki. Moja rola kończy się na pytaniu i na zgłoszeniu z trace’em, a dalej jest już triage, ten z mikroskopem.

Znane porażki mają swoje zgłoszenia i dopisuję tylko ich stan. Osiem testów konta pada od wczoraj, bo trwałe środowisko nocne nie dostało danych z zasilania. Trzy timeouty na opcjach dostawy wracają co kilka nocy i w historii nocnych przebiegów to trzecia czerwona noc z ostatnich siedmiu.

Akapit, który wkleiłem do wątku pod automatyczną wiadomością z nocy:

Nightly 15.10: 12 porażek, 1 nowa.
Nowa: logowanie › nieistniejący e-mail daje wspólny komunikat.
Zamiast "Nieprawidłowy e-mail lub hasło" strona pokazuje "Nie znaleziono
konta", a kryterium mówi o jednym komunikacie, bez ujawniania, czy konto
istnieje. Wczoraj po południu na main weszła zmiana komunikatów logowania.
Pytanie do autora i trace są w zgłoszeniu.
Znane: 8 testów konta bez danych (zasilanie, zgłoszenie z 14.10)
i 3 timeouty na opcjach dostawy (trzecia noc z ostatnich siedmiu,
triage w tym tygodniu).
Niczego nie ponawiam. Raporty HTML i JUnit są w artefaktach przebiegu.

Z modelu zostały w nim liczby, nazwa nowego testu, oba klastry znanych porażek i kolejność. Ode mnie jest wczorajszy merdż, kryterium, stan zgłoszeń, seria siedmiu nocy i ostatnie zdanie. Pod takim akapitem mogę się podpisać, bo każde zdanie sprawdziłem w wyciągu, w XML albo w historii repozytorium.

Teraz to, czego ten akapit nie robi, bo tu przebiega cała granica. Kolor nocnego przebiegu ustawia kod wyjścia runnera. Playwright kończy się błędem, kiedy padł choć jeden test, job jest czerwony i nic, co napiszę w wątku, tego nie zmieni. Nocny przebieg nie jest wymaganym checkiem i nie blokuje żadnego merdża, a bramka pull requesta jest zielona albo czerwona z runnera, nie z akapitu. Żaden krok w pipelinie nie czyta tego, co napisał model. Model nie ma też tokena do niczego: ani do Slacka, ani do repozytorium, ani do zgłoszeń.

Mechanika publikacji zostaje mechaniką. Przy wynikach Cypressa w Azure DevOps krok z testami dostał continueOnError: true, żeby publikacja wyników w ogóle się wykonała, a failTaskOnFailedTests: true przeniosło czerwień na krok publikacji. Od tamtej pory wiem, że kolor ustawia konfiguracja, a nie opis. Wiadomość na Slacka po testach wysyłam od 2020 roku krokiem, który wykonuje się zawsze: wtedy z condition: always() w Azure DevOps, dziś z if: always() w Actions. Wychodzi z linkami do raportu bez względu na wynik i nadal jest pierwszym sygnałem rano. Akapit nie zastępuje ani publikacji, ani wiadomości. Trafia do wątku pod automatyczną wiadomością, więc liczby z mechanizmu i moje zdania stoją obok siebie. Jeśli się różnią, błędny jest akapit.

Akapit ma trzy możliwe miejsca i wybieram je ja. Wątek, kiedy w nocy jest coś nowego. Zgłoszenie, kiedy nowa porażka potrzebuje właściciela, i wtedy do zgłoszenia idzie zdanie z akapitu, trace i link do raportu, a nie cały szkic. Kosz, kiedy nowego nie ma nic. Automatyczna wiadomość mówi wtedy wszystko, co trzeba, a codzienny akapit “11 znanych, 0 nowych” byłby dokładnie tym szumem, przed którym w lipcu zeszłego roku broniłem się, nie wpinając modelu w CI.

Muszę się tu uczciwie odnieść do tego, co napisałem rok temu przy dokumentacji testowej z LLM. Pisałem wtedy, że streszczenie nocy wrzucone na kanał zespołu przestaje być streszczeniem i staje się decyzją, której nikt nie zweryfikował, i że takiego streszczenia nie publikuję jako ustalenia. Podpisuję się pod tym dalej. Na kanał nie idzie streszczenie modelu, tylko mój akapit: sprawdzony zdanie po zdaniu, z dwiema rzeczami, których model wiedzieć nie mógł, i z odesłaniem do raportów, które nadal są dokumentem obowiązującym. Różnica nie leży w tym, kto napisał pierwszą wersję, tylko w tym, kto ją sprawdził i kto się pod nią podpisał.

Szkic nie dostał też własnego joba. W czerwcu model dostał taki job przy pull requestach w repozytorium z przykładami, z głosem doradczym i bez prawa do bramki, i tam ma to sens, bo komentarz pod diffem czyta autor, który i tak czeka na recenzję. Job po nocnym przebiegu, który sam wysyła szkic na kanał, publikowałby właśnie niesprawdzone streszczenie. Job, który tylko odkłada szkic do artefaktów, produkowałby co noc tekst, którego nikt nie zamawiał, i trzymałby klucz do API w sekretach pipeline’u bez żadnego powodu.

Szkic powstaje więc rano na moim laptopie, przez Chat Completions, z temperature ustawionym na zero. Wejście, czyli prompt i wyciąg, ma około 950 tokenów, a szkic około 420, więc przy cenach mini cały poranek kosztuje niecałe cztery setne centa. Zero w temperature nie robi z modelu funkcji: dwa wywołania na tym samym wyciągu nie muszą dać tego samego tekstu, więc szkiców nie porównuję między sobą, tylko wyciągi. Mógłbym złożyć to samo jako własny GPT albo asystenta z plikiem, bo jedno i drugie jest od DevDay 6 listopada zeszłego roku, ale do przepisania 34 linii nie potrzebuję ani pamięci, ani plików.

Gdy model kłamie

Procedura jest ta sama, którą rok temu opisałem przy dokumentacji, i jest nudna z założenia. Szkic w jednym oknie, wyciąg w drugim. Każde zdanie z liczbą albo z nazwą joba, pliku czy testu musi mieć źródło w wyciągu. Zdania bez źródła nie poprawiam, tylko usuwam. Kiedy coś budzi wątpliwość, otwieram surowy XML albo raport HTML, bo wyciąg też jest tylko skrótem.

Skreślenie pierwsze: zmyślony job. “Job nightly-api zakończył się bez błędów, więc problem nie dotyczy warstwy API.” W tym przebiegu nie ma joba nightly-api, a prompt mówił to wprost, jednym zdaniem. Testy API chodzą u mnie na main po merdżu, w drugim koszyku z 2022 roku, a nie w nocy. To ta sama klasa co e2e-nightly z lipca 2023: kształt wiarygodny, konkret wymyślony. Tym razem jest jednak gorzej, bo zmyślony job nie jest ozdobą, tylko przesłanką. Zdanie wyklucza warstwę API na podstawie joba, którego nie ma, a przy ośmiu porażkach z pustymi listami na stronach konta API jest pierwszym miejscem, które sprawdziłby każdy, kto nie zna wczorajszego zgłoszenia. Kto przeczyta to zdanie na kanale, zacznie szukać gdzie indziej.

Skreślenie drugie: WebDriver w raporcie Playwrighta. “To typowa niestabilność WebDrivera przy wolno ładujących się elementach; pomoże zwiększenie implicit wait.” W jednym zdaniu są dwa błędy. Pierwszy to narzędzie. W tym przebiegu nie ma WebDrivera, a implicit wait to pojęcie z Selenium, którego Playwright nie potrzebuje, bo przed każdą akcją czeka sam. Prompt podawał runner razem z numerem wersji i to nie pomogło, tak samo jak w lipcu zeszłego roku nie pomógł log z locator.click w pierwszej linii błędu. Drugi błąd to poprawka, której zakazałem. W surowym XML call log przy kurierze ma jedną linię, waiting for getByRole('radio', { name: 'Kurier' }), a przy paczkomacie i odbiorze w punkcie wygląda tak samo, z inną nazwą. Przez piętnaście sekund na stronie nie pojawił się żaden z trzech przycisków opcji. Dłuższe czekanie dałoby ten sam wynik później albo schowałoby wolną stronę, którą ktoś powinien zobaczyć. W lipcu zeszłego roku taka rada zostawała w moim oknie czatu. W akapicie na kanale ktoś mógłby ją po prostu wykonać.

Skreślenie trzecie: “noc wygląda na niestabilność środowiska”. Ostatnia sekcja szkicu nazywa się “Podsumowanie” i nie zamawiałem jej. Prompt ma sześć punktów i żaden nie prosi o wniosek. Model dopisał go sam, w dwóch zdaniach spłaszczył noc do jednego koszyka i zalecił ponowienie całego przebiegu, czego zakazałem wprost. To podsumowanie przeczy trzem rzeczom naraz. Przeczy sekcji “Nowe” z tego samego szkicu, bo inny tekst komunikatu na logowaniu nie jest niestabilnością czegokolwiek. Przeczy sekcji “Czego nie wiem”, w której model sam napisał, że nie wie, czy timeouty biorą się z aplikacji, czy z agenta. I przeczy danym. Test na pustej liście zamówień nie jest niestabilny, tylko pada za każdym razem, dopóki ktoś nie zasili konta, więc ponowienie dałoby te same osiem czerwonych i tę samą asercję na logowaniu, a najwyżej zazieleniłoby timeouty i zrobiło noc ładniejszą, niż była. We wrześniu 2022 opisałem moment, w którym bramka staje się formalnością: ktoś klika “re-run”, nie czytając logu. To podsumowanie podsuwa ten klik całemu zespołowi i z całego szkicu najłatwiej je skopiować na kanał, bo brzmi jak wniosek.

Czwartego trybu w tym szkicu nie ma i to jest zasługa skryptu, nie modelu. Pierwsza wersja skryptu nie dopisywała etykiet, a pierwsza wersja promptu kazała modelowi samemu oddzielić znane porażki od nowych. Dla porównania puściłem ją na wyciągu z tej nocy z wyciętymi etykietami i dostałem jeden klaster na dziewięć testów:

Asercje (9): listy i elementy na stronach konta są puste albo niewidoczne
(toHaveCount, toBeVisible, toHaveText), prawdopodobnie brak danych.

Test logowania siedział w tym klastrze jako dziewiąty i nie było go nigdzie indziej. Model nie miał skąd wiedzieć, co jest znane, więc pogrupował po pierwszej linii błędu, bo dziewięć z dwunastu porażek zaczyna się od Error: Timed out 5000ms waiting for expect(locator), a nie po tym, co mówi para Expected i Received. Klaster z wyjaśnieniem to najlepsze miejsce do schowania nowej porażki: wygląda na porządek, a zjada jedyną informację, dla której ktoś rano otwiera raport. Od tamtej pory nowość liczy skrypt, a prompt każe wypisać każdy test z etykietą NOWY osobno, nawet jeśli jest jeden.

Po skreśleniach robię cztery sprawdzenia, które nie wymagają myślenia, i dlatego robię je zawsze. Liczby w szkicu mają być równe nagłówkowi wyciągu. Każda linia NOWY z wyciągu ma być w sekcji “Nowe”, z nazwą testu. Każdy job, plik i test wymieniony w szkicu ma być w wyciągu albo w prompcie. Czwarte sprawdzenie dotyczy skryptu, nie modelu: suma atrybutów failures z elementów testsuites w trzech plikach ma się zgadzać z liczbą porażek w wyciągu. Tej nocy było to 6, 3 i 3, razem 12, tyle samo, ile w nagłówku. Skrypt, który gubi porażkę, jest groźniejszy od modelu, który ją zmyśla, bo jego wyniku nikt nie czyta drugi raz.

Lepszy model tej procedury nie zmienia. Nie sprawdzałem, czy GPT-4o albo Claude 3.5 Sonnet dałyby tej nocy mniej skreśleń, i nie to jest pytanie, bo każdy z nich przeszedłby te same cztery sprawdzenia. Model zmienia to, ile skreślam. Nie zmienia tego, że czytam.

Podsumowanie

W październiku 2024 nocny raport z modelem to u mnie tani szkic z GPT-4o mini, recenzowany tak jak pull request: zdanie po zdaniu, ze źródłem w wyciągu. Bramka zostaje przy teście, nie przy czacie.

Trzy zdania, które chcę móc przeczytać za rok. Skrypt na 26 linii zamienia trzy pliki JUnit z nocy w wyciąg na 34 linie, sam liczy porażki i sam oznacza nowe, a model dostaje tylko to, co skrypt przeczytał. gpt-4o-mini-2024-07-18 składa z tego szkic za niecałe cztery setne centa i jest dobry w tym, co jest porządkiem, a słaby w tym, co jest wnioskiem: z syntetycznej nocy z dwunastoma porażkami skreśliłem zmyślony job, WebDrivera z radą o czekaniu i podsumowanie, które zrzuca noc na środowisko i każe ją powtórzyć. Na kanał idzie mój akapit z dwiema rzeczami, których model nie mógł wiedzieć, czyli z wczorajszym merdżem i kryterium, a kolor przebiegu ustawia runner.

Czego z tego nie wynika. Nie wynika, że model wszedł do mojego nocnego pipeline’u, bo szkic powstaje rano na laptopie i żaden krok w pipelinie go nie czyta. Nie wynika, że triage jest zbędny, bo akapit kończy się zgłoszeniem, które ktoś musi odtworzyć, a mapa nocy tylko pokazuje, gdzie przyłożyć mikroskop. Nie wynika też, że oszczędzam dużo czasu. Przeklikiwanie trzech raportów zastąpił szkic, ale czytanie szkicu obok wyciągu zjada mi mniej więcej połowę tego, co oszczędziłem, dokładnie jak przy przypadkach testowych w styczniu 2023.

Mierzę przy tym jedną rzecz i zapisuję ją przy każdym akapicie: ile zdań skreśliłem. Jeśli skreślanie zacznie mi zajmować więcej czasu niż napisanie akapitu od zera, szkic z modelu wypada z procesu, a wyciąg zostaje, bo wyciąg działa bez modelu. Liczby do metryki z 2022 roku, czyli odsetka nocnych przebiegów, w których coś padło bez zmiany w kodzie, biorę z wyciągów, nie z akapitów.

W podsumowaniu 2023 chciałem, żeby odpowiedź na pytanie “gdzie w tym procesie jest AI” dała się powiedzieć jednym zdaniem z czasownikiem “recenzuję”. Dla nocy brzmi ono tak: model szkicuje akapit z wyciągu, a ja go recenzuję, uzupełniam, publikuję i podpisuję. Tańszy szkic nie sprawia, że ktoś rano patrzy na nocny przebieg. Sprawia tylko, że ten, kto patrzy, szybciej przechodzi od trzech raportów do jednego akapitu, pod którym może się podpisać.