← Voltar ao Blog

TDD com Playwright MCP e CI: pipeline Claude Code em 47 min

O ciclo Red-Green-Refactor sempre foi vendido como disciplina, quase moral. Você escreve o teste, vê o vermelho, faz o verde, refatora. Em papel bonito. Na prática, meu loop de feedback ficava em 12 minutos por causa do CI, e eu passava metade da tarde alternando entre a aba do editor e a aba do Actions esperando o “amarelinho ficar verde”.

Semana passada eu montei um pipeline onde o Claude Code escreve o teste Playwright via MCP, o CI roda em headless com @playwright/mcp 0.0.78 (Microsoft, Apache-2.0), e o loop caiu para 47 segundos. Este post é a receita.

Se você já leu meu texto sobre as 24 horas em que meu agente de IA gerou 8 incidentes de segurança, pode pular direto para a seção “Permissões no runner”. O resto do artigo assume que você ainda não passou por essa dor.

Por que TDD com Claude Code, e não Claude Code sozinho

Deixa eu ser honesto: minha primeira versão dessa pipeline não tinha TDD. O Claude Code escrevia teste e implementação na mesma sessão, e eu ficava satisfeito porque o CI ficava verde. Até que percebi uma coisa constrangedora — o teste sabia demais sobre a implementação.

Existe um termo para isso: contaminação de contexto. Quando o mesmo agente que implementa também escreve o teste, o teste não verifica correção. Ele carimba a implementação. É o equivalente digital de corrigir a própria prova de casa. Você acerta 100 %, mas não aprende nada.

A solução que virou padrão para mim é separar as fases em subagentes independentes. Um subagente escreve o teste sem enxergar código de produção. Outro implementa sem ver o raciocínio do primeiro. O terceiro refatora. Cada um tem contexto isolado. O teste não pode trapacear porque literalmente não viu o gabarito.

Comparação: mesma sessão vs subagentes separados no Red-Green-Refactor

Setup: Playwright MCP em 3 comandos

Playwright MCP é um servidor MCP mantido pela Microsoft que expõe o navegador via árvore de acessibilidade em vez de screenshots. Isso muda tudo em CI headless: você não precisa de OCR nem de modelo visão para o agente entender a tela.

Instalação:

npx @playwright/mcp@latest --headless

Configuração no .claude/mcp.json:

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest", "--headless"]
    }
  }
}

Requer Node.js 18+. Sem plano pago, sem métrica de uso, sem rate limit. Roda 100 % na sua máquina (ou no runner do GitHub Actions, no nosso caso). Ao rodar uma tool, o servidor devolve um snapshot estruturado da página: elementos, roles, texto. É o mesmo mecanismo que o Claude Code usa para clicar em botões sem “ver” a tela.

O agente que escreve o teste (subagente 1)

O subagente tdd-test-writer recebe apenas a descrição da feature. Nunca vê src/. O prompt do agente:

# .claude/agents/tdd-test-writer.md

Você é um escritor de testes. Sua ÚNICA função é escrever testes que falham.

Regras:
- Você NÃO PODE ler nem referenciar arquivos de implementação (src/**)
- Escreva testes baseados APENAS na descrição da feature
- Cubra caminho feliz e casos limite (input vazio, mobile 375px, etc.)
- Use Playwright MCP para inspecionar a UI real quando existir
- Rode os testes e confirme que FALHAM antes de terminar

Quando eu invoco esse agente com “adiciona um botão de export CSV no dashboard”, ele abre o servidor local via mcp__playwright__browser_navigate, vê que o botão não existe, e escreve o teste baseado na descrição — não no que ele acha que a implementação vai ser.

O teste sai assim:

test('exporta CSV com filtro de data ativo', async ({ page }) => {
  await page.goto('http://localhost:3000/dashboard');
  await page.getByLabel('De').fill('2026-07-01');
  await page.getByRole('button', { name: 'Export CSV' }).click();
  const download = await page.waitForEvent('download');
  expect(download.suggestedFilename()).toMatch(/^dashboard-2026-07-01/);
});

Repare no getByRole('button', { name: 'Export CSV' }). Isso é Playwright MCP guiando o agente para seletores estáveis, em vez daquele .btn-primary.mt-4 frágil que quebra sempre que alguém troca de Tailwind.

O CI que fecha o loop em 47 segundos

Aqui está o workflow inteiro (removi comentários para caber):

# .github/workflows/tdd-loop.yml
name: TDD Loop
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  tdd:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      - run: npm ci
      - run: npx playwright install chromium --with-deps
      - run: npm run dev &
      - run: npx wait-on http://localhost:3000

      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          claude_args: |
            --mcp-config '{
              "mcpServers": {
                "playwright": {
                  "command": "npx",
                  "args": ["@playwright/mcp@latest", "--headless"]
                }
              }
            }'
            --model claude-sonnet-4-6
            --allowed-tools "Read,Edit,Bash(npm run test),mcp__playwright__*"
          prompt: |
            Rode o teste falhando em tests/. Se um subagente
            tdd-test-writer já adicionou tests/*.spec.ts nesta PR,
            invoque o subagente tdd-implementer para escrever a
            implementação mínima em src/ que faça o teste passar.
            NÃO modifique arquivos em tests/.

Os pontos de atenção que quebraram na minha primeira tentativa:

  • --allowed-tools restringe o subagente ao mínimo. Sem isso ele tenta rodar git push no meio da PR e o Actions bloqueia (bem que devia).
  • --headless no MCP é obrigatório. Sem ele o Chromium tenta abrir X server no runner e trava por 30 s antes de morrer.
  • timeout-minutes: 5 é um teto psicológico. Se o loop passar de 5 minutos, o desenho está errado, não a máquina.

O tempo real de execução na minha máquina:

EtapaAntesDepois
Escrever teste (local)3 min12 s (subagente)
Push + CI cold start4 min1 min (cache)
Rodar teste no CI3 min25 s (headless + a11y)
Ler resultado + iterar2 min10 s (comment no PR)
Total12 min47 s

Permissões no runner: o campo minado

Quando o Claude Code roda no CI, ele herda o GITHUB_TOKEN. Se você deixar permissions: write-all (o default de muitos templates), o agente pode fechar issues, deletar branches, e criar releases. Já vi acontecer.

O mínimo viável para essa pipeline:

permissions:
  contents: read      # ler o código
  pull-requests: write # comentar na PR

Nada de issues: write, nada de actions: write, nada de packages: write. Se o agente tentar algo fora desse escopo, o GitHub bloqueia e você vê no log. É a diferença entre “meu agente é bem comportado” e “meu agente é estruturalmente incapaz de fazer besteira”.

O detalhe cruel: --allowed-tools restringe o Claude Code, mas não impede o subagente que ele invoca via MCP de fazer qualquer coisa que o token permita. Por isso a camada de permissions: no workflow é mais importante do que a whitelist de tools.

Quanto custa isso em BRL

Cada execução da pipeline (test-writer + implementer + refactorer) consome em média:

  • 45 000 tokens de input (contexto do repositório + resposta do MCP)
  • 4 000 tokens de output (código + explicação)

Com Claude Sonnet 4.6 a US$ 3 / M input e US$ 15 / M output:

input:  45 000 × $3  / 1 000 000 = $0.135
output: 4 000  × $15 / 1 000 000 = $0.060
total: $0.195 por execução ≈ R$ 1,07 (câmbio 5,5)

Com Sonnet 5 (lançado em 30 de junho de 2026 a US$ 2 / US$ 10 até 31 de agosto), o mesmo loop cai para US$ 0.13 ≈ R$ 0,72. Um dev que roda 40 PRs por dia gasta R$ 40 / dia, ou seja, menos do que uma dose de café especial. E o CI para uma dose de café.

Se você comparar com um freelancer QA júnior a R$ 60 / hora rodando teste manual, o breakeven é uma execução por hora.

Onde eu ainda tropeço

Duas coisas que ainda me incomodam nessa arquitetura:

1. Debug de teste flaky é pior. Quando o teste passa localmente e falha no CI, o subagente tdd-implementer não tem contexto do que o tdd-test-writer “queria dizer”. Ele fica adivinhando. Solução parcial: um README.md em tests/ que descreve invariantes de negócio (nada de detalhes técnicos), e que ambos os subagentes leem.

2. Playwright MCP em CI paralelo compete por porta. Se o workflow roda 3 shards, os 3 tentam abrir Chromium na 9222. Solução feia mas funcional: PLAYWRIGHT_MCP_PORT=$((9222 + RANDOM % 100)).

O que fica

TDD não é sobre disciplina moral. É sobre isolamento de contexto. Quando o agente que escreve o teste não vê o gabarito, o teste vira uma spec real. E quando a spec é executável no CI em 47 segundos, ela deixa de ser “aquela coisa que a gente devia fazer” e vira o loop de feedback padrão.

A parte que me surpreendeu foi que essa arquitetura é mais barata que o processo antigo, não mais cara. R$ 1 por PR é menos do que o custo de 10 minutos do meu tempo esperando o CI. E o subagente escreve teste melhor do que eu quando estou com pressa.

Se você já se queimou com agente sem permissions: explícito, o outro texto que eu recomendo é as 24 horas em que meu agente gerou 8 incidentes de segurança — os padrões de escape de sandbox que aparecem lá são exatamente os que essa pipeline previne.


ken imoto · WebRTC & Voice AI engineer · kenimoto.dev · TabNews