Voltar aos insights

Engenharia

Código gerado por IA: requisitos que podem falhar

Um agente de programação consegue construir uma funcionalidade em minutos. Funciona na demonstração, os testes passam e o diff parece arrumado. Algumas semanas depois, falha numa situação que ninguém descreveu: uma conta com muito mais dados do que as fixtures, um portátil mais lento, uma rede real, alguém a usar um leitor de ecrã. O código gerado por IA não estava errado face ao pedido. O pedido é que nunca disse o que tinha de se manter nessas condições. Por isso, o agente preencheu as lacunas com suposições e depois escreveu testes que confirmaram o próprio código. Quando uma revisão de código ou um utilizador dá pelo problema, já há outro trabalho construído sobre esse desenho.

Nada disto começou com o desenvolvimento assistido por IA. Sempre se escreveu código contra requisitos que ninguém formulou. O desenvolvimento orientado por testes (TDD) foi uma das respostas mais práticas: escrever um teste que falha, fazê-lo passar e depois arrumar o código enquanto o teste mantém tudo honesto. Escrever o teste primeiro obriga a decidir o que significa “funcionar” antes de existir código, e o teste fica depois para apanhar regressões. Os testes de aceitação levaram mais tarde a mesma ideia das unidades isoladas para o comportamento que o utilizador vê.

Continuo a achar que o TDD compensa. Só que, sozinho, não cobre o caso acima. Na prática, orienta sobretudo comportamento funcional, uma unidade de cada vez. Também parte do princípio de que quem escreve o teste sabe aquilo de que o produto precisa, e com um agente é muitas vezes o mesmo sistema que escreve a implementação e os testes, pelo que os testes herdam as mesmas suposições. E há ainda o tipo de falha a que o frontend é propenso. Falha em aspetos não funcionais: quantos dados existem, quão rápido é o dispositivo, quando a rede responde, o que recebem as tecnologias de apoio. Os testes unitários escritos primeiro raramente falam de nada disto.

A parte do TDD que vale a pena manter é a ideia de que um requisito só se torna real quando alguma coisa pode falhar contra ele. A ordem em que os ficheiros são criados importa muito menos. E essa ideia funciona muito para lá dos testes unitários. “Os resultados de pesquisa pertencem sempre à consulta atual” pode ser uma verificação. Também podem ser “a lista mantém-se reativa com 5000 linhas num CPU limitado”, “uma sequência de teclas mantém no máximo um pedido em curso” e “o JavaScript desta rota fica dentro do orçamento”.

Se requisitos como estes existirem como verificações executáveis antes de o desenho começar, o agente pode testar a própria proposta contra eles. Percebe que uma lista ingénua não cumpre o orçamento de reatividade enquanto ainda está a escolher a abordagem, em vez de depois de um revisor ou um utilizador dar com o problema. O trabalho de qualidade passa para o momento em que o código é desenhado e escrito.

Cada requisito abaixo indica a condição, o resultado observável, o limite, se existir, e o ambiente onde é medido. O âmbito, a parte mais fácil de esquecer, aparece perto do fim. Os exemplos foram construídos para ilustração. Os números são orçamentos de exemplo, não recomendações para todos os produtos.

Uma lista rápida com cinquenta linhas

Peça a um agente uma lista de projetos com um campo de filtro, e uma primeira versão típica filtra um array a cada tecla e apresenta todos os resultados. Com as cinquenta linhas de fixture do repositório parece instantânea, e o próprio teste do agente confirma que escrever “billing” deixa no ecrã os projetos de faturação. As duas coisas são verdade. Nenhuma diz nada sobre uma conta com 5000 projetos num portátil de gama média.

Um prompt melhor não resolve isto. O que falta é um requisito com um tamanho, um limite e um ambiente:

Com 5000 projetos carregados e o CPU limitado 4×, nenhuma tecla no campo de filtro demora mais de 200 ms entre a entrada e a pintura seguinte.

Os 200 ms vêm do limite “bom” de Interaction to Next Paint (INP), a métrica de reatividade das Core Web Vitals. Aqui é um orçamento de exemplo para uma interação num teste de laboratório. O número 5000 importa tanto como o limite. Um agente testa com os dados que recebe, e um requisito que não indica um tamanho aceita em silêncio o tamanho das fixtures.

Numa configuração de Playwright com Chromium, o requisito pode tornar-se uma verificação:

ts
import { expect, test } from '@playwright/test';

declare global {
  interface Window {
    __slowestInteraction: number;
  }
}

test('filtering 5,000 projects stays within the interaction budget', async ({ browserName, page }) => {
  test.skip(browserName !== 'chromium', 'CPU throttling uses the Chrome DevTools Protocol');

  const cdp = await page.context().newCDPSession(page);
  await cdp.send('Emulation.setCPUThrottlingRate', { rate: 4 });

  await page.goto('/projects?fixture=5000');
  await page.evaluate(() => {
    window.__slowestInteraction = 0;
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        window.__slowestInteraction = Math.max(window.__slowestInteraction, entry.duration);
      }
    }).observe({ type: 'event', buffered: true }); // reports interactions of 104 ms and longer
  });

  await page
    .getByRole('searchbox', { name: 'Filter projects' })
    .pressSequentially('billing', { delay: 120 });
  await page.waitForTimeout(500); // let the last event-timing entries arrive

  const slowest = await page.evaluate(() => window.__slowestInteraction);
  expect(slowest).toBeLessThanOrEqual(200);
});

O observer lê entradas de Event Timing. A duração cobre o atraso da entrada, o trabalho dos handlers e o tempo até a frame seguinte ser apresentada. A limitação do CPU através do Chrome DevTools Protocol só existe no Chromium, por isso o teste ignora-se a si próprio nos outros motores.

Esse limite aplica-se ao orçamento, não ao requisito inteiro. O Playwright corre o mesmo ficheiro de teste em Chromium, Firefox e WebKit, pelo que a metade funcional (ficam os projetos certos depois de filtrar 5000) pode correr nos três. O trabalho interativo também já não está preso ao Chromium. O Safari 27 inclui um servidor MCP, que o WebKit apresentou em julho de 2026 e que se ativa nas definições de Programador em “Allow remote automation and external agents”. A Mozilla publica o Firefox DevTools MCP, que controla o Firefox através do WebDriver BiDi. Nenhum dos dois documenta limitação de CPU, por isso o orçamento continua a ser uma medição no Chromium. O comportamento pode ser verificado em todos os motores que o produto suporta.

Com a verificação disponível enquanto desenha, a primeira tentativa do agente falha, e essa falha é útil. Agora pode comparar opções reais: virtualizar as linhas, paginar, adiar a atualização da lista em relação à entrada ou passar o filtro para o servidor. A verificação não escolhe por ele. Elimina a opção que não cumpre o requisito, antes de alguém construir um ecrã em cima dela.

Cada opção restante tem um custo, e esses custos também são requisitos. As linhas virtualizadas não estão no DOM, por isso a pesquisa na página não as encontra, e as tecnologias de apoio precisam de saber o número real de linhas. Se o produto depende de alguma destas coisas, escreva-a como uma segunda verificação. Um agente deve ver os requisitos que puxam em sentidos diferentes antes de se comprometer com um desenho.

As verificações de desempenho têm mais ruído do que as funcionais. Corra-as numa máquina consistente, deixe uma margem abaixo do orçamento e repita uma execução falhada antes de lhe chamar regressão. Uma execução de laboratório que passa diz pouco sobre os utilizadores reais. Diz que este desenho cabe neste orçamento nestas condições, e é tudo o que o requisito afirma.

Uma caixa de pesquisa que multiplica a carga no servidor

Agora passe o filtro para o servidor, porque a conta real tem mais projetos do que o browser deve descarregar. Uma primeira versão comum envia um pedido a cada tecla. Escrever “billing” dispara sete pedidos, e seis respondem a perguntas que o utilizador ainda não tinha acabado de fazer. Contra uma API local, na máquina de quem programa, nada parece errado. Em produção, o custo cresce com cada pessoa a escrever ao mesmo tempo e cai num serviço que a equipa de frontend pode nem gerir.

O agente não vê esse custo a menos que o requisito o indique:

Escrever uma consulta de sete caracteres a 120 ms por tecla, contra uma API que demora 300 ms, envia no máximo dois pedidos de pesquisa, nunca mantém mais de um em curso e mostra os resultados da consulta final até 800 ms depois da última tecla.

Este requisito aperta o desenho dos dois lados. O limite de pedidos exclui enviar a cada tecla. O limite de 800 ms exclui uma espera longa antes de enviar: com uma API de 300 ms, um debounce muito acima de algumas centenas de milissegundos não deixa margem. O agente tem de encontrar algo que caiba entre os dois, e o teste diz-lhe se conseguiu.

O Playwright pode fazer de API lenta e contar o que a página envia:

ts
import { expect, test, type Request } from '@playwright/test';

declare global {
  interface Window {
    __searchTiming: { lastInput: number; rendered: number };
  }
}

const isSearch = (url: string) => url.includes('/api/projects?q=');

test('typing a query keeps search load bounded', async ({ page }) => {
  let started = 0;
  let inFlight = 0;
  let maxInFlight = 0;

  await page.route('**/api/projects?q=*', async (route) => {
    await new Promise((resolve) => setTimeout(resolve, 300)); // slow but healthy API
    const query = new URL(route.request().url()).searchParams.get('q') ?? '';
    await route.fulfill({ json: projectsMatching(query) }).catch(() => {}); // may be aborted
  });

  page.on('request', (request) => {
    if (!isSearch(request.url())) return;
    started += 1;
    inFlight += 1;
    maxInFlight = Math.max(maxInFlight, inFlight);
  });
  const settle = (request: Request) => {
    if (isSearch(request.url())) inFlight -= 1;
  };
  page.on('requestfinished', settle);
  page.on('requestfailed', settle);

  await page.goto('/projects');
  await page.evaluate(() => {
    // measure inside the page: the test runner's retries are too coarse for timing
    const timing = { lastInput: 0, rendered: 0 };
    window.__searchTiming = timing;
    document.addEventListener('input', () => (timing.lastInput = performance.now()), true);
    new MutationObserver(() => (timing.rendered = performance.now())).observe(
      document.querySelector('[role="status"]')!,
      { childList: true, characterData: true, subtree: true },
    );
  });

  await page
    .getByRole('searchbox', { name: 'Filter projects' })
    .pressSequentially('billing', { delay: 120 });

  await expect(page.getByRole('status')).toHaveText('12 projects match "billing"');
  const { lastInput, rendered } = await page.evaluate(() => window.__searchTiming);
  expect(rendered - lastInput).toBeLessThanOrEqual(800);
  expect(started).toBeLessThanOrEqual(2);
  expect(maxInFlight).toBe(1);
});

projectsMatching representa uma fixture que devolve um conjunto de resultados conhecido, para que o texto de estado possa ser exato. O tempo é medido dentro da página de propósito. Quando o medi a partir do processo de teste, os intervalos de nova tentativa do Playwright faziam uma resposta de cerca de 575 ms parecer de 830 ms, e o teste reprovava um desenho que cumpria o orçamento. Os eventos de pedido, ao contrário da limitação de CPU, funcionam nos três motores, por isso esta verificação corre inteira em Chromium, Firefox e WebKit.

Com a verificação em vigor, um desenho que passa combina um debounce curto com o cancelamento do pedido anterior através de um AbortController. Cancelar reduz a carga. Não garante resultados corretos, porque uma resposta pode chegar antes de o cancelamento ter efeito. Manter os resultados ligados à consulta que os produziu é um requisito separado e precisa da sua própria verificação. Essa verificação impede que um agente trate “os pedidos são cancelados” como prova de que não aparecem resultados desatualizados.

Esta verificação cobre um padrão de escrita. Alguém que faz uma pausa a meio de uma palavra é outro caso. Se o produto se preocupa com isso, dê-lhe um cenário próprio em vez de alargar o limite deste.

Um botão que o agente “corrige” para pior

Nem todos os requisitos são sobre velocidade ou carga. Alguns existem para proteger comportamento que já funciona contra alterações bem-intencionadas.

Imagine que pede a um agente para melhorar a acessibilidade da barra de ferramentas de um relatório. Ele conhece uma regra verdadeira, a de que botões só com ícone precisam de um nome acessível, e aplica-a de forma demasiado ampla. Os botões com texto também recebem um aria-label:

html
<button type="button" aria-label="Submit">Save report</button>

O botão já tinha nome. Um botão nativo recebe o nome a partir do texto, como explica a orientação do APG sobre nomes acessíveis. O aria-label substituiu “Save report” por “Submit”. Um utilizador de controlo por voz que diga “click Save report” está agora a falar com um botão que as tecnologias de apoio conhecem por outro nome. O WCAG 2.5.3 Label in Name existe precisamente para esta incoerência. Também é uma alteração fácil de aprovar numa revisão de código. O diff só acrescenta um atributo, e o atributo parece uma correção de acessibilidade, enquanto a barra de ferramentas ficou pior.

O requisito que o evita é curto:

Todos os botões da barra de ferramentas do relatório têm um nome acessível que contém o texto visível. Os botões só com ícone têm um nome que descreve a ação.

O Playwright calcula nomes acessíveis, por isso o requisito pode cobrir a barra inteira e não apenas um botão:

ts
import { expect, test } from '@playwright/test';

const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

test('toolbar buttons are named by what people see', async ({ page }) => {
  await page.goto('/reports/demo');
  const toolbar = page.getByRole('toolbar', { name: 'Report actions' });

  for (const button of await toolbar.getByRole('button').all()) {
    const visible = (await button.innerText()).trim();
    if (visible) {
      await expect(button).toHaveAccessibleName(new RegExp(escapeRegExp(visible), 'i'));
    }
  }

  await expect(toolbar.getByRole('button', { name: 'Delete report' })).toBeVisible();
});

O ciclo aplica a regra a todos os botões com texto, incluindo os que forem acrescentados mais tarde. A última verificação confirma o botão só com ícone pelo nome pretendido. Um agente que acrescente um aria-label incoerente falha agora uma verificação que pode correr sozinho, antes de alguém rever a alteração.

A verificação tem limites. O cálculo de nomes do Playwright aproxima o que os browsers expõem. Não é uma sessão com leitor de ecrã, e um teste que passa não torna a página conforme com as WCAG. Mostra uma propriedade de uma barra de ferramentas, e é tudo o que o requisito afirma.

Os requisitos precisam de um âmbito e de um sítio

As três verificações acima protegem três sítios do código. A arquitetura por trás delas é mais ampla. Seis meses depois, alguém pede a um agente um segundo ecrã de pesquisa, desta vez para relatórios arquivados, e o agente escreve o seu próprio pedido a cada tecla. O teste antigo continua a passar, porque cobre o ecrã de projetos, e nada diz ao agente que a decisão anterior valia para todas as pesquisas do produto. Cada verificação cobre um ecrã, por isso a intenção por trás dela pode perder-se sem nenhum teste ficar vermelho. Ninguém decide assumir esse tipo de dívida técnica. Ela acumula-se no código gerado por IA, um ecrã de aspeto razoável de cada vez.

Há duas coisas que travam essa deriva. Uma é uma verificação que cobre todo o código e não apenas um ecrã, por exemplo uma regra de imports que só deixa os pedidos de pesquisa passar pelo módulo de pesquisa partilhado. A arquitetura evolutiva chama a verificações destas fitness functions: medidas automáticas de quão perto o sistema se mantém de um objetivo de arquitetura. A outra é um sítio onde o requisito, o motivo e o âmbito ficam escritos, para que um agente que constrói algo novo encontre a regra antes de existir qualquer teste para ela.

Ferramentas orientadas por especificações, como o OpenSpec e o GitHub Spec Kit, dão estrutura a esse sítio: requisitos com cenários, guardados no repositório e lidos pelo agente quando propõe uma alteração. Importa-me menos o formato do que algo com que os exemplos anteriores foram tropeçando. Os requisitos não funcionais são condicionais. O orçamento das 5000 linhas importa para listas de dados do utilizador que crescem. Numa página de definições com dez entradas é esforço desperdiçado. Sem âmbito, o requisito é aplicado de forma demasiado estreita, e o novo ecrã sai sem ele, ou demasiado ampla, e um agente virtualiza todas as listas do produto. O segundo erro é outra vez o do botão: uma regra correta no sítio errado.

Por isso, ponha o âmbito no próprio requisito:

markdown
### Requirement: Responsive filtering of large collections

For any view that filters a user-owned collection which can exceed 1,000 items,
the system SHALL keep each filter keystroke within 200 ms from input to next paint.
Views over fixed or small sets are out of scope.
Reason: accounts grow, and small fixtures hide the cost.

#### Scenario: Filtering 5,000 projects on a slow CPU

- **WHEN** 5,000 projects are loaded, the CPU is throttled 4× and the user types "billing"
- **THEN** no keystroke takes longer than 200 ms from input to next paint

O cenário descreve o teste do primeiro exemplo, e o teste pode remeter de volta para o cenário. Quando chegar a pesquisa do arquivo, o agente tem uma regra que diz se o novo ecrã está no âmbito, e um revisor tem algo concreto a apontar se o agente falhar. É a quinta parte de um requisito verificável: condição, resultado observável, limite, ambiente e o âmbito a que se aplica.

Um requisito que só vive num prompt dura uma conversa. Numa especificação e numa verificação no repositório, aplica-se a todas as alterações seguintes, seja quem for ou o que for que as faça. É isso que leva de facto um agente a rever o próprio trabalho: um comando de testes que se espera que corra, um hook depois de cada edição, uma barreira antes do push que não deixa passar um resultado vermelho. Pedir-lhe cuidado faz muito menos. E nem todos os requisitos precisam de um browser. O orçamento de JavaScript de uma rota pode ser uma verificação de tamanho no build, e um erro que continua a voltar pode tornar-se uma regra de lint.

O TDD pedia o mesmo: decidir o que significa “funcionar” antes de o código existir. O que mudou foi quem lê o requisito. Agora é algo que escreve código depressa e confia nos próprios testes, por isso o requisito tem de ser específico o suficiente para falhar. Testes verdes, por si só, não deixam o código gerado por IA pronto para produção.

Se o produto já está em funcionamento, a parte difícil é muitas vezes descobrir de que requisitos depende sem o dizer e quais deles nada verifica. É por aí que começa uma Frontend Risk Audit: as tarefas que importam, o comportamento de que dependem e uma lista priorizada do que verificar e corrigir.

Definições de privacidade

Escolha se permite a analítica e a gravação de sessões do PostHog. Ambas permanecem desativadas até à sua autorização. Pode utilizar o site e o formulário de contacto sem as permitir.

A sua escolha guardada: Não selecionada

Serviços e utilização de dados

PostHog Cloud EU — analítica e gravação de sessões facultativas

As seguintes funções só são carregadas após a sua autorização:

  • Analítica de visitas e interações. As visualizações de páginas, as ações selecionadas e os resultados de envio do formulário ajudam-nos a compreender como o site é utilizado e onde os visitantes encontram dificuldades.
  • Monitorização de erros e desempenho. Os erros do navegador e as medições de desempenho das páginas ajudam-nos a identificar problemas técnicos e a verificar as correções.
  • Gravação de sessões. A navegação, os cliques, a deslocação na página e os movimentos do ponteiro ajudam-nos a investigar problemas da interface. Todo o texto da página é mascarado e o formulário de contacto completo é excluído antes do envio dos dados de gravação. Esta função não grava o seu ambiente de trabalho, outras aplicações, a câmara ou o microfone.

Um identificador aleatório guardado no navegador associa visitas repetidas, eventos e gravações a um perfil pseudonimizado. Estes dados não são totalmente anónimos. Não enviamos o seu nome, endereço de email ou conteúdo do formulário para o PostHog.

A Cloudflare estima o país a partir da ligação de rede. Enviamos o código do país para o PostHog; o endereço IP original, a cidade e a localização precisa não são guardados nos eventos de analítica.

Não vendemos estes dados nem os partilhamos para fins de marketing próprio de terceiros.

Cloudflare — funcionamento e segurança do site

A Cloudflare aloja e disponibiliza o site e protege-o contra utilizações abusivas. O Turnstile verifica sinais do navegador e da ligação para ajudar a impedir envios automatizados do formulário. Não enviamos a sua mensagem para o widget do Turnstile.

As medições operacionais ajudam-nos a acompanhar pedidos técnicos, erros e o processamento do formulário. Baseamo-nos nos nossos interesses legítimos em manter o site disponível, prevenir utilizações abusivas e diagnosticar falhas técnicas. Limitamos este tratamento ao que é necessário para essas finalidades.

Estas funções operam independentemente da sua escolha de analítica. Recusar a analítica não as desativa.

A Cloudflare também processa e entrega as mensagens do formulário de contacto. A autorização para utilizar o seu pedido de contacto para lhe responder é solicitada separadamente no formulário.

Preferências guardadas no navegador

Guardamos neste navegador o tema de cor que escolher e a sua decisão sobre a analítica (permitir ou recusar), para os aplicar nas visitas seguintes. Estas preferências não são utilizadas para acompanhar visitas.

Estas preferências utilizam armazenamento local do navegador (localStorage) e funcionam independentemente da autorização de analítica.

A sua escolha de analítica é guardada durante 180 dias. Este é o prazo da escolha guardada, não o prazo de conservação dos dados de analítica.

Alterar a escolha e pedir a eliminação

Pode alterar a sua escolha a qualquer momento em Política de Privacidade e definições de privacidade, no rodapé.

Não permitir interrompe a analítica e a gravação futuras. Não elimina os dados já recolhidos.

Para pedir a eliminação da analítica associada a este navegador, utilize Pedir eliminação dos dados de analítica na página de definições de privacidade. O pedido abrange o perfil, os eventos e as gravações de sessões associados. A eliminação é processada de forma assíncrona.

Consulte a Política de Privacidade para conhecer os prazos de conservação e obter mais informações sobre os prestadores e o tratamento dos dados.

Ler Política de Privacidade ↗

Fechar esta janela com ×, Escape ou um clique fora guarda uma recusa e interrompe a analítica e a gravação.