# Zielony test niczego nie dowodzi: 500 notatek mojego agenta AI

> Mój agent AI zapisuje notatkę po każdym błędzie. Po ponad 500 notatkach największa lekcja dotyczy testów: pięć prawdziwych przypadków, w których zielona suita ukryła błąd.

- URL: https://www.maciejbednarczyk.com/blog/green-test-proves-nothing
- Author: Maciej Bednarczyk
- Language: pl
- Published: 2026-11-13

## Zeszyt błędów

Większość kodu w Cmssy pisze Claude Code, a ja go recenzuję. To działa dzięki jednemu nawykowi: za każdym razem, gdy coś pójdzie źle, agent zapisuje notatkę. Co się stało, dlaczego i jak zastosować tę lekcję następnym razem.

Notatki leżą w moim vaulcie Obsidiana, a agent czyta indeks danego obszaru, zanim go dotknie. Jest ich już ponad 500.

Duża część dotyczy testów. Z różnych stron dochodzą do tego samego zdania: zielony test niczego nie dowodzi, dopóki nie zobaczysz, jak pada. Poniżej pięć przypadków z tego zeszytu, z datami. Dwa z nich były dziurami bezpieczeństwa w moim własnym produkcie. Obie są naprawione.

## Jak wygląda notatka

Każda notatka ma ten sam układ: jednozdaniowy opis, konkretny incydent, przyczyna i sposób stosowania reguły. Ta jest skrócona.

```markdown
---
name: guard-must-be-proven-to-refuse
description: Test, którym sam ustawiam nagłówek i patrzę na 200, dowodzi że strażnik WPUSZCZA - nigdy że ODMAWIA
type: feedback
---

**Why:** test wpuszczenia jest zielony także wtedy, gdy strażnika nie ma wcale.
Dowód istnienia strażnika to wyłącznie żądanie, które ma dostać odmowę - i ją dostaje.

**How to apply:**
- Każdy strażnik = para testów: uprawnione żądanie przechodzi, nieuprawnione dostaje 401/403.
- Nazwij w teście dokładnie podrobione żądanie.
- Zanim napiszesz w PR "nie wystawia tego na internet", wyślij to żądanie na żywy deploy.
```

* * *

## 1\. Strażnik, który szukał napisu

29 lipca. Agent napisał dwa strażniki źródłowe wokół mappera pól. Oba były zielone. Oba były bezwartościowe.

Pierwszy sprawdzał, czy plik zawiera nazwę `toPropertyField`. W uproszczeniu:

```typescript
it("uses the shared field mapper", () => {
  const source = readFileSync(mapperPath, "utf8");
  expect(source).toContain("toPropertyField");
});
```

Przechodził przy w pełni przywróconym, zepsutym mapperze, bo `toPropertyFields` zawiera tę nazwę jako podciąg. Drugi strażnik wykrywał tłumaczenie płaskie na zagnieżdżone, a prawdziwym błędem było **pomijanie kluczy**. Agent recenzujący udowodnił to, pisząc szósty mapper, który miał ten błąd i dalej przechodził.

Notatka zapisana potem mówi, że test, który nie potrafi zaświecić się na czerwono, jest dekoracją i zwiększa ryzyko, bo wygląda jak pokrycie. Sprawdzaj właściwość („nic, co wywołujący może ustawić, nie ginie”), a nie obecność symbolu. A gdy błąd polega na *braku* czegoś, skan źródeł z definicji go nie zobaczy. Wtedy potrzebny jest niezmiennik, na przykład round-trip.

* * *

## 2\. Test, który bronił podatności

4 października. Testy logowania w panelu admina zawierały to:

```typescript
it("never calls the backend - the token is already proof", async () => {
  await authorize("oauth-token", { accessToken: accessToken() });
  expect(authFetch).not.toHaveBeenCalled();
});
```

To zdanie było treścią podatności. Callback logowania jest publicznym endpointem, a `authorize` tylko dekodowało środkowy człon JWT. Bez sprawdzenia podpisu, bez sprawdzenia wystawcy. Każdy mógł złożyć token ręcznie i dostać ważną sesję admina.

Suita była zielona z jednego powodu: test *wymagał*, żeby backend nie był pytany. Token jest dowodem tylko dla kogoś, kto wie, kto go wybił. Publiczny endpoint wierzy wołającemu na słowo.

Poprawka weszła 7 października. Został sposób czytania testów w kodzie logowania i tenantów: pytanie brzmi „co to zdanie twierdzi o świecie”, a nie „czy przechodzi”. Test, który sprawdza, że wywołanie weryfikujące **nie** nastąpiło, to sygnał ostrzegawczy. Taki test się kasuje i nazywa w opisie pull requesta, a nie łagodzi. Złagodzony zostawia tezę w repo.

* * *

## 3\. Strażnik, o którym wiadomo było tylko, że wpuszcza

10 września. W SDK 16.7.0 wyszła trasa z danymi bloków, a opis pull requesta mówił, że „odpowiada tylko zweryfikowanemu żądaniu edytora”. Nie odpowiadała tylko jemu. Strażnik czytał nagłówek `x-cmssy-edit`. Proxy nadpisuje ten nagłówek tylko na trasach objętych matcherem, a standardowy matcher wycina `/api/*`. Na trasie API był to po prostu nagłówek od wołającego.

Przeszło, bo test lokalny ustawiał nagłówek ręcznie, dostawał 200 i zapisywał sukces. To mierzy, że mechanizm wpuszcza żądanie. Nikt nie zapytał, czy jakieś odrzuca.

<table><thead><tr><th>Sprawdzenie</th><th>Czego dowodzi</th></tr></thead><tbody><tr><td>Żądanie z nagłówkiem zwraca 200</td><td>Trasa wpuszcza żądania. Prawda także wtedy, gdy strażnika nie ma</td></tr><tr><td>GET zwraca 405</td><td>Trasa jest zamontowana</td></tr><tr><td>POST z podrobionym nagłówkiem zwraca 403</td><td>Strażnik odmawia</td></tr></tbody></table>

Każdy strażnik to teraz para testów: uprawnione żądanie przechodzi, nieuprawnione dostaje 401 albo 403. Sam pierwszy test jest wart zero. Notatka dodaje też regułę projektową: sygnał autoryzacji wybijaj po zweryfikowanej stronie i nieś go w żądaniu. Nagłówek ustawiany przez middleware jest wiarygodny tylko na trasach, które to middleware faktycznie obsługuje, więc sprawdź matcher, a nie założenie.

* * *

## 4\. Rada z review, która osłabiła test

Recenzent zaproponował rozsądnie brzmiącą zmianę: nie sprawdzaj komunikatu błędu graphql-js, bo wersja minor go przepisze. Sprawdź, że `data` jest puste i że jest jeden błąd.

Po zastosowaniu rady mutant `t.int → t.float` przeżył. Przy `Float` ułamek dociera do strażnika w serwisie, który też zwraca jeden błąd i puste `data`. Asercja przestała rozróżniać dwie warstwy.

Rada z review dotyczy zwykle kruchości, a nie siły. Kruchy komunikat bywa jedynym świadkiem, który rozróżnia dwie warstwy padające tak samo. Dlatego każda poprawka po review przechodzi ten sam próg co pierwotna asercja: po zmianie odpal mutanta jeszcze raz. Jeśli przeżył, rada była błędna.

* * *

## 5\. Agent, który padł między mutacją a przywróceniem

6 października, pull request #324 w cmssy-sdk. Dwóch agentów recenzujących padło na `HTTP 529 Overloaded`. Jeden z nich właśnie zaczynał eksperymenty mutacyjne. Padł po zmianie kodu, a przed jego przywróceniem, więc zmutowany typ (`formId: string` zamieniony na `formId?: string | null`) został w worktree. Raportu nie było, bo agent nie wrócił.

Bez sprawdzenia kolejny agent recenzowałby zmutowany kod. Wyszły z tego trzy reguły. `git status --short` po każdym agencie, także po tym, który padł albo przekroczył czas. Agenty mutujące działają pojedynczo. Commit przed wpuszczeniem agenta, żeby przywrócenie było dokładne.

* * *

## Reguła: zmutuj na czerwono

Wszystkie pięć przypadków kończy się tą samą procedurą. Zanim test zostanie uznany za zabezpieczenie:

1.  Zepsuj dokładnie tę właściwość, której test ma bronić.
2.  Uruchom test i zobacz czerwone.
3.  Przywróć kod.
4.  Podaj wynik mutacji, a nie samo „zielone”.

W samej procedurze siedzą dwie pułapki.

**Mutacja mogła w ogóle nie trafić.** 26 września mutacja regexem wycięła `"boolean"` z pliku, w którym ten token występuje trzy razy. Trafiła w inną tablicę niż zamierzona, testy zostały zielone i wyglądało to na mutanta, który przeżył, czyli na lukę w testach. To była mutacja, która nie dotknęła celu. Po mutacji czytaj wartość, a nie plik.

**Raport subagenta to hipoteza.** W jednej rundzie review wróciło dwanaście znalezisk. Pięć było prawdziwymi błędami, a trzy były błędne, przy czym te błędne były konkretne i wiarygodne. „Naprawienie” ich zepsułoby działający kod. W innym przypadku dziesięciu agentów zgodziło się jednomyślnie i nie miało racji. Każde znalezisko jest sprawdzane w kodzie, zanim trafi do planu albo pull requesta.

## Co to zmieniło w repo

Notatki zamieniły się w mechanizmy. Hook odrzuca asercje, które nie mogą nie przejść. Testy-strażnicy mają w dokumentacji jedną zasadę domu: nowy strażnik jest udowadniany mutacją, a zielony przy pierwszym uruchomieniu niczego nie dowodzi. Ślepa recenzja osobnego agenta idzie przed otwarciem pull requesta, a nie po.

Agent AI nie robi mniej błędów niż ja. Robi je szybciej. Zeszyt jest sposobem na to, żeby ten sam błąd nie zdarzył się dwa razy, a testowi można ufać dopiero wtedy, gdy zobaczyłem, jak pada.