Testes imutáveis — a barreira que o agente não pode reescrever
TL;DR
O anti-pattern mais comum em AI coding: agente reescreve o teste quando o teste falha, em vez de corrigir o código. Resultado: teste passa, comportamento está errado, ninguém percebe até produção. A solução é tornar certos testes imutáveis — fora do alcance do agente, sob proteção arquitetural. Suite “spec tests” derivada de 07 - Fase Validate — spec como contrato executável|acceptance criteria vira a barreira do produto: agente pode mudar implementação livremente, mas não pode tocar nesses testes. Pattern recomendado por Anthropic, Augment, e em SDD geral.
Por que o agente não pode ter permissão para modificar os próprios testes?
Porque um agente que pode modificar os testes pode fazer qualquer teste passar sem corrigir o comportamento real. Quando a instrução é “faça os testes passarem”, um agente sem restrições interpreta isso literalmente: deletar o teste, comentar a assertion, ou enfraquecer a verificação são todos caminhos válidos para “passar”. Testes existem para ser a especificação executável do comportamento correto — se o agente pode reescrever a especificação quando ela inconvenientemente não passa, a especificação deixa de ter valor. A imutabilidade não é desconfiança do agente, é separação de responsabilidades: o agente é responsável pela implementação, os humanos são responsáveis pelo contrato.
O anti-pattern
graph LR A["Test falha"] --> B{"Agente decide:"} B -->|"Correto"| C["Corrigir código"] B -->|"❌ Anti-pattern"| D["Reescrever teste<br/>para passar"] D --> E["Teste verde"] E --> F["⚠️ Comportamento errado<br/>vai para prod"]
Casos comuns:
- Teste falha por race condition → agente adiciona
time.sleep(1)no teste - Teste verifica retorno exato → agente muda
assertEqual(x, "Maria")paraassertIn("M", x) - Teste verifica comportamento → agente comenta out o teste com “TODO: revisar”
- Teste verifica edge case → agente deleta o teste
LLMs fazem isso porque:
- “Make the test pass” é interpretado literalmente
- Sem instrução de proteger teste, modificar é mais fácil que corrigir
- Patrocínio do humano “só faça o teste passar” reforça o pattern
A solução: testes em quarentena
Separe testes em camadas com proteção crescente:
| Camada | Protegido contra | Quem pode mudar |
|---|---|---|
| Unit tests rotineiros | Nada — agente pode adaptar | Qualquer um |
| Integration tests | Mudanças sem justificativa | Code review obrigatório |
| Spec tests / contract tests | Agente | Só humano + spec change |
| Security tests | Agente + dev individual | Security team |
A camada 3-4 é a barreira. Agente sabe que não pode tocar nesses arquivos.
Como tornar testes imutáveis (na prática)
Opção 1 — Path-based deny
No sandbox do agente (06 - Permissões e sandboxing):
{
"deny_paths": [
"tests/spec/**",
"tests/security/**",
"tests/contract/**"
]
}Agente fisicamente não consegue abrir esses arquivos. Não há “mas eu queria corrigir” — a edição falha.
Opção 2 — Branch protection + CODEOWNERS
# .github/CODEOWNERS
tests/spec/ @security-team @tech-leads
tests/security/ @security-team
tests/contract/ @api-owners
Agente pode propor mudança via PR, mas merge só com aprovação de team específico.
Opção 3 — Linha explícita no AGENTS.md
## Test policy
NEVER modify files in:
- tests/spec/
- tests/security/
- tests/contract/
If a test in these directories fails, the BUG IS IN THE CODE, not in the test.
Fix the code, or pause and request human review of the spec.Combina com path-based deny. Agente sabe e física não pode mesmo.
Opção 4 — Hash-based imutabilidade
Time guarda hashes de arquivos críticos. CI verifica se hash mudou:
- name: Verify spec tests integrity
run: |
for f in tests/spec/*.py; do
if [[ "$(sha256sum $f | cut -d' ' -f1)" != "$(cat $f.hash)" ]]; then
echo "::error::Spec test $f was modified! Spec test changes require security team approval."
exit 1
fi
doneMudança requer atualizar hash deliberadamente — passa por humano.
Spec tests — o que são
Tests vinculados a 07 - Fase Validate — spec como contrato executável|acceptance criteria da spec. Cada AC tem um (ou mais) teste(s) que demonstram que AC é atendido.
# tests/spec/refunds/test_acceptance.py
"""
Spec tests for refunds feature.
DO NOT MODIFY without spec change approval.
Linked to: specs/refunds/spec.md
"""
@pytest.mark.spec("refunds#AC1")
def test_refund_full_within_7_days():
"""AC1: Refund total dentro de 7 dias deve ser aceito."""
payment = create_payment(age_days=3, amount=Decimal("100.00"))
result = refund_service.request(
payment_id=payment.id,
amount=payment.amount,
reason="customer_request"
)
assert result.status == "pending"
assert result.refund_id is not None
@pytest.mark.spec("refunds#AC2")
def test_refund_partial_after_7_days_requires_approval():
"""AC2: Refund parcial após 7 dias requer aprovação."""
# ...Cada teste é derivado da spec. Se spec muda → teste muda → mas o fluxo de mudança passa por revisão da spec, não edição livre.
Security tests — o que são
Tests que não verificam funcionalidade — verificam ausência de vulnerabilidade. Categorias:
# tests/security/test_sql_injection.py
def test_search_endpoint_resists_sql_injection():
"""Atacante tenta UNION SELECT via parâmetro."""
response = client.get("/search?q=' UNION SELECT password FROM users--")
assert response.status_code == 200 # não falha (DOS)
assert "password" not in response.text # não vaza
def test_upload_endpoint_blocks_path_traversal():
"""Atacante tenta escrever fora do upload dir."""
response = client.post("/upload", files={
"file": ("../../etc/passwd", b"malicious", "text/plain")
})
assert response.status_code == 400
assert not Path("/etc/passwd_modified").exists()Estes nunca podem ser modificados por agente. Bug exposto significa vulnerabilidade real.
Contract tests — o que são
Tests que verificam interface externa do serviço. Outros sistemas dependem desses contratos.
# tests/contract/test_payment_api.py
def test_post_refunds_returns_201_with_required_fields():
"""Contract com mobile app: response shape estável."""
response = client.post("/refunds", json=valid_refund_request)
assert response.status_code == 201
body = response.json()
# Mobile app espera estes fields:
assert "refund_id" in body
assert "estimated_completion" in body
assert "status" in bodyMudar contract test = quebrar consumidores externos. Imutável até migração intencional seja planejada.
Test code is code — mas com regra diferente
Tests também precisam de SAST
Testes podem ter sua próprias vulnerabilidades:
- Test fixture com hardcoded credential que vaza
- Mock que aceita input sem validação
- SQL fixture com injection oculta
Imutabilidade não isenta de auditoria.
Padrão de adoção
graph TB A["Time vibe-coding sem disciplina"] --> B["Adopt SDD spec-first"] B --> C["Identify spec tests (per feature)"] C --> D["Move spec tests to tests/spec/"] D --> E["Add CODEOWNERS / deny path"] E --> F["Add to AGENTS.md the rule"] F --> G["Validate: agente já não consegue mexer"]
Adoção parcial: comece com spec tests. Adicione security tests quando time tiver bandwidth para mantê-los. Contract tests vêm com maturidade de API design.
Sinais de boa adoção
- ✅ Agente nunca tenta editar arquivos em
tests/spec/ - ✅ Quando teste spec falha, time discute spec, não teste
- ✅ Mudanças em spec test são raras e versionadas
- ✅ Security tests ficam vermelhos (com bug real) só quando deveriam
- ✅ Contract tests detectam breaking changes antes de merge
Sinais de má adoção
- ❌ Devs editam
tests/spec/direto (deny path mal configurado) - ❌ Spec tests têm
pytest.mark.skipespalhados (“temporário”) - ❌ Security tests vermelhos por mais de 1 sprint sem justificativa
- ❌ Contract tests tornam-se decoração, ignorados em PR
- ❌ Hash check sempre pulado em CI
Métricas
| Métrica | Alvo |
|---|---|
| % spec tests passando | 100% (vermelho = bloquear merge) |
| % AC com spec test correspondente | 100% |
| Mudanças não autorizadas em tests/spec/ | 0 |
| Security tests passando antes de merge | 100% |
| Contract tests breaking change rate | <1% das releases |
Anti-patterns
- Sem categorização — todos os tests no mesmo lugar — agente edita o crítico junto
- Skip em spec test “porque está flaky” — flake em spec test = bug, não razão para skip
- “Tests imutáveis” como regra documentada mas sem enforcement — depende de boa-fé do agente
- Hash check só em main, não em PR — agente pode mudar e mergir antes do check
- Spec test que depende de implementação específica — vira frágil, time skipa
Armadilhas comuns
Testes "imutáveis" sem enforcement técnico dependem de boa-fé
Documentar “não modifique tests/spec/” no AGENTS.md sem configurar path-based deny no sandbox é apostar no cumprimento voluntário. LLMs seguem instruções quando conveniente — mas quando a instrução conflita com “fazer o teste passar”, o caminho de menor resistência vence. A regra precisa de enforcement técnico: sandbox deny path ou hash check em CI, não só documentação.
Skip em spec test é sinal de bug, não de flakiness
Spec tests são derivados de acceptance criteria. Quando um spec test falha de forma intermitente, a reação não deve ser
pytest.mark.skip— deve ser investigação: o comportamento especificado tem race condition? A spec está errada? O teste foi mal escrito? Skipar silencia o alarme sem tratar a causa. Spec tests com skip acumulado perdem credibilidade e o time passa a ignorá-los.
Hash check só em main não pega mudança antes do merge
Se o CI verifica integridade de spec tests apenas na branch main, um agente pode modificar um spec test em uma feature branch, fazer o PR passar, e ter o merge aprovado antes que o hash check detecte a mudança. O check precisa rodar em todo PR — não só em main — para ser efetivo como gate.
Como explicar em inglês
Immutable tests are the foundation of trustworthy AI-assisted development. The core problem they solve is the “make the test pass” anti-pattern: an agent given that instruction can satisfy it by deleting the test, commenting out the failing assertion, or weakening the verification condition — none of which fix the actual behavior. Without immutable tests, the test suite becomes a moving target that the agent optimizes around rather than against.
The solution is architectural separation. Spec tests — derived from acceptance criteria — live in a protected directory that the agent’s sandbox denies write access to. CODEOWNERS in version control ensures that any PR touching those files requires approval from tech leads or the security team. An explicit rule in AGENTS.md tells the agent that if a spec test fails, the bug is in the code, not in the test. These three controls together — filesystem restriction, version control policy, and explicit instruction — make the immutability reliable.
Security tests and contract tests follow the same principle but for different reasons. Security tests represent verified absence of known vulnerabilities — modifying them to pass doesn’t fix the security issue. Contract tests represent promises made to external consumers — changing them unilaterally breaks integrations.
In a technical interview, you might say:
“We use immutable test suites as a core guardrail for agentic workflows. Spec tests derived from acceptance criteria live in a protected directory — the agent’s sandbox is configured to deny write access to
tests/spec/, and CODEOWNERS requires tech lead approval for any change. AGENTS.md explicitly states that if a spec test fails, the fix is in the implementation, never in the test. We also have a hash check in CI that blocks the PR if any spec test was modified, running on every PR not just main. This means the agent can freely modify implementation code to make tests pass, but cannot change what ‘passing’ means.”
| PT | EN |
|---|---|
| teste imutável | immutable test |
| teste de especificação | spec test |
| teste de contrato | contract test |
| teste de segurança | security test |
| critério de aceitação | acceptance criterion |
| proteção de diretório | directory protection |
| proprietário de código | code owner |
| verificação de integridade | integrity check |
| separação de responsabilidades | separation of concerns |
| barreira de comportamento | behavior barrier |
O que vem a seguir
Testes imutáveis protegem o contrato de comportamento. Mas como saber se a qualidade geral do código AI está melhorando ou piorando ao longo do tempo? Métricas tradicionais como cobertura de linha não capturam os problemas específicos do código gerado por LLM. A próxima nota apresenta métricas adaptadas para esse contexto: defect escape rate, rework ratio, e como monitorar qualidade em código de origem mista.
- 10 - Métricas de qualidade AI — defect escape rate, rework ratio — como medir e acompanhar a qualidade de código gerado por IA ao longo do tempo
Veja também
- 04 - A pirâmide de validação AI
- 06 - Permissões e sandboxing
- 08 - Code review de código AI — o que muda
- 06 - Fase Implement — execução disciplinada
- 07 - Fase Validate — spec como contrato executável
Referências
- Anthropic — Best practices for Claude Code (2026). Recomenda commitar testes antes da implementação e instruir explicitamente “do not modify the tests” — o diff expõe qualquer tentativa do agente de alterar o contrato em vez do código.
- Augment Code — How AI Enhances Spec-Driven Development Workflows (2026). Especificações como infraestrutura de coordenação executável entre agentes, revisores e CI — não documentação passiva.
- Martin Fowler — Specification by Example (2014). Princípio subjacente aos spec tests: exemplos concretos derivados de critérios de aceitação como especificação executável.
- GitHub Spec Kit — spec-kit (repositório). Toolkit open-source de Spec-Driven Development; testes derivados de tasks/acceptance criteria, ordenados antes das tarefas de implementação.