Ochrona wyroczni: testy, których agent nie może zmienić
Ochrona wyroczni polega na tym, że sprawdzenia decydujące o „gotowe” (testy, snapshoty, konfiguracja testów i workflowy CI) są poza zasięgiem agenta w zadaniu, które oceniają. Ochrona ma warstwy: reguły deny i sandbox w sesji, CODEOWNERS, wymagany check CI spoza diffa, scenariusze holdout poza repozytorium oraz audyt wykrywający osłabione testy.
Poprosiłeś agenta o naprawienie padającego testu koszyka. Po dwudziestu minutach suita jest zielona, podsumowanie mówi „naprawiłem błąd zaokrąglania”, a diff ma 40 linii. Cztery z nich są w teście: toBe(19.99) zmieniło się w toBeCloseTo(20, 0). Nikt nie kłamał celowo. Agent miał sprawić, żeby test przeszedł, a edycja testu była najkrótszą drogą. Ta strona zamyka tę drogę i sprawia, że zmiana testu, gdy naprawdę jest potrzebna, nie przechodzi po cichu.
Co zyskujesz, blokując wyrocznię
Dział zatytułowany „Co zyskujesz, blokując wyrocznię”- Mapę wyroczni jako wzorce ścieżek do użycia w każdej warstwie.
- Blokady sesji do wklejenia: reguły deny, ustawienia sandboxa i hook
PreToolUsedla Claude Code; profil uprawnień Codex przetestowany na Codex CLI 0.157.1; oraz to, co Cursor potrafi, a czego nie potrafi wymusić. - Blokady w forge i w CI: blok
CODEOWNERS, wymagany workflow, którego pull request nie może edytować, i job uruchamiający testy z gałęzi bazowej na nowym kodzie. - Job holdout, który uruchamia scenariusze akceptacyjne nigdy niewidziane przez agenta i raportuje tylko identyfikatory i liczby.
- Skrypt audytu osłabień, przetestowany na realnie osłabionych zmianach.
- Cztery prompty do skopiowania i ćwiczenie red-team, które po każdej aktualizacji narzędzi dowodzi, że blokada trzyma.
Dlaczego agent edytuje test zamiast kodu
Dział zatytułowany „Dlaczego agent edytuje test zamiast kodu”„Spraw, żeby testy przeszły” jest spełnione tak samo przez poprawkę kodu, jak przez zmianę testu, a test bywa mniejszą edycją. Polecenie „nigdy nie zmieniaj testów” w CLAUDE.md albo AGENTS.md to tekst, który model waży wobec zadania, a nie reguła, którą coś egzekwuje. Jak ujmuje to rejestr autonomii na poziomie 5: jeśli agent może edytować wyrocznię, wyrocznia jest tylko sugestią. Ta strona zakłada, że pętla informacji zwrotnej już istnieje (etap testowania), a jej siłę zostawia stronie jak silna jest twoja wyrocznia; tutaj chodzi tylko o to, żeby trzymać ją poza zasięgiem agenta.
Co wchodzi w skład wyroczni?
Dział zatytułowany „Co wchodzi w skład wyroczni?”Wyrocznia to wszystko, czego zmiana może zamienić czerwony przebieg w zielony bez poprawy kodu. To więcej niż katalog tests/:
| Składnik wyroczni | Przykłady | Jak się ją osłabia |
|---|---|---|
| Kod testów | tests/**, **/*.test.ts, **/*_test.go, test_*.py | Poluzowana asercja, usunięty przypadek, .skip, xfail |
| Zapisane oczekiwania | **/__snapshots__/**, pliki golden, fixtures | Snapshot wygenerowany ponownie z -u, fixture dopasowany do błędu |
| Konfiguracja testów i pokrycia | vitest.config.ts, jest.config.*, pytest.ini, .coveragerc | Pliki wyłączone z przebiegu, obniżony próg pokrycia |
| Bramki statyczne | tsconfig.json, konfiguracja lintera, // @ts-ignore, # noqa | Wyłączony strict, wyłączona reguła, dopisany komentarz tłumiący |
| Pipeline | .github/workflows/** | Usunięty krok testów albo continue-on-error |
| Same blokady | CODEOWNERS, .claude/, .codex/ | Reguła ochrony usunięta razem ze zmianą |
Komentarzy tłumiących w plikach produkcyjnych nie da się zablokować ścieżką; wyłapuje je audyt osłabień opisany dalej.
Pięć warstw ochrony wyroczni
Dział zatytułowany „Pięć warstw ochrony wyroczni”Żadna pojedyncza kontrola nie obejmuje każdego agenta i każdego obejścia, więc układasz pięć warstw. Każda łapie to, co przepuściła poprzednia.
| Warstwa | Gdzie żyje | Co zatrzymuje | Co przez nią przechodzi | Których agentów wiąże |
|---|---|---|---|---|
| 1. Blokada sesji | Ustawienia agenta na maszynie, na której działa | Edycję w chwili próby, z komunikatem, na który agent może zareagować | Wszystko, czego ustawienia nie obejmują; agentów w chmurze z innymi ustawieniami | Jedno narzędzie, jedna maszyna |
| 2. Blokada w forge | CODEOWNERS plus reguła „Require review from Code Owners” | Merge zmiany wyroczni bez akceptacji wskazanego właściciela | Nic, jeśli reguła jest włączona, a plik poprawny | Każdego agenta i każdego człowieka |
| 3. CI poza diffem | Wymagany workflow w innym repozytorium | Pull request, który przepisuje własny pipeline | Słabe testy: CI uruchamia to, co istnieje | Każdy pull request |
| 4. Holdouty | Osobne repozytorium, którego agent nie może czytać | Kod dostrojony do widocznych testów | Zachowanie, którego nie opisuje żaden scenariusz | Każdy pull request |
| 5. Audyt osłabień | Job CI nad diffem | Zmiany testów, które wyglądają na uzasadnione, a luzują sprawdzenie | Subtelne osłabienie semantyczne; to czyta właściciel kodu | Każdy pull request |
Warstwa 1 daje najszybszą informację zwrotną. Warstwy 2–5 trzymają wtedy, gdy agent działa w chmurze, w terminalu kogoś innego albo w narzędziu, którego nie skonfigurowałeś.
Blokowanie wyroczni krok po kroku
Dział zatytułowany „Blokowanie wyroczni krok po kroku”-
Zmapuj wyrocznię. Uruchom w repozytorium pierwszy prompt poniżej. Zwraca wyrocznię jako listę ścieżek i wzorców. Przejrzyj ją raz; tej samej listy używasz w każdej warstwie.
-
Zablokuj ją w sesji. Zastosuj ustawienia swojego narzędzia z zakładek w następnej sekcji. Zacommituj je, żeby każdy programista i każde uruchomienie agenta w CI dziedziczyło blokadę.
-
Nadaj wyroczni właścicieli. Dodaj blok
CODEOWNERSi włącz „Require review from Code Owners” dla gałęzi domyślnej. -
Wynieś decydujący check poza diff. Umieść joby testów, wyroczni bazowej i audytu w workflowie, którego pull request nie może edytować, i wymuś go regułą organizacji (ruleset).
-
Dodaj holdouty. Załóż prywatne repozytorium scenariuszy i job holdout. Zacznij od 10–20 scenariuszy dla przepływów, na których zarabiasz.
-
Rozdziel zmiany testów od zmian kodu. Gdy zachowanie musi się zmienić, sesja pisząca testy tworzy nowe testy, właściciel kodu je zatwierdza, a osobna sesja implementacyjna sprawia, że przechodzą, przy zablokowanych testach.
-
Zaatakuj własną blokadę. Wykonaj ćwiczenie red-team z końca tej strony i powtarzaj je po każdej aktualizacji agenta albo CI.
Jak zablokować pliki testowe w Claude Code, Codex i Cursorze?
Dział zatytułowany „Jak zablokować pliki testowe w Claude Code, Codex i Cursorze?”Każde z trzech narzędzi egzekwuje blokadę sesji inaczej, a jednego nie da się dziś w pełni zweryfikować. Użyj zakładki dla każdego narzędzia, z którego korzysta zespół.
W .claude/settings.json użyj trzech mechanizmów: reguł deny dla wbudowanych narzędzi plikowych, sandboxa dla wszystkiego, co zapisuje polecenie powłoki, oraz hooka PreToolUse, który mówi agentowi, co zrobić zamiast edycji. Sprawdzone 26 września 2026 z Claude Code 2.1.283 i dokumentacją uprawnień, sandboxa i hooków.
{ "permissions": { "deny": [ "Edit(tests/**)", "Edit(**/*.test.ts)", "Edit(**/__snapshots__/**)", "Edit(/vitest.config.ts)", "Edit(/.github/**)", "Edit(/CODEOWNERS)" ] }, "sandbox": { "enabled": true, "allowUnsandboxedCommands": false, "filesystem": { "denyWrite": ["./tests", "./.github", "./CODEOWNERS", "./vitest.config.ts"] } }, "hooks": { "PreToolUse": [ { "matcher": "Edit|Write|NotebookEdit", "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-oracle.sh" } ] } ] }}Dlaczego wszystkie trzy:
- Reguły deny blokują w każdym trybie uprawnień, także w
bypassPermissions, a reguła allow nie wytnie z nich wyjątku. RegułyEdit(...)obejmują teżWrite; reguła ścieżki zapisana jakoWrite(...)jest akceptowana, ale nigdy nie jest sprawdzana. Jako reguła denyEdit(tests/**)pasuje do katalogutestsna dowolnej głębokości, a wiodący/wEdit(/vitest.config.ts)kotwiczy ścieżkę w katalogu głównym projektu. Reguły deny dlaEditdziałają też na polecenia powłoki, które Claude Code rozpoznaje, takie jaksed,teei przekierowania>. - Sandbox zamyka lukę, którą reguły deny zostawiają: według dokumentacji nie obejmują one „arbitrary subprocesses that read or write files indirectly, like a Python or Node script that opens files itself”.
sandbox.filesystem.denyWriteegzekwuje system operacyjny dla każdego polecenia powłoki i jego procesów potomnych. Uwaga na inną składnię ścieżek: tu./testsjest względne wobec projektu, w regule uprawnień tę rolę pełni/tests. Sandbox działa na macOS, Linuksie i WSL2, nie na natywnym Windowsie.allowUnsandboxedCommands: falsenie pozwala ponowić zablokowanego polecenia poza sandboxem. - Hook zamienia gołą odmowę we wskazówkę. Agent czyta stderr blokady z kodem wyjścia 2 i zmienia kierunek:
#!/usr/bin/env bash# .claude/hooks/protect-oracle.sh: refuse edits to the files that decide "done"command -v jq >/dev/null || { echo "protect-oracle: jq missing" >&2; exit 2; }file=$(jq -r '.tool_input.file_path // .tool_input.notebook_path // empty')rel=${file#"$CLAUDE_PROJECT_DIR"/}case "$rel" in tests/*|*/tests/*|*.test.ts|*.spec.ts|*/__snapshots__/*|.github/*|CODEOWNERS|vitest.config.*|.claude/*) echo "BLOCKED: $rel is part of the oracle for this task. Change the code under test instead." \ "If you believe the test itself is wrong, stop and write your reasoning to TEST_DISPUTE.md." >&2 exit 2 ;;esacexit 0Skrypt wymaga jq w PATH; bez niego linia kontrolna blokuje każdą edycję komunikatem „jq missing”, zamiast po cichu przepuszczać wszystkie (reguły deny i tak blokują). Wzorzec */tests/* odpowiada dopasowaniu reguły deny na dowolnej głębokości, więc w monorepo packages/api/tests/x.ts dostaje ten sam komunikat. Nadaj skryptowi prawo wykonywania (chmod +x .claude/hooks/protect-oracle.sh). Blokuje kod wyjścia 2; kod 1 to błąd nieblokujący i edycja przechodzi. Katalog .claude/ jest też ścieżką chronioną: w trybie Manual, acceptEdits i w trybie auto zapis do niego wymaga twojej zgody albo trafia do klasyfikatora zamiast automatycznej akceptacji, więc agent nie usunie po cichu własnej blokady.
Codex egzekwuje blokadę przez profil uprawnień (beta, Codex CLI 0.138.0 lub nowszy), który w sandboxie ustawia katalogi wyroczni jako tylko do odczytu. Dodaj go do ~/.codex/config.toml na każdej maszynie programisty:
default_permissions = "locked-oracle"
[permissions.locked-oracle]extends = ":workspace"
[permissions.locked-oracle.filesystem.":project_roots"]"tests" = "read"".github" = "read"".codex" = "read""CODEOWNERS" = "read""vitest.config.ts" = "read"Przetestowaliśmy ten profil 26 września 2026 na Codex CLI 0.157.1 pod Linuksem, przez codex sandbox. Zapisy do src/ się udały. Zapisy, rm i mv w tests/, .github/, .codex/ i CODEOWNERS kończyły się błędem „Read-only file system” albo „Device or resource busy”. Żeby wybrać profil dla jednego uruchomienia zamiast domyślnie:
# Terminal lub CI, Codex CLI 0.157.1codex exec -c default_permissions=locked-oracle "Make the failing test in tests/checkout.test.ts pass by changing src/ only."Na świeżym runnerze CI tego profilu nie ma, więc jego wybór kończy się błędem. Zapisz blok [permissions.locked-oracle] do $CODEX_HOME/config.toml na runnerze, zanim uruchomisz codex exec, albo przypnij go przez requirements.toml.
Trzy ograniczenia, o których trzeba wiedzieć:
- Wzorce glob działają tylko z
deny. W 0.157.1 wpis"**/*.test.ts" = "read"zatrzymuje Codex komunikatem: „filesystem glob path**/*.test.tsonly supportsdenyaccess; use an exact path or trailing/**forreadsubtree access”. Glob zdenyblokuje też odczyt, a agent musi testy czytać. Pliki testowe leżące obok kodu wymagają więc katalogu, dokładnej ścieżki albo warstw CI. - Nie łącz profilu z
--sandboxani--approve-for-me. OpenAI pisze, że profile i starszy system sandboxa „do not compose”. - Raz sprawdź narzędzie edycji. Testowaliśmy zapisy z powłoki. Zanim zaufasz profilowi, poproś agenta na gałęzi roboczej o edycję pliku w
tests/i potwierdź, że edycja zostaje odrzucona (ćwiczenie red-team poniżej).
Codex ma też hooki: 12 zdarzeń, w tym PreToolUse i Stop. Hooki projektu wymagają zapisanego zaufania, które przeglądasz przez /hooks, więc jeśli chcesz tego samego komunikatu, przenieś logikę skryptu z Claude Code na schemat hooków Codex. W zespole administrator może przypiąć profil kluczem permission_profile w requirements.toml, którego programiści nie nadpiszą.
Cursor ma hooki, które „run before or after defined stages of the agent loop and can observe, block, or modify behavior” (cursor.com/docs/hooks, sprawdzone 28 sierpnia 2026). Hook blokujący edycje plików pasujących do wzorców wyroczni to odpowiednik skryptu z Claude Code.
26 września 2026 nie mogliśmy ponownie sprawdzić nazw zdarzeń hooków, pliku konfiguracyjnego ani ustawień trybów uruchamiania w Cursorze, bo cursor.com był niedostępny z naszego środowiska. Nazwy zdarzeń i schemat wejścia weź z dokumentacji hooków Cursora w dniu, w którym budujesz, i przenieś tę samą logikę case.
Cloud Agents w Cursorze „run in isolated VMs in the cloud with full development environments” (sprawdzone 28 sierpnia 2026), więc blokada ustawiona na twoim laptopie ich nie obejmuje. W Cursorze traktuj blokadę sesji jako szybką informację zwrotną, a egzekwowanie opieraj na warstwach 2–5. Bugbot „reviews pull requests and identifies bugs, security issues, and code quality problems”: przydaje się jako dodatkowy czytelnik zmian wyroczni, ale to recenzent, a nie blokada.
Nadaj wyroczni właściciela przez CODEOWNERS
Dział zatytułowany „Nadaj wyroczni właściciela przez CODEOWNERS”Blokada w forge to jedyna warstwa, która wiąże każdą powierzchnię agenta: lokalne CLI, agenta w chmurze, agenta uruchamianego z GitHuba i człowieka. Zmianę wyroczni da się zmergować tylko za zgodą wskazanego zespołu.
# .github/CODEOWNERS: changes to the oracle need a named owner's approval/tests/ @acme/test-owners*.test.ts @acme/test-owners**/__snapshots__/ @acme/test-owners/vitest.config.ts @acme/test-owners/.github/ @acme/platform/.claude/ @acme/platform/.codex/ @acme/platformNastępnie włącz Require review from Code Owners w regule ochrony gałęzi albo w rulesecie dla gałęzi domyślnej. O tym, czy to działa, decydują cztery zachowania opisane w dokumentacji GitHuba:
- Obowiązuje
CODEOWNERSz gałęzi bazowej. Pull request, który edytujeCODEOWNERS, nie zmieni recenzentów samego siebie, ale może osłabić plik dla wszystkich kolejnych. Linia/.github/sprawia, że zmianaCODEOWNERSteż wymaga akceptacji zespołu platformy. - Niepoprawne linie są po cichu pomijane. Literówka zostawia ścieżkę bez właściciela. Po każdej edycji sprawdź widok błędów pliku na GitHubie albo endpoint REST zwracający błędy
CODEOWNERS. - Wystarczy jeden właściciel. Jeśli ścieżka ma kilku właścicieli, reguła jest spełniona po jednej akceptacji. Ogranicz właścicieli wyroczni do zespołu, który naprawdę czyta testy.
- Właściciele muszą mieć uprawnienia zapisu do repozytorium, żeby dostać prośbę o review.
Jeśli CODEOWNERS nikogo nie poprosił o review, przyczyną jest jedno z powyższych: pominięta linia, właściciel bez prawa zapisu albo gałąź docelowa z innym CODEOWNERS. Sprawdź konfigurację jednorazowym pull requestem, który dotyka tests/. Ten sam wzorzec, CODEOWNERS plus regułę review właścicieli kodu, GitHub zaleca dla plików konfiguracyjnych, które mógłby zmienić jego Copilot cloud agent.
Uruchamiaj decydujący check tam, gdzie pull request go nie zmieni
Dział zatytułowany „Uruchamiaj decydujący check tam, gdzie pull request go nie zmieni”Workflow pull_request uruchamia się z merge commita pull requesta. Wykonuje się więc ten YAML, który zawiera pull request, i agent, który usunie krok testów z .github/workflows/ci.yml, nadal dostanie zielony check o nazwie test. CODEOWNERS na .github/ sprawia, że taka edycja jest widoczna. Wymagany workflow odbiera tę możliwość całkowicie.
W rulesecie organizacji reguła Require workflows to pass before merging wymusza plik workflowu z innego repozytorium. Pull request nie może go edytować, bo tego pliku nie ma w diffie. Tam umieść joby wyroczni:
# acme/ci-policy/.github/workflows/oracle.yml, required by an org rulesetname: oracleon: pull_requestpermissions: contents: read pull-requests: read # lets oracle-approved.sh list the pull request's reviewsjobs: base-oracle: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: { fetch-depth: 0, persist-credentials: false } - uses: actions/checkout@v7 with: repository: acme/ci-policy path: .policy token: ${{ secrets.POLICY_READ_TOKEN }} persist-credentials: false - id: owner name: Check for an oracle owner's approval of this exact commit, before any pull request code runs env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} PR: ${{ github.event.pull_request.number }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: bash .policy/oracle-approved.sh >> "$GITHUB_OUTPUT" - name: Run the base branch's tests and runner config against this pull request's code env: OWNER_APPROVED: ${{ steps.owner.outputs.approved }} run: | git checkout ${{ github.event.pull_request.base.sha }} -- tests/ vitest.config.ts npm ci npx vitest run tests/ || { [ "$OWNER_APPROVED" = true ] || exit 1 echo "::warning::Old expectations fail; accepted because an oracle owner approved this commit." } oracle-audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: { fetch-depth: 0, persist-credentials: false } - uses: actions/checkout@v7 with: repository: acme/ci-policy path: .policy token: ${{ secrets.POLICY_READ_TOKEN }} persist-credentials: false - id: owner env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} PR: ${{ github.event.pull_request.number }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: bash .policy/oracle-approved.sh >> "$GITHUB_OUTPUT" - env: OWNER_APPROVED: ${{ steps.owner.outputs.approved }} run: | bash .policy/oracle-audit.sh ${{ github.event.pull_request.base.sha }} || { [ "$OWNER_APPROVED" = true ] || exit 1 echo "::warning::Weakening signal accepted because an oracle owner approved this commit." }Oba joby uruchamiają kod kontrolowany przez pull request (skrypty cyklu życia npm ci, przebieg testów), więc blok permissions na poziomie workflowu ogranicza ich GITHUB_TOKEN do czytania repozytorium i jego pull requestów. Job base-oracle przywraca każdy plik testowy, który pull request zmienił albo usunął, oraz konfigurację runnera do wersji z gałęzi bazowej, zostawia testy dodane przez pull request i uruchamia całość na nowym kodzie. Porażka pokazuje dokładnie, które stare oczekiwania zmiana łamie. Każde z nich musi się znaleźć w zmianie specyfikacji (spec delta) pull requesta jako zamierzona zmiana zachowania; porażka bez odpowiadającej linii to regresja albo osłabiony test. Dopasuj tests/ i polecenie runnera do swojego układu.
Dokumentacja rulesetów GitHuba dodaje dwa ograniczenia. Reguła blokuje bezpośrednie pushe, więc stosuj ją tylko do gałęzi zmienianych przez pull requesty. Prywatne repozytorium z workflowem obsłuży tylko repozytoria prywatne, wewnętrzne tylko wewnętrzne i prywatne, a publiczne wszystkie. Jeśli repozytorium z polityką jest prywatne albo wewnętrzne, zezwól też na dostęp do jego workflowów z innych repozytoriów w Settings → Actions → General → Access tego repozytorium, bo inaczej wymagany workflow nigdy się nie uruchomi.
Przepuść zatwierdzoną zmianę wyroczni
Dział zatytułowany „Przepuść zatwierdzoną zmianę wyroczni”Uzasadniona zmiana zachowania z definicji łamie stare oczekiwania, więc oba joby potrzebują ścieżki zaliczenia, bo inaczej każda zamierzona zmiana testu byłaby zablokowana na zawsze. Tą ścieżką jest zatwierdzenie, nie etykieta: etykietę może dodać każdy z dostępem triage, także token agenta. oracle-approved.sh leży w repozytorium z polityką obok skryptu audytu i uruchamia się przed jakimkolwiek kodem pull requesta:
#!/usr/bin/env bash# oracle-approved.sh: print approved=true only if a listed oracle owner's latest review# approves this exact head commit. Any API or parse error fails the step (fail closed).set -euo pipefail: "${REPO:?}" "${PR:?}" "${HEAD_SHA:?}"owners=$(grep -vE '^\s*(#|$)' .policy/oracle-approvers.txt | jq -R . | jq -sc .)reviews=$(gh api --paginate "repos/$REPO/pulls/$PR/reviews" | jq -s 'add // []')ok=$(jq -r --argjson owners "$owners" --arg sha "$HEAD_SHA" ' [ .[] | select(.state != "COMMENTED" and .state != "PENDING") ] | group_by(.user.login) | map(max_by(.submitted_at)) | any(.[]; .state == "APPROVED" and .commit_id == $sha and (.user.login as $u | $owners | index($u)))' <<<"$reviews")if [ "$ok" = true ]; then echo "approved=true"; else echo "approved=false"; fioracle-approvers.txt zawiera po jednym loginie GitHuba w wierszu: osoby z zespołu wyroczni z CODEOWNERS. To plik w repozytorium z polityką, a nie zapytanie o zespół, bo GITHUB_TOKEN workflowu nie może czytać członkostwa w zespołach organizacji. Utrzymuj obie listy w zgodzie.
Przebieg zamierzonej zmiany: joby padają, właściciel z listy czyta zmianę specyfikacji i zatwierdza, po czym klika Re-run failed jobs. Ponowne uruchomienie używa tego samego zdarzenia i commita, ale odpytuje recenzje od nowa, więc check przechodzi z adnotacją ostrzegawczą. Trzy właściwości trzymają tę furtkę zamkniętą. Zatwierdzenie musi dotyczyć dokładnie commita HEAD, więc push po zatwierdzeniu ponownie uzbraja check. Późniejsze „request changes” właściciela albo odrzucenie (dismiss) recenzji anuluje zatwierdzenie, a zwykły komentarz nie. Błąd API, brak pliku z listą albo puste wyjście kończą job porażką zamiast zaliczenia. Uruchomiliśmy skrypt na spreparowanych listach recenzji: zatwierdzenie commita HEAD przez właściciela dało approved=true; zatwierdzenie przez osobę spoza listy, zatwierdzenie starszego commita, zatwierdzenie z późniejszym „request changes”, odrzucone zatwierdzenie i brak recenzji dały approved=false; błąd API zakończył skrypt kodem niezerowym. Nie wyłączaj przy tym „Require review from Code Owners”: skrypt rozstrzyga, czy check przechodzi, a reguła gałęzi blokuje merge bez właściciela.
Trzymaj scenariusze holdout poza repozytorium
Dział zatytułowany „Trzymaj scenariusze holdout poza repozytorium”Nawet zablokowana suita uczy agenta, jak wygląda „gotowe”, bo agent może czytać testy. Po wielu iteracjach kod dryfuje w stronę przechodzenia właśnie tych przypadków. Zbiór holdout to odpowiedź z uczenia maszynowego na ten sam problem: scenariusze, których agent nigdy nie widział, uruchamiane tylko w CI i raportowane tylko jako zaliczone albo nie.
# Two jobs in the required oracle.yml (same read-only top-level permissions) build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: { persist-credentials: false } - run: npm ci && npm run build - uses: actions/upload-artifact@v7 with: { name: app-build, path: dist/ } holdout: needs: build runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v8 with: { name: app-build, path: dist/ } - uses: actions/checkout@v7 with: repository: acme/checkout-holdouts path: .holdout token: ${{ secrets.HOLDOUT_READ_TOKEN }} persist-credentials: false - name: Run holdouts and print IDs and counts only run: | npm ci --prefix .holdout .holdout/node_modules/.bin/playwright install --with-deps chromium .holdout/node_modules/.bin/playwright test --config .holdout/playwright.config.ts --reporter=json > "$RUNNER_TEMP/holdout.json" || true node .holdout/summarize.mjs "$RUNNER_TEMP/holdout.json"Zasady, dzięki którym holdout pozostaje holdoutem:
- Agent nie ma dostępu do odczytu repozytorium scenariuszy. Ani na maszynie programisty, ani przez serwer MCP, ani w prompcie. Token to sekret CI z prawem odczytu tylko tego jednego repozytorium.
- Artefakt buildu musi być samowystarczalny. Job
holdoutprzywraca tylkodist/, co działa dla aplikacji statycznej albo w pełni zbundlowanej. Jeśli serwer potrzebuje zależności w czasie działania, niech jobbuildwysyła też produkcyjnenode_modulesalbo kieruj scenariusze na wdrożony adres podglądu. - Build w jednym jobie, ocena w drugim.
npm cii build pull requesta działają w jobiebuild, który nigdy nie widzi scenariuszy. Jobholdoutnie ma checkoutu pull requesta ani jegonode_modules: Playwright pochodzi z lockfile’a repozytorium holdoutów, więc zależność dodana przez pull request nie odczyta.holdout/i nie wypisze go do logu.persist-credentials: falsetrzyma token poza konfiguracją git checkoutu. - Nazwij ryzyko, które zostaje. Testowana aplikacja to nadal kod z pull requesta, a jej serwer działa na tym samym runnerze co
.holdout/. Przy ważnym zbiorze uruchamiaj aplikację w kontenerze, który nie montuje.holdout/, albo kieruj scenariusze na wdrożony adres podglądu. - Logi zawierają identyfikatory, nie treść.
summarize.mjswypisuje coś w rodzajuholdout: 41 passed, 2 failed (HX-07, HX-19)i kończy się kodem1przy każdej porażce. Kończy się kodem1także wtedy, gdy raportu JSON brakuje albo nie ma w nim żadnych wyników, więc awaria Playwrighta (ukryta przez|| true) oblewa job, zamiast go zaliczyć. Jeśli agent przeczyta potem log CI, żeby poprawić swój pull request, dowie się, który scenariusz padł, a nie co sprawdza. - Oblany holdout trafia do człowieka. Właściciel przekłada go na zmianę specyfikacji albo kryterium akceptacji, słowami opisującymi zachowanie, a nie scenariusz. Gdy szczegóły scenariusza trafią do agenta, przenieś go do widocznej suity i napisz nowy w jego miejsce.
- Sekrety trafiają tylko do gałęzi z tego samego repozytorium. GitHub nie przekazuje sekretów do uruchomień
pull_requestz forków, więc job holdout nie działa dla pull requestów z forków.
Scenariusze napisane na podstawie wykonywalnych kryteriów akceptacji są dobrymi holdoutami. Sprawdzenia niezmienników z testów opartych na właściwościach jeszcze trudniej przeuczyć, bo testują regułę, a nie przykład.
Wykrywaj osłabianie testów, gdy testy zmieniają się zasadnie
Dział zatytułowany „Wykrywaj osłabianie testów, gdy testy zmieniają się zasadnie”Testy muszą się zmieniać, gdy zmienia się zachowanie, więc warstwy 1 i 2 nie mogą znaczyć „testy nigdy się nie zmieniają”. Znaczą: „testy nigdy nie zmieniają się po cichu”. Skrypt audytu flaguje mechaniczne formy osłabienia i oblewa job, żeby właściciel kodu musiał to obejrzeć:
#!/usr/bin/env bash# oracle-audit.sh BASE_SHA: flag changes that can make a check pass without fixing the codeset -euo pipefailbase="$1"oracle='(^|/)(tests?|__snapshots__)/|\.(test|spec)\.[jt]sx?$|_test\.(py|go)$|^\.github/|^CODEOWNERS$|(vitest|jest|playwright)\.config\.|^\.coveragerc$|^pytest\.ini$'changed=$(git diff --name-only "$base"...HEAD | grep -E "$oracle" || true)[ -z "$changed" ] && { echo "Oracle untouched."; exit 0; }
echo "Oracle files changed:"; echo "$changed" | sed 's/^/ /'diff=$(git diff -U0 "$base"...HEAD -- $changed)skips=$(grep -cE '^\+.*(\.skip\(|\.only\(|\bxit\(|\bxdescribe\(|@pytest\.mark\.(skip|xfail)|t\.Skip\()' <<<"$diff" || true)removed=$(grep -cE '^-.*\b(expect|assert)' <<<"$diff" || true)added=$(grep -cE '^\+.*\b(expect|assert)' <<<"$diff" || true)snaps=$(grep -cE '__snapshots__/|\.snap$' <<<"$changed" || true)configs=$(grep -cE '(vitest|jest|playwright)\.config\.|^\.coveragerc$|^pytest\.ini$' <<<"$changed" || true)
echo "Added skip/only/xfail markers: $skips"echo "Assertion lines removed: $removed, added: $added"echo "Snapshot files changed: $snaps"echo "Runner or coverage config files changed: $configs"if [ "$skips" -gt 0 ] || [ "$removed" -gt "$added" ] || [ "$snaps" -gt 0 ] || [ "$configs" -gt 0 ]; then echo "WEAKENING SIGNAL: a code owner must approve this oracle change." >&2 exit 1fiUruchomiliśmy go na pliku testowym, w którym dwie dokładne asercje zamieniono na jedno toBeTruthy() wewnątrz test.skip. Zgłosił jeden znacznik skip, dwie usunięte linie asercji i jedną dodaną, i zakończył się kodem 1. Zmiana, która tylko dopisywała wzorzec exclude do vitest.config.ts, dała jeden zmieniony plik konfiguracji i kod 1. Pull request, który tylko dodawał nowy test, przeszedł. Skrypt liczy linie, nie rozumie ich. Zamiana toBe(19.99) na toBeCloseTo(20, 0) nie zmienia bilansu, dlatego każda zmiana wyroczni trafia też do właściciela kodu, a prompt audytowy poniżej pyta o semantykę. Rozszerz wzorce o komentarze tłumiące ze swojego stosu (@ts-ignore, # noqa, eslint-disable) i klucze progów.
Rozdziel zmiany testów od zmian kodu
Dział zatytułowany „Rozdziel zmiany testów od zmian kodu”Najczystsza obrona to nigdy nie pozwalać jednej sesji pisać jednocześnie testów i kodu. To programowanie sterowane testami z jawnie określonym pochodzeniem testów:
- Sesja pisząca testy. Agent pisze albo zmienia testy na podstawie kryteriów akceptacji, przy zablokowanym kodzie produkcyjnym (w Claude Code
Edit(src/**)wdeny). Właściciel kodu zatwierdza testy. Czytanie testów to czytanie specyfikacji, znacznie krótszej niż implementacja. - Sesja implementacyjna. Nowa sesja sprawia, że zatwierdzone testy przechodzą, przy zablokowanej wyroczni. Jeśli uzna, że test jest błędny, zatrzymuje się i pisze
TEST_DISPUTE.md, a decyzję podejmuje człowiek.
Skąd wiesz, że blokada naprawdę trzyma?
Dział zatytułowany „Skąd wiesz, że blokada naprawdę trzyma?”Udowodnij działanie blokady ćwiczeniem red-team na gałęzi roboczej i zachowaj wynik jako dowód.
Porównaj tabelę z warstwami. Próby 1–4 powinny zatrzymać się w sesji w Claude Code i Codex. Próba 5 powinna zatrzymać się w sesji, jeśli konfiguracja runnera jest zablokowana; jeśli przejdzie, audyt padnie na zmianie konfiguracji, a base-oracle uruchomi się z konfiguracją z gałęzi bazowej. Próba 6 powinna zatrzymać się w sesji, a nawet gdyby przeszła, nie miałaby znaczenia, bo wymagany workflow leży gdzie indziej. Mimo to wypchnij gałąź i potwierdź, że CODEOWNERS prosi właścicieli o review, a job audytu pada. Każda próba, która przeszła, to poprawka w warstwie, która powinna była ją zatrzymać.
Zatwierdzanie należy do dwóch ról. Właściciele kodu dla ścieżek wyroczni zatwierdzają każdą zmianę wyroczni i nikt inny nie może tego zrobić. Tech lead odpowiada za konfigurację blokady i ćwiczenie: uruchom je przy wdrożeniu blokady, po każdej aktualizacji CLI agenta albo runnerów CI i przy zmianie układu repozytorium. Tech leadom, którzy śledzą to w całym zespole, wystarczy jedna liczba tygodniowo: odsetek pull requestów agentów modyfikujących pliki wyroczni i ile z nich oflagował audyt. Rosnący odsetek oznacza, że zmiany testów i kodu znowu się mieszają.
Co się psuje, gdy blokujesz wyrocznię?
Dział zatytułowany „Co się psuje, gdy blokujesz wyrocznię?”git checkout albo git merge pada z „unable to unlink old”. Sandbox Claude Code nie pozwala zastąpić pliku pod ścieżką z denyWrite, a przełączenie gałęzi dokładnie tego wymaga. Naprawa: przełączaj gałęzie poza sesją agenta albo daj każdemu zadaniu osobny worktree (zobacz stronę o równoległych agentach w izolowanych worktree), żeby agent nigdy nie przełączał gałęzi.
Agent utknął, bo test naprawdę jest błędny. Blokada bez wyjścia sprawia, że agent miota się, aż skończy mu się budżet. Naprawa: polecenie TEST_DISPUTE.md w komunikacie hooka i w prompcie implementacyjnym daje mu legalny sposób, żeby się zatrzymać. Człowiek czyta spór, a jeśli test ma się zmienić, zmienia się w sesji piszącej testy.
Treść holdoutu wyciekła do kontekstu agenta. Ktoś wkleił oblany scenariusz do promptu albo log wypisał treść asercji. Naprawa: przenieś ten scenariusz do widocznej suity, napisz nowy i popraw summarize.mjs, żeby wypisywał tylko identyfikatory.
Wyrocznia nigdy nie była dość silna, żeby ją chronić. Zablokowana suita, która nie łapie błędów, to teatr bezpieczeństwa. Naprawa: zmierz ją testami mutacyjnymi, jak opisuje strona jak silna jest twoja wyrocznia, zanim zaufasz zielonemu wynikowi z pętli bez nadzoru.
Instrukcje i blokada mówią co innego. CLAUDE.md albo AGENTS.md każe agentowi „aktualizować testy w razie potrzeby”, a blokada za każdym razem to odrzuca. Naprawa: zastąp tę linię opisem dwufazowego protokołu, żeby instrukcje i egzekwowanie mówiły to samo.