Przejdź do głównej zawartości

Testy charakteryzacyjne: utrwal zachowanie legacy, zanim dotknie go agent

Testy charakteryzacyjne zapisują to, co kod legacy robi dziś, a nie to, co powinien robić, i padają, gdy ten wynik się zmieni. Snapshoty, testy zatwierdzające (approval tests) i golden master na szwie zamieniają nieprzetestowany kod w wyrocznię, na której agent może refaktoryzować. Nagraj je na nietkniętym kodzie, udowodnij testami mutacyjnymi, że mogą paść, i zablokuj, zanim agent zacznie.

Musisz oddać agentowi podział 1400-liniowego statement.js, którego nikt nie dotykał od 2019 roku. Nie ma testów, drukuje wyciągi dla klientów, a dział finansów co miesiąc uzgadnia z nimi księgi. Jeśli poprosisz agenta o „zrefaktoryzuj i dopisz testy”, napisze testy opisujące jego własny, zrefaktoryzowany kod, a one przejdą niezależnie od tego, czy miesięczna opłata nalicza się tak samo jak wcześniej. Ta strona jest dla dewelopera, który musi oddać taką refaktoryzację agentowi, a potem ją zatwierdzić bez czytania każdej linii.

Co zyskujesz, utrwalając zachowanie legacy na początku

Dział zatytułowany „Co zyskujesz, utrwalając zachowanie legacy na początku”
  • Tabelę decyzyjną, która dobiera snapshot, test zatwierdzający, golden master albo kombinacyjny test zatwierdzający (combination approval) do szwu, jaki masz.
  • Procedurę w ośmiu krokach: nagranie plików golden na nietkniętym kodzie, usunięcie niedeterminizmu, dowód, że mogą paść, i blokada.
  • Dwa przetestowane szkielety testów: testy kombinacyjne z ApprovalTests w Pythonie i snapshoty plikowe z zamrożonym zegarem w Vitest, oba uruchomione 26 września 2026.
  • Cztery prompty do skopiowania: inwentaryzacja szwów, nagranie testów, audyt osobliwości i refaktoryzacja pod zablokowaną wyrocznią.
  • Konfiguracje narzędzi, które trzymają agenta z dala od kodu legacy podczas nagrywania i z dala od plików golden podczas refaktoryzacji, w Claude Code, Codex i Cursorze.

Dlaczego agent nie może napisać testów po refaktoryzacji?

Dział zatytułowany „Dlaczego agent nie może napisać testów po refaktoryzacji?”

Test jest wyrocznią tylko wtedy, gdy powstał na podstawie czegoś innego niż kod, który ocenia. Kod legacy nie ma specyfikacji, więc jedynym niezależnym źródłem prawdy jest działający system. Michael Feathers nazwał tę technikę w książce Working Effectively with Legacy Code (2004): test charakteryzacyjny opisuje faktyczne zachowanie fragmentu kodu i pozostaje punktem odniesienia, dopóki ktoś świadomie nie zdecyduje, że to zachowanie ma się zmienić.

Przy agentach kolejność kroków przesądza o wszystkim. Agent, który najpierw refaktoryzuje, a potem testuje, pisze asercje zgodne ze zrefaktoryzowanym wynikiem, więc zgubiona opłata albo zmienione zaokrąglenie staje się „oczekiwanym zachowaniem”. Agent, który najpierw nagrywa na commicie, którego nie zmienił, wytwarza pliki golden, które refaktoryzacja musi odtworzyć. Różnica nie leży w kodzie testów, tylko w tym, z którego commita pochodzą pliki golden.

Testy charakteryzacyjne utrwalają zachowanie razem z błędami i o to chodzi. Refaktoryzacja ma zmienić strukturę, a nie zachowanie. Każdy błąd znaleziony podczas nagrywania trafia na listę osobliwości i zostanie naprawiony później, we własnej, świadomej zmianie.

Jaki rodzaj testu charakteryzacyjnego pasuje do twojego szwu?

Dział zatytułowany „Jaki rodzaj testu charakteryzacyjnego pasuje do twojego szwu?”

Szew to miejsce, w którym możesz obserwować zachowanie kodu bez edytowania go: wartość zwracana przez funkcję, odpowiedź HTTP, plik wynikowy zadania wsadowego, wiersze zapisywane przez zadanie. Wybierz najwyższy szew, który jest deterministyczny i dość szybki, żeby uruchamiać go przy każdym pull requeście. Wysoki szew przetrwa refaktoryzację; niski utrwala implementację, którą agent ma właśnie zmienić.

FormaCo utrwalaNajlepszy szewNarzędzia (sprawdzone 2026-09-26)
Snapshot plikowyJeden zserializowany wynik na przypadek wejściowy, zapisany jako plik, który da się porównaćFunkcje bliskie czystym, renderery, odpowiedzi APIVitest toMatchFileSnapshot (5.0.2), snapshoty Jest (30.5.2), syrupy dla pytest (6.1.1)
Test zatwierdzającyWynik received porównywany z plikiem approved; człowiek zatwierdza, promując plikRaporty tekstowe, dokumenty, wszystko, co człowiek oceni, czytając diffApprovalTests: approvaltests 19.1.1 (PyPI), approvals 7.3.0 (npm)
Test kombinacyjnyKażdą kombinację kilku wartości wejściowych w jednym zatwierdzonym plikuFunkcje z kilkoma parametrami i wieloma gałęziami (ceny, podatki, uprawnienia)verify_all_combinations w approvaltests 19.1.1
Golden masterCały wynik przebiegu na dużym, stałym zbiorze wejśćZadania wsadowe, eksporty, narzędzia CLI, endpointy HTTP nagrane na wdrożeniuDowolny runner i diff; wersja HTTP jest na stronie modernizacja legacy

Jeśli masz więcej niż jeden szew, utrwal oba: golden master na krawędzi dowodzi, że system odpowiada tak samo, a test kombinacyjny na kluczowej funkcji mówi, gdzie coś się zmieniło.

Nagranie zrób jako osobny pull request, scalony przed rozpoczęciem jakiejkolwiek refaktoryzacji. Większość pisania może wykonać agent; ty odpowiadasz za wybór szwu, listę osobliwości i blokadę.

  1. Zmapuj szwy w trybie tylko do odczytu. Poproś agenta o inwentaryzację punktów wejścia, efektów ubocznych i źródeł niedeterminizmu w module (pierwszy prompt niżej). Uruchom ją w trybie planowania, żeby nic nie zostało zmienione. Wybierz jeden szew na każdy wycinek pracy.

  2. Zneutralizuj niedeterminizm bez zmiany logiki produkcyjnej. Zamroź zegar, ustaw ziarno albo zaślepkę losowości, ustal locale i strefę czasową, posortuj wszystko, czego kolejność nie jest gwarantowana. Wybieraj mechanizmy po stronie testu (fałszywe timery, zaślepione Math.random, TZ=UTC) zamiast zmian w kodzie. Jeśli kod czyta zegar głęboko w środku, jedyną dopuszczalną zmianą produkcyjną w tym pull requeście jest szew bez wpływu na zachowanie, na przykład parametr now ze starą wartością domyślną.

  3. Zbierz wejścia, które przechodzą przez gałęzie. Zacznij od prawdziwych danych: zanonimizowanych żądań z produkcji, listy dziwnych kont od działu wsparcia, dat na koniec miesiąca i roku. Dodaj wartości graniczne dla każdego porównania, które widzisz (zero, wartość ujemna, próg, tuż za progiem). Uruchom testy z pokryciem gałęzi na szwie i dodawaj wejścia, aż każda osiągalna gałąź zostanie wykonana. Vitest potrzebuje do tego dostawcy pokrycia w tej samej wersji co sam Vitest (zależność peer dostawcy wymaga dokładnie tej wersji):

    Okno terminala
    # Terminal (@vitest/coverage-v8 5.0.2 pasuje do Vitest 5.0.2, sprawdzone 2026-09-26)
    npm install --save-dev @vitest/coverage-v8@5.0.2
    npx vitest run --coverage --coverage.include=src/legacy/statement.js
  4. Nagraj pliki golden na nietkniętym commicie. Wygeneruj snapshoty albo pliki zatwierdzone z kodu w takiej postaci, w jakiej jest na gałęzi domyślnej. Umieść w jednym commicie szkielet testów, wejścia i pliki golden, a w opisie pull requesta podaj hash commita, na którym je nagrano.

  5. Przeczytaj pliki golden raz, szukając osobliwości. Przejrzyj każdy plik golden pod kątem wyników, które wyglądają źle: opłata naliczona od zerowej kwoty, saldo ujemne z niewłaściwym znakiem, kraj null wyceniany jak zagranica. Niczego teraz nie poprawiaj. Wpisz każdą osobliwość do QUIRKS.md obok plików golden, z właścicielem, który zdecyduje później.

  6. Udowodnij, że pliki golden mogą paść. Uruchom testy mutacyjne tylko na module legacy. Każdy ocalały mutant to albo wejście, którego jeszcze nie nagrałeś, albo mutant równoważny; dla pierwszych dodaj wejścia, drugie opisz. Szczegóły i progi są na stronie jak silna jest twoja wyrocznia.

    Okno terminala
    # Terminal, JavaScript lub TypeScript (StrykerJS 10.0.0, sprawdzone 2026-09-26)
    npm install --save-dev @stryker-mutator/core @stryker-mutator/vitest-runner
    npx stryker run --testRunner vitest --mutate src/legacy/statement.js
    # Terminal, Python (mutmut 3.8.0 w PyPI, sprawdzone 2026-09-26)
    pip install mutmut

    W mutmut ustaw w pyproject.toml klucz source_paths na pakiet legacy, a wybór testów na zestaw charakteryzacyjny, a potem uruchom mutmut run:

    # pyproject.toml (klucze z README mutmut 3.8.0)
    [tool.mutmut]
    source_paths = ["legacy/"]
    pytest_add_cli_args_test_selection = ["tests/characterization/"]
  7. Zablokuj pliki golden. Obejmij katalog z plikami golden regułą CODEOWNERS, odbierz agentowi prawo edycji (zakładki narzędzi niżej) i dodaj strażnika CI z sekcji niżej jako wymagany check. Sam pull request z nagraniem zmienia tests/characterization/, więc opiekun oznacza go etykietą behavior-change (albo strażnika scalasz osobnym pull requestem zaraz po nagraniu). Każdy kolejny pull request przechodzi przez strażnika bez etykiety. Od tej chwili refaktoryzacja, która zmienia plik golden, pada z mocy reguły. QUIRKS.md leży w strzeżonym katalogu, więc osobliwość dodana albo zmieniona po zablokowaniu też przechodzi przez pull request z etykietą behavior-change.

  8. Przekaż refaktoryzację agentowi. Definicja ukończenia brzmi: „zestaw charakteryzacyjny zielony, pliki golden niezmienione”. Pull request z refaktoryzacją niesie wynik zestawu i wynik strażnika w swoim pakiecie dowodów.

Utrwal funkcję z wieloma gałęziami testem kombinacji

Dział zatytułowany „Utrwal funkcję z wieloma gałęziami testem kombinacji”

Funkcje cenowe, podatkowe i sprawdzające uprawnienia mają mało parametrów i dużo gałęzi. Test kombinacyjny zapisuje każdą kombinację kilku wartości na parametr w jednym pliku, który człowiek przejrzy w minutę. Oto funkcja legacy:

legacy/shipping.py
def shipping_cost(weight_kg, country, express):
if weight_kg <= 0:
raise ValueError("weight must be positive")
base = 4.99 if country == "PL" else 9.99
if weight_kg > 20:
base += (weight_kg - 20) * 0.5
if express:
base *= 1.8
return round(base, 2)

Test charakteryzacyjny wybiera wartości po obu stronach każdego porównania oraz śmieciowe dane, które kod legacy dziś akceptuje:

tests/characterization/test_shipping_char.py
from approvaltests import verify_all_combinations
from legacy.shipping import shipping_cost
def test_shipping_cost_pinned():
verify_all_combinations(
shipping_cost,
[
[-1, 0, 0.1, 20, 20.01, 35], # boundaries around 0 and 20
["PL", "DE", "", None], # home, abroad, and junk the legacy code accepts
[True, False],
],
)

Pierwsze uruchomienie pytest pada i zapisuje test_shipping_char.test_shipping_cost_pinned.received.txt z jedną linią na kombinację, razem 48. Wyjątki są zapisywane jako wynik, więc gałąź z ValueError też zostaje utrwalona:

args: (-1, 'PL', True) => ValueError('weight must be positive')
...
args: (35, None, True) => 31.48
args: (35, None, False) => 17.49

Zatwierdzasz, zmieniając nazwę pliku na .approved.txt i commitując go. Kolejne uruchomienie przechodzi. Dwa szczegóły z naszego przebiegu z approvaltests 19.1.1 i pytest: pierwsze uruchomienie tworzy też pusty plik .approved.txt, więc nigdy nie commituj zatwierdzonego pliku o zerowym rozmiarze; a None jako kraj jest wyceniany jak zagranica, co jest osobliwością do QUIRKS.md, a nie czymś do poprawienia podczas refaktoryzacji.

Potem ręcznie zmutowaliśmy funkcję. Zmiana weight_kg <= 0 na < 0 wywróciła test, bo 0 jest wśród wejść. Zmiana mnożnika ekspresowego z 1.8 na 1.9 też go wywróciła. Zmiana weight_kg > 20 na >= 20 przeszła: przy dokładnie 20 kg dopłata wynosi (20 - 20) * 0.5, czyli zero w obu wersjach. To mutant równoważny, a sklasyfikowanie go odróżnia wynik mutacyjny od zgadywania.

Utrwal renderer snapshotami plikowymi i zamrożonym zegarem

Dział zatytułowany „Utrwal renderer snapshotami plikowymi i zamrożonym zegarem”

Gdy wynikiem jest dokument, osobny snapshot plikowy dla każdego przypadku wejściowego utrzymuje każdy plik golden w rozmiarze, który da się przejrzeć. Ten renderer legacy czyta zegar i Math.random, więc szkielet testów zamraża oba, zamiast edytować kod:

tests/characterization/statement.char.test.js
import { readdirSync, readFileSync } from 'node:fs';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { renderStatement } from '../../src/legacy/statement.js';
const dir = new URL('./cases/', import.meta.url);
const cases = readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
describe('renderStatement: pinned legacy behavior', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-02-01T09:00:00Z')); // freeze the clock
vi.spyOn(Math, 'random').mockReturnValue(0.123456789); // freeze the random reference
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it.each(cases)('%s', async (file) => {
const account = JSON.parse(readFileSync(new URL(file, dir), 'utf8'));
await expect(renderStatement(account)).toMatchFileSnapshot(`./golden/statement/${file}.txt`);
});
});

Każdy plik JSON w cases/ to jedno konto. Na maszynie dewelopera npx vitest run zapisuje każdy brakujący plik golden. Z ustawionym CI=true to samo polecenie wywraca każdy test, któremu brakuje pliku golden, zamiast go tworzyć (przetestowane z Vitest 5.0.2). Właśnie to zachowanie domyślne nie pozwala pustej wyroczni przejść w pipeline’ie. Plik golden nagrany dla konta w taryfie podstawowej wygląda tak:

Statement for Bo (2026-02-01)
2026-01-02 -0.01 fee 1.50
2026-01-03 0.00
Balance: -1.51
Ref: 4fzzzxjy

Obciążenie na jeden cent pociąga za sobą opłatę 1,50: to osobliwość na listę, a nie poprawka do refaktoryzacji. Gdy zmieniliśmy warunek opłaty z tx.amount < 0 na tx.amount <= 0, padł przypadek z zerową kwotą, i właśnie dlatego ten przypadek jest wśród wejść.

W wersji 5.0.2 vitest run -u przyjmuje true, "new", "all" albo "none". Każda aktualizacja przepisuje pliki golden, więc jest zmianą zachowania i nigdy nie należy do gałęzi z refaktoryzacją.

Jeśli nie da się zamrozić wartości u źródła, zastąp ją w wyniku przed porównaniem. ApprovalTests nazywa to scrubberem:

from approvaltests import Options, verify
from approvaltests.scrubbers import combine_scrubbers, create_regex_scrubber
from legacy.report import legacy_report # your legacy function under test
scrubber = combine_scrubbers(
create_regex_scrubber(r"\d{4}-\d{2}-\d{2}T[\d:.]+", "<timestamp>"),
create_regex_scrubber(r"Ref: \w+", "Ref: <ref>"),
)
verify(legacy_report(), options=Options().with_scrubber(scrubber))

Po pierwszym uruchomieniu zawsze sprawdź plik received. W naszym teście z approvaltests 19.1.1 wbudowany scrub_all_dates zostawił nietknięty znacznik czasu ISO 8601 z mikrosekundami (2026-09-26T12:14:35.785033), więc plik golden padłby przy następnym uruchomieniu. Scrubber, na którego wynik nie spojrzałeś, to zgadywanie.

Maskuj wyłącznie wartości, które różnią się między dwoma uruchomieniami tego samego kodu: znaczniki czasu, identyfikatory żądań, losowe numery referencyjne. Jeśli maskujesz pole dlatego, że zrefaktoryzowany kod formatuje je inaczej, właśnie zdecydowałeś, że ta różnica nie ma znaczenia. Taka decyzja trafia na listę osobliwości z właścicielem, a nie do wyrażenia regularnego.

Ten strażnik działa przy każdym pull requeście, niezależnie od narzędzia, które go otworzyło. Pada, gdy pull request zmienia plik golden, chyba że opiekun oznaczył go etykietą jako świadomą zmianę zachowania:

.github/workflows/characterization-guard.yml
name: characterization-guard
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
permissions:
contents: read
jobs:
goldens-unchanged:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'behavior-change') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- name: Fail if this pull request changes pinned behavior
env:
BASE_REF: ${{ github.base_ref }}
run: |
changed=$(git diff --name-only "origin/$BASE_REF...HEAD" -- tests/characterization/)
if [ -n "$changed" ]; then
printf 'Pinned behavior changed:\n%s\n' "$changed" >&2
echo "Split it into its own pull request labeled behavior-change." >&2
exit 1
fi

Etykietę mogą dodać tylko osoby z uprawnieniem triage albo write, a wpis CODEOWNERS dla tests/characterization/ i tak wymaga przeglądu właściciela wyroczni przy oznaczonym pull requeście. Ustaw ten job i uruchomienie zestawu charakteryzacyjnego jako wymagane checki w rulesecie gałęzi.

Prompty do skopiowania dla testów charakteryzacyjnych

Dział zatytułowany „Prompty do skopiowania dla testów charakteryzacyjnych”

Szkielety testów, prompty i strażnik CI są identyczne we wszystkich trzech narzędziach. Różni się sposób egzekwowania dwóch blokad: podczas nagrywania agent nie może edytować kodu produkcyjnego, a podczas refaktoryzacji nie może edytować plików golden. Polecenia Claude Code i Codex sprawdziliśmy na Claude Code 2.1.283 i Codex CLI 0.157.1 26 września 2026; funkcje Cursora sprawdziliśmy na cursor.com 28 sierpnia 2026.

Faza nagrywania. Inwentaryzację szwów uruchom w trybie planowania (/plan), a potem nagrywaj z zakazem edycji kodu produkcyjnego w commitowanym .claude/settings.json. Wiodący / wskazuje katalog główny projektu:

{
"permissions": {
"deny": ["Edit(/src/**)", "Edit(/.github/**)", "Edit(/.claude/**)"]
}
}

Faza refaktoryzacji. W osobnym commicie podmień regułę, tak żeby zablokowane były pliki golden: "deny": ["Edit(/tests/characterization/**)", "Edit(/.github/**)", "Edit(/.claude/**)"]. Reguły deny dla Edit obejmują narzędzia edycji i zapisy z powłoki, które Claude Code rozpoznaje, takie jak sed i przekierowania >, ale nie każdy skrypt, który agent napisze i uruchomi; gdy agent ma dostęp do powłoki, dodaj sandbox i hook ze strony ochrona wyroczni.

Nagrywanie bez interakcji. Sesja claude -p startuje w trybie uprawnień Manual, więc przyznaj tylko to, czego potrzebuje nagrywanie:

Okno terminala
# Terminal, z katalogu głównego repozytorium
claude -p "$(cat prompts/record-characterization.md)" \
--allowedTools "Read,Grep,Glob,Write,Edit,Bash(npx vitest run *)" \
--max-budget-usd 3

Reguła deny z .claude/settings.json nadal obowiązuje, więc Write i Edit sięgają testów, ale nie src/.

Skąd wiesz, że zestaw testów charakteryzacyjnych wystarczy?

Dział zatytułowany „Skąd wiesz, że zestaw testów charakteryzacyjnych wystarczy?”

Nie czytasz plików golden linia po linii, poza jednym przejściem w poszukiwaniu osobliwości. Sprawdzasz dowody wytworzone przez maszynę:

SprawdzenieDowódKto zatwierdza
Nagrane na nietkniętym kodzieRodzic commita z nagraniem leży na gałęzi domyślnej, a git diff na src/ w pull requeście z nagraniem jest pusty albo nie zmienia zachowaniaRecenzent pull requesta z nagraniem
DeterministyczneDwa kolejne uruchomienia z CI=true przechodzą bez zmian w plikach goldenCI
Pokrycie gałęzi szwuRaport pokrycia modułu legacy; każda niepokryta gałąź jest wymieniona i wyjaśnionaDeweloper odpowiedzialny za wycinek
Potrafią paśćWynik mutacyjny na module legacy na poziomie progu zespołu lub wyżej, każdy ocalały mutant sklasyfikowany jako brakujące wejście albo mutant równoważnyDeweloper; próg ustala tech lead
Osobliwości mają właścicielaQUIRKS.md wymienia każdy podejrzany wynik z właścicielem i datą decyzjiWłaściciel produktu albo domeny
ZablokowaneWpis CODEOWNERS, reguła deny albo profil agenta i job strażnika jako wymagany checkTech lead

Dopiero gdy wszystkie sześć warunków jest spełnionych, zestaw liczy się jako wyrocznia dla refaktoryzacji wykonanej przez agenta. Pull request z refaktoryzacją nie wymaga wtedy czytania przeniesionego kodu linia po linii pod kątem zachowania: wynik zestawu, wynik strażnika i wynik mutacyjny trafiają do pakietu dowodów, a recenzent czyta właśnie je.

Pliki golden nagrano po zmianie. Agent najpierw zrefaktoryzował, potem wygenerował snapshoty i wszystko świeci na zielono. Wyjście: wróć do commita z gałęzi domyślnej, uruchom na nim zestaw i każdy błąd traktuj jako zmianę zachowania wprowadzoną przez refaktoryzację. Zrób z linii „nagrane na commicie X” obowiązkowy element pull requesta z nagraniem.

Ktoś uruchomił flagę aktualizacji. Gałąź z refaktoryzacją zawiera wynik vitest -u albo --snapshot-update, a strażnika obeszła etykieta. Wyjście: cofnij zmiany w plikach golden, uruchom zestaw ponownie na zrefaktoryzowanym kodzie, a każdą prawdziwą zmianę zachowania wydziel do osobnego pull requesta z etykietą, zatwierdzanego przez właściciela wyroczni.

Pliki golden są niestabilne. Przechodzą na jednej maszynie, a w CI padają przez strefę czasową, locale, kolejność iteracji po mapie albo znacznik czasu, który przeoczył scrubber. Wyjście: uruchom zestaw dwa razy z CI=true i TZ=UTC, porównaj oba przebiegi i każdą różnicę zamroź albo zamaskuj u źródła.

Scrubber ukrywa prawdziwe zachowanie. Maska dodana, żeby „naprawić” niestabilny plik golden, połyka też pole, które zmieniła refaktoryzacja. Wyjście: każdy scrubber ma komentarz, jaką różnicę między uruchomieniami usuwa; prompt audytu osobliwości wymienia, co każdy z nich mógłby ukryć.

Szew jest za nisko. Testy utrwalają prywatne funkcje pomocnicze albo kolejność wywołań, więc każda zmiana struktury je wywraca, a agent ma pokusę, żeby je wygenerować od nowa. Wyjście: przenieś szew wyżej, na funkcję publiczną, odpowiedź HTTP albo plik wynikowy, i usuń niskopoziomowe pliki golden, gdy wysokie pokrywają te same gałęzie.

Pliki golden są za duże, żeby je przejrzeć. Jeden golden master na 30 000 linii ukrywa zmienioną linię w szumie. Wyjście: podziel wynik na przypadki wejściowe, tak jak robi to szkielet testów w Vitest, żeby błąd wskazywał przypadek, a diff mieścił się na ekranie.

Utrwalony błąd na zawsze staje się specyfikacją. Nikt nie jest właścicielem QUIRKS.md, więc błędne zachowanie przeżywa przepisanie systemu. Wyjście: każda osobliwość ma właściciela i datę; jej naprawa to osobny pull request z etykietą behavior-change, w którym plik golden zostaje zaktualizowany i świadomie zatwierdzony.

Najczęstsze pytania

Czym jest test charakteryzacyjny?

Test charakteryzacyjny zapisuje to, co istniejący kod robi dziś, a nie to, co powinien robić, i pada, gdy ten wynik się zmieni. Typowe formy to snapshoty, testy zatwierdzające (approval tests) i golden master. Dają nieprzetestowanemu kodowi legacy wyrocznię, zanim agent zacznie go refaktoryzować.

Dlaczego nie pozwolić agentowi napisać testów po refaktoryzacji?

Testy napisane do nowego kodu opisują nowy kod razem z jego błędami i przechodzą z definicji. Testy charakteryzacyjne nagrywa się na nietkniętym kodzie w osobnej zmianie, więc opisują zachowanie, od którego zależą użytkownicy, a refaktoryzacja musi je odtworzyć.

Skąd wiadomo, że zestaw testów charakteryzacyjnych jest wystarczający?

Każdy plik golden nagrano na nietkniętym commicie, wejścia pokrywają gałęzie szwu, testy mutacyjne na module legacy zabijają istotne mutanty, każdy ocalały mutant jest sklasyfikowany, a pliki golden są zablokowane przed edycją przez agenta w sesji i w CI.