Mam ticket i katalog z testami, chcę README, nie keynote
Jest 15 października 2023. Na biurku mam dwie rzeczy: ticket z kryteriami akceptacji i katalog tests/ z kilkoma plikami Playwrighta. Brakuje trzeciej: dokumentacji, którą ktoś inny przeczyta, zanim dotknie tego kodu. Planu testów, który mówi co jest w zakresie, checklisty kryteriów akceptacji, i README z odpowiedzią na jedno pytanie: jak to odpalić lokalnie.
To nie jest nowy problem. Proces testowy, który rozpisywałem tu w 2019 roku, zakładał, że ktoś wie, które testy są szybkie, a które nocne. Podział na PR, main i nightly z zeszłego roku zakładał to samo. W obu przypadkach ta wiedza siedziała u mnie w głowie, a w repozytorium był tylko kod. Dokumentacja to zawsze była ta czynność, którą robię w piątek po południu albo nie robię wcale.
Teza tego wpisu jest jedna i chcę ją mieć na górze, żeby nie zginęła w demie. Model językowy jest dzisiaj dobrym pierwszym szkicem dokumentacji testowej, ale tylko wtedy, kiedy źródłem jest ticket albo kod, a nie jego pamięć o “typowej aplikacji bankowej”. Kiedy pytam otwarcie, dostaję keynote: cztery środowiska, macierz przeglądarek, sekcję o zarządzaniu ryzykiem i zero związku z moim repozytorium. Kiedy wklejam kryteria akceptacji i listę plików, dostaję szkic, który da się poprawić szybciej, niż napisałbym go od zera.
W styczniu opisywałem tutaj generowanie przypadków testowych z modelu i to jest dokładnie ten sam mechanizm o poziom wyżej: nie pytam o wiedzę domenową, pytam o przepisanie materiału, który już mam, w narzuconą formę. Różnica jest w tym, że przypadek testowy weryfikuję, uruchamiając test. Dokument weryfikuję tylko czytaniem, więc bramka musi być inna.
Wejście: AC z ticketu, lista plików, sposób uruchomienia
Model dostaje trzy rzeczy i ani jednej więcej. Ticket, stan repozytorium, sposób uruchomienia.
Ticket na potrzeby tego wpisu wymyśliłem od zera. Nie ma tu numeru z żadnego systemu, nazwy klienta ani adresu wewnętrznego, i to nie jest ozdoba wpisu, tylko reguła pracy: do promptu nie wkleja się cudzego ticketu.
Ticket syntetyczny: Reset hasła przez link w mailu
Użytkownik, który nie pamięta hasła, podaje adres e-mail na /reset
i dostaje wiadomość z jednorazowym linkiem do formularza nowego hasła.
Kryteria akceptacji:
AC1. Formularz na /reset przyjmuje adres e-mail i zawsze pokazuje ten sam
komunikat potwierdzenia, niezależnie od tego, czy konto istnieje.
AC2. Mail z linkiem wychodzi w ciągu 60 sekund dla istniejącego konta.
AC3. Token w linku jest jednorazowy i traci ważność po 60 minutach.
AC4. Nowe hasło ma co najmniej 12 znaków; formularz odrzuca hasło
identyczne z poprzednim.
AC5. Po udanej zmianie hasła wszystkie aktywne sesje są unieważnione.
AC6. Trzy próby resetu z jednego adresu IP w ciągu 10 minut kończą się
odpowiedzią 429.Druga rzecz to stan repozytorium, wklejony jako zwykły listing, bez opisu słownego:
$ ls tests/
account-profile.spec.ts
auth-login.spec.ts
auth-logout.spec.ts
password-reset.spec.ts
helpers/mail-stub.ts
helpers/rate-limit.tsTrzecia to fragment workflow, żeby model wiedział, co się uruchamia i kiedy. Nie streszczam go zdaniem, bo streszczenie jest miejscem, w którym gubi się prawda:
on:
pull_request:
schedule:
- cron: "0 2 * * *"
jobs:
pr:
steps:
- run: npx playwright test --project=chromium --grep-invert @slow
nightly:
if: github.event_name == 'schedule'
steps:
- run: npx playwright test --workers=4Do tego dochodzi warstwa, której nie wklejam przy każdej rozmowie, bo od lipca siedzi w ustawieniach. Custom instructions weszły do ChatGPT 20 lipca, najpierw w becie dla subskrypcji Plus, potem szerzej, i to jest dla mnie najbardziej praktyczna zmiana tego półrocza. Nie dlatego, że model stał się mądrzejszy. Dlatego, że pięć zdań, które wcześniej przepisywałem do każdego promptu, teraz obowiązuje domyślnie:
Odpowiadasz po polsku.
Piszesz tylko z materiału, który dostałeś w tej rozmowie.
Czego nie ma w materiale, oznaczasz jako TODO(człowiek) i nie zgadujesz.
Nie wymyślasz nazw środowisk, jobów CI, usług ani adresów.
Wynik oddajesz jako markdown, nagłówki i tabele, bez wstępu i bez podsumowania.Efekt jest mierzalny i skromny. Zdanie o zgadywaniu zmniejszyło liczbę wymyślonych rzeczy w szkicu, ale jej nie wyzerowało: trzy skreślenia z sekcji niżej są z rozmowy, w której te instrukcje były aktywne. Zdanie o formacie działa najlepiej z całej piątki, bo tabela to kształt, a nie fakt. Zdanie o polskim oszczędza mi kalek typu “przypadki testowe zostały wykonane”.
Pracuję w oknie przeglądarki, w interfejsie Plus. Od 20 września Copilot Chat jest w publicznej becie dla subskrypcji indywidualnych, więc czat mam też w edytorze i używam go do kodu, ale dokumentację składam tam, gdzie mam stałe instrukcje i całą historię jednej rozmowy. Żadne z tego nie wisi w pipeline. Zestaw testów nie woła w tej historii niczego po sieci.
Trzy artefakty: plan, checklista, README
Nie proszę o “dokumentację”. Proszę o trzy osobne artefakty, w trzech osobnych wiadomościach, bo każdy ma inną cenę pomyłki.
Plan testów: zakres i to, co poza zakresem
Pierwszy szkic po skreśleniach i po moich uzupełnieniach wygląda tak:
# Plan testów: reset hasła przez link w mailu
## Zakres
- /reset: walidacja adresu i jednakowy komunikat dla konta istniejącego
i nieistniejącego (AC1)
- wysyłka i czas dostarczenia mierzony na stubie pocztowym (AC2)
- token: jednorazowość i wygaśnięcie po 60 minutach (AC3)
- nowe hasło: długość i zakaz powtórzenia poprzedniego (AC4)
- unieważnienie sesji po zmianie hasła (AC5)
- limit prób z jednego IP i odpowiedź 429 (AC6)
## Poza zakresem
- szablon HTML maila i jego treść: TODO(człowiek), właściciel nieustalony
- dostarczalność u realnego dostawcy poczty, SPF i DKIM
- logowanie przez dostawcę zewnętrznego
- historia haseł głębsza niż jedno poprzednie; AC4 mówi o jednym
## Ryzyka
- AC1 kontra AC2: skoro komunikat jest taki sam, test dla konta
nieistniejącego nie ma sygnału w UI i musi patrzeć na stub pocztowy
- AC3: bez kontroli czasu w aplikacji test wygaśnięcia to 60 minut
czekania, czyli kandydat na @slow i na nocny przebieg
- AC6: nightly chodzi z --workers=4, a limit jest na IP; równoległe
workery na jednym agencie widzą jeden adres
## Poziomy
- jednostkowe: generator tokenu, walidator hasła
- integracyjne: unieważnienie sesji, licznik limitu
- e2e Playwright: AC1, AC2, AC4 na jednej ścieżceSekcja “poza zakresem” jest tu najcenniejsza i to jest dla mnie zaskoczenie tego eksperymentu. Zwykle pisałem plan jako listę tego, co przetestuję, a rzeczy pominięte zostawały w głowie, skąd nikt ich nie wyciągnie na review. Model wypełnia tę sekcję chętnie, bo to jest wnioskowanie z ticketu przez zaprzeczenie: ticket mówi o jednym poprzednim haśle, więc historia haseł jest poza zakresem.
Trzecia linia w ryzykach to najlepsza rzecz, jaką w tej rozmowie dostałem, i nie wymyśliłbym jej szybciej sam. Powiązanie --workers=4 z limitem na adres IP jest możliwe wyłącznie dlatego, że wkleiłem workflow. Gdybym napisał “testy chodzą w nocy równolegle”, nie dostałbym niczego poza ogólnikiem. Cała różnica między szkicem użytecznym i szkicem do wyrzucenia siedzi w tym, czy wejście jest materiałem, czy streszczeniem materiału.
Checklista AC
Drugi artefakt to tabela i tu model jest po prostu szybszy ode mnie o kilka minut. Sześć kryteriów, dla każdego test, plik i stan:
| AC | Co sprawdzam | Poziom | Plik | Stan |
|-----|-------------------------------------|--------------|-------------------------|-------|
| AC1 | jednakowy komunikat, konto jest | e2e | password-reset.spec.ts | jest |
| AC1 | jednakowy komunikat, konta nie ma | e2e | password-reset.spec.ts | brak |
| AC2 | mail na stubie w 60 s | e2e | password-reset.spec.ts | brak |
| AC3 | token jednorazowy | integracyjny | TODO(człowiek) | brak |
| AC3 | token po 60 minutach nieważny | integracyjny | TODO(człowiek) | brak |
| AC4 | hasło krótsze niż 12 znaków | jednostkowy | TODO(człowiek) | brak |
| AC4 | hasło identyczne z poprzednim | jednostkowy | TODO(człowiek) | brak |
| AC5 | sesje unieważnione po zmianie | integracyjny | TODO(człowiek) | brak |
| AC6 | czwarta próba zwraca 429 | integracyjny | helpers/rate-limit.ts | brak |Kolumna “stan” jest wypełniona przeze mnie, nie przez model, i to jest świadoma decyzja. Model widział nazwy plików, ale nie widział ich treści, więc nie ma prawa twierdzić, że coś jest pokryte. Kiedy raz pozwoliłem mu zgadnąć tę kolumnę, dostałem “jest” w pięciu wierszach na podstawie samej nazwy password-reset.spec.ts. Taki dokument jest gorszy niż brak dokumentu, bo wygląda na przejrzany.
Rozbicie jednego kryterium na dwa wiersze to też robota modelu i jest dobra. AC1 to dwa różne testy, mimo że w ticketcie jest jednym zdaniem. AC3 to jednorazowość i czas, dwie różne rzeczy w jednej linijce.
README: jak to odpalić lokalnie
Tu model jest najbliżej bycia bezpiecznym, bo źródłem jest kod, a nie ticket. Wklejam package.json, playwright.config.ts i plik compose, a on je streszcza do instrukcji, której nikt nie musi wyciągać z kanału na czacie:
# Testy: reset hasła
Wymagania: Node 18 lub nowszy, Docker z pluginem Compose.
Playwright w tym repozytorium jest przypięty na 1.39.0.
npm ci
npx playwright install --with-deps chromium
docker compose up -d mail-stub
npx playwright test tests/password-reset.spec.ts --project=chromium
Testy oznaczone @slow są wyłączone na pull requeście i chodzą w nocnym
przebiegu (cron 02:00). Lokalnie: npx playwright test --grep @slowWersję Playwrighta podaję modelowi jawnie, bo to jedyny sposób, żeby jej nie zgadywał. 1.39.0 wyszło 11 października, cztery dni temu, i tak samo jak pinuję tagi obrazów i snapshoty modelu, pinuję wersję w README. Numer wersji w dokumentacji, która się rozjechała z package-lock.json, jest pierwszą rzeczą, przez którą nowa osoba traci wieczór.
Trzy skreślenia z pierwszego szkicu
Szkic planu miał niecałą stronę i trzy zdania, które wykreśliłem. Wszystkie trzy przyszły w rozmowie z aktywnymi custom instructions, więc traktuję je jako podłogę, a nie jako sufit błędów.
Skreślenie pierwsze: wymyślone środowiska. Szkic miał sekcję, której nie zamawiałem:
## Środowiska
| Środowisko | Adres | Dane |
|------------|----------------|-------------------------------|
| DEV | dev.internal | anonimizowana kopia produkcji |
| UAT | uat.internal | pełny zestaw danych |
| PREPROD | preprod.internal | kopia produkcji z 24h |Ja mam lokalnie i mam kontener w CI. Nic więcej nie istnieje i nic z tego nie było w moim wejściu. To nie jest pomyłka w szczególe, to jest cały mechanizm tego wpisu w jednym miejscu: tam, gdzie ticket milczy, model wstawia średnią ze swojego materiału treningowego, a średnia dokumentacji testowej w internecie to duża firma z czterema środowiskami. Skreślam całą sekcję i nie proszę o poprawkę, bo poprawka wróci jako mniejsza wersja tego samego.
Skreślenie drugie: docker-compose w wersji, której nie ma. W README pojawiła się linia:
docker-compose up -d mailhogDwa błędy w jednym wierszu. Pierwszy: mój plik compose ma usługę mail-stub, a mailhog to nazwa, którą model dopisał, bo w jego materiale stub pocztowy najczęściej tak się nazywa. Drugi: docker-compose z dywizem to Compose V1, wycofany w tym roku, a dzisiaj instaluje się plugin i woła docker compose. Nowa osoba, która wykona tę linię, dostaje “command not found” i nie wie, czy zepsuty jest jej Docker, czy moje repozytorium. Poprawna wersja to docker compose up -d mail-stub i taka trafia do pliku.
Skreślenie trzecie: wymyślony cel wdrożenia. To skreślenie przeżyło dwa moje czytania i dlatego jest tu najważniejsze:
## Wdrożenie i testy po wdrożeniu
Po zmergowaniu do main pipeline wdraża zmianę na slot staging w Azure
App Service, a testy smoke uruchamiają się przeciwko slotowi przed swapem.Workflow, który wkleiłem, kończy się na npx playwright test. Nie ma tam wdrożenia, slotu ani swapu. Zdanie przetrwało dwa czytania nie dlatego, że jest sprytne, ale dlatego, że brzmi jak zdanie z mojego świata: Azure, slot, smoke po wdrożeniu, wszystko to pisałem tu wcześniej w innym kontekście. Wymyślony fragment, który pasuje do stylu autora, jest trudniejszy do wyłapania niż wymyślony fragment, który do niego nie pasuje. I jest groźniejszy, bo taki dokument idzie do następnej rozmowy jako materiał wejściowy i wtedy fikcja staje się źródłem.
Trzy skreślenia na niecałą stronę to bilans, który akceptuję, bo poprawianie jest tańsze od pisania od zera. Akceptuję go pod jednym warunkiem: każdą linię umiem sprawdzić w ticketcie albo w repozytorium. W dniu, w którym nie umiem, ten bilans przestaje mieć znaczenie, bo nie wiem, ile skreśleń przegapiłem.
Sama procedura jest nudna i taka ma być. Czytam szkic obok wejścia, w dwóch oknach. Każde zdanie z liczbą, nazwą albo adresem musi mieć źródło w materiale. Zdania bez źródła nie poprawiam, tylko usuwam. Na końcu doklejam to, czego model wiedzieć nie mógł, i to ja jestem autorem commita.
Gdzie nie używam LLM-a
Dwie rzeczy zostają po mojej stronie i to nie jest ostrożność na zapas, tylko rachunek kosztu pomyłki.
Nocny raport jako jedyne źródło prawdy. Model potrafi zgrupować podobne awarie z nocnego przebiegu i taki tekst czyta się rano lepiej niż listę stu asercji. Problem jest w tym, co się z nim dzieje dalej: streszczenie “trzy migotliwe, jedna prawdziwa” wrzucone na kanał zespołu przestaje być streszczeniem i staje się decyzją, której nikt nie zweryfikował. Dokumentem obowiązującym zostaje raport z przebiegu razem ze śladem, a nie akapit o nim. Streszczenie mogę sobie przeczytać przy biurku, żeby wiedzieć, gdzie zacząć. Nie publikuję go jako ustalenia i nie wpuszczam modelu w pętlę, w której sam czyta wynik i sam aktualizuje dokumentację.
Changelog dla klienta. Ten tekst wychodzi na zewnątrz i jest podpisany moim nazwiskiem. Model nie wie, co naprawdę weszło na produkcję, bo tego nie ma w moim wejściu, a zdanie “poprawiono walidację hasła” w dokumencie, który ktoś czyta jako obietnicę, jest kosztowniejsze niż wieczór pisania. Do tego, żeby był użyteczny, musiałbym wkleić do promptu kontekst klienta, a tego nie robię. Ta reguła jest starsza niż modele językowe: przy eksperymencie z API GPT-3 w 2021 roku wniosek był ten sam, tylko dotyczył danych. Do promptu nie idzie nic cudzego, ani dane, ani ticket, ani nazwa.
Trzecia rzecz, mniej oczywista: dokumentacja modelu nie jest “żywą specyfikacją”. Widziałem już propozycję, żeby generować plan testów przy każdej zmianie i traktować wynik jako aktualny stan wiedzy. To jest to samo, co generowanie danych testowych przy każdym uruchomieniu zestawu: płacenie za coś, czego nikt nie przeczytał, i wpuszczenie cudzego niedeterminizmu w miejsce, w którym potrzebuję jednej wersji. Dokument jest żywy wtedy, kiedy ktoś go zmienił i podpisał, a nie wtedy, kiedy został wygenerowany.
Podsumowanie: szkic z modelu, merge dokumentacji jak merge kodu
Wniosek po tygodniu mieści się w jednym zdaniu: model językowy jest dziś dobrym pierwszym szkicem dokumentacji testowej, o ile źródłem jest ticket albo kod, a publikuje człowiek.
Rozbijam to na cztery zdania, które chcę móc przeczytać za rok. Jakość szkicu zależy prawie wyłącznie od jakości wejścia: kryteria akceptacji wklejone dosłownie i workflow wklejony dosłownie dają uwagi, których sam bym nie zauważył, a opis słowny tego samego daje ogólniki. Custom instructions z lipca są realnym ułatwieniem, bo przenoszą reguły formy i zakaz zgadywania z promptu do ustawień, ale nie są bramką i trzy skreślenia z tego wpisu przyszły przy włączonych instrukcjach. Wymyślone treści układają się w powtarzalne klasy: nieistniejące środowiska, przestarzała albo przekręcona komenda, wymyślony cel wdrożenia, i najgroźniejsza jest ta, która brzmi jak moje własne zdanie. Kolumny “pokryte” model nie wypełnia, bo pokrycia nie widział.
Czego z tego nie wynika. Nie wynika, że mam nowe narzędzie w pipeline, bo cała ta praca dzieje się przy biurku, w oknie przeglądarki. Nie wynika, że dokumentacja pisze się sama, bo skreślenia i uzupełnienia zajmują mi tyle czasu, ile wcześniej zajmowało pisanie pierwszej wersji, tylko są przyjemniejsze i mniej ich odkładam. Nie wynika też, że model rozumie mój projekt: rozumie mój listing.
Reguła, którą zostawiam sobie na stałe, jest krótka. Dokumentację merguję tak, jak merguję kod. Plan, checklista i README leżą w repozytorium obok testów, które opisują, wchodzą w diff, mają jednego recenzenta i jednego autora commita. Model obniża koszt pierwszego szkicu i tyle, a odpowiedzialność nie przesuwa się o milimetr, bo podpisać można się tylko pod czymś, co się przeczytało.
Na listopad zostawiam sobie temat, który wisi obok tego i którego dziś nie ruszam: bezpieczeństwo. Checklista AC z tego wpisu ma sześć wierszy o funkcjach i ani jednego o tym, co się stanie, kiedy ktoś potraktuje formularz resetu hasła jako wejście, a nie jako funkcję. Do tego czasu ładniejsza dokumentacja nie naprawia mi procesu. Sprawia tylko, że wcześniej widać, czego w ticketcie nie napisano.

