CommunityEscrita e Ediçãogithub.com

meganemura/nukadoko

Contratos de passos tipados para Gherkin, permitindo que um cenário seja considerado incorreto antes mesmo de ser executado. O vocabulário é legível por máquina para agentes, e cada execução deixa um recibo medido pela ferramenta, não relatado pelo agente.

O que é nukadoko?

Nukadoko é uma biblioteca de contratos de passos tipados para Gherkin, projetada para migrar suítes existentes do Cucumber.js ou DSLs próprias para um sistema de definição de passos com validação de tipos, dependências explícitas e execução isolada. Ela substitui as anotações do Cucumber por `defineStep`, que aceita esquemas Zod para argumentos e retornos, declara dependências via `from` e marca mutações com `mutates`. A migração ocorre em dois estágios separados para garantir rastreabilidade de falhas: primeiro, troca-se o import para `nukadoko/compat` sem alterar a lógica; depois, promovem-se passos individuais para contratos tipados, um de cada vez. A ferramenta gera recibos de execução que registram o que realmente ocorreu, independentemente de relatórios do agente, e suporta execução direta de passos via `nuka do` para teste isolado. Ideal para equipes que desejam maior segurança em testes BDD sem reescrever toda a suíte de uma vez.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add meganemura/nukadoko

Installed? Explore more Escrita e Edição skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Migrando para nukadoko

Para que serve

Uma suíte existente migrando para nukadoko, uma peça de cada vez — não uma reescrita. O ponto de partida difere:

  • Uma suíte cucumber-js — arquivos de feature mais cola (glue), geralmente dirigindo Playwright.
  • Uma suíte já moldada como passos tipados — sua própria DSL, não a do cucumber, mas próxima o suficiente de defineStep do nukadoko para que a migração seja principalmente tradução.

De qualquer forma, a migração ocorre em estágios, nunca de uma só vez; o restante desta skill explica o porquê e, em seguida, detalha cada ponto de partida por vez.

Tudo abaixo pressupõe que o projeto já está inicializado. Se ainda não estiver (sem nukadoko.config.ts), execute nuka init primeiro.

Dois estágios, nunca de uma só vez

Altere uma coisa, depois a próxima — nunca ambas ao mesmo tempo. Se um passo quebrar após duas alterações serem aplicadas juntas, não há como saber qual delas o quebrou: as causas se misturam em uma única falha. Dividir o trabalho em estágios mantém cada falha rastreável a exatamente uma alteração. Esse é o objetivo principal — os próprios estágios são secundários a isso.

Se os estágios abaixo não se encaixarem no seu ponto de partida, derive seus próprios dois estágios a partir deste princípio. O que deve ser mantido é a rastreabilidade, não esses nomes de estágios específicos.

Duas aplicações seguem: uma suíte cucumber-js se divide em "trocar o import" e "adicionar tipagem" (abaixo); uma suíte já moldada como passos tipados se divide de forma diferente, descrita mais adiante.

Vindo de uma suíte cucumber-js

Este é o caso para o qual os dois estágios acima são nomeados.

Estágio 1 — fazer funcionar

Altere o único import que cada arquivo de cola (glue) usa:

// antes: import { Given, When, Then } from "@cucumber/cucumber";
import { Given, When, Then } from "nukadoko/compat";

A extensão decide o que a descoberta lê, não o histórico da suíte: arquivos de cola .ts, .mts, .js e .mjs são lidos da mesma forma, então uma suíte que sempre foi JavaScript puro não precisa ser reescrita para TypeScript apenas para iniciar este estágio. Um arquivo .cjs é a única exceção: nukadoko é exclusivamente ESM, então a descoberta nunca importa um; nuka check o nomeia em vez disso (step-file-unsupported-extension) em vez de deixá-lo desaparecer como um undefined-step inexplicável. Renomeá-lo para .js só ajuda se o código do próprio arquivo já for sintaxe de módulo ES; se ainda chamar require(...), isso falha no mesmo import pelo mesmo motivo de ser exclusivamente ESM, e nuka check informa isso como step-file-import-failed.

Em seguida, execute nuka check <feature> e nuka run <feature> e leia o que eles dizem. Corrija o que eles apontarem e execute-os novamente. Repita até que a suíte existente esteja verde — essa é a condição de conclusão do Estágio 1, nada mais. Essa condição é nuka check e nuka run ficarem verdes, não tsc verificando tipos de forma limpa; os dois podem discordar em qualquer direção.

nuka steps e nuka describe permanecem utilizáveis durante todo o Estágio 1, mesmo enquanto alguns arquivos de cola ainda falham ao importar: ambos leem arquivos de passos um de cada vez, então um arquivo que ainda falha ao importar é nomeado (import_failures em --json, stderr caso contrário) em vez de esvaziar todo o vocabulário. Leia o vocabulário à medida que avança, em vez de esperar que todos os arquivos importem de forma limpa primeiro; nuka run <feature> e nuka do <step> ainda se recusam completamente em um arquivo de cola ilegível, intencionalmente, já que estão prestes a executar.

Não procure uma lista do que o compat não suporta antes de começar. O que não funcionar falhará de forma ruidosa, seja no import ou no primeiro nuka run — a falha nomeia o que quebrou, e é nisso que você deve agir. Uma lista escrita aqui ficaria desatualizada assim que a cobertura do compat crescer; a saída do próprio CLI nunca fica.

Estágio 2 — dar contratos a ele

Depois que a suíte estiver funcionando, alguns passos valem a pena serem tipados. Isso é o que muda quando um deles é:

passo compatpasso tipado
Entradaapenas captura de padrão, não verificadavalidada contra um esquema args, cada campo com um .describe()
Saídadescartada — o result do recibo é nullvalidada contra um esquema returns e armazenada no recibo
Dependênciasefeitos colaterais no World, invisíveis na assinatura da funçãodeclaradas com from, nomeadas em um import, verificadas por nuka check antes de qualquer coisa ser executada e registradas como used no recibo
Palavra-chavedecorativa — um passo vinculado a Then ainda pode sofrer mutaçãomutates é uma declaração em que nukadoko confia: declare mutates: true e um ambiente somente leitura se recusa a executá-lo, e nuka check avisa se estiver vinculado a Then; o que realmente foi executado ainda é registrado nas contagens observed do recibo
Execução isoladanão é possível (o World está vazio fora de um cenário)nuka do <step> o executa diretamente, recibo impresso no stdout; uma chave from vem de --args como qualquer outra, ou de --use <receipt-id> para uma extraída de uma execução anterior; uma chamada de fixture resultOf dentro de run ainda não encontra nada, já que não há cenário para percorrer

Essa última linha é um fato separado da linha "Dependências" acima, não uma consequência dela: um passo compat não pode ser executado sozinho porque o que ele precisa está em um World que ninguém populou ainda, e o World não faz parte de sua assinatura — não há nada para inspecionar para saber o que configurar primeiro. As dependências de um passo tipado são nomeadas como entradas from, visíveis em um import, então uma chamada nuka do pode fornecê-las manualmente: --args para uma chave comum, ou --use <receipt-id> para uma extraída do resultado de uma execução anterior — o nome do próprio passo upstream nunca precisa aparecer na linha de comando, porque o recibo citado já o carrega. Um passo cujas todas as chaves chegam dessa forma não precisa de --args: --use por si só é uma invocação completa. O que ainda não encontra nada fora de um cenário é uma dependência lida através da fixture resultOf de dentro de run: essa chamada não tem cadeia para percorrer quando não houve cenário para construir uma, com ou sem --use.

Promova primeiro os passos que você executa com mais frequência, um de cada vez — não a suíte inteira em uma única passada. Como reescrever qualquer peça de cola é uma decisão a ser tomada no momento; nukadoko não prescreve uma receita única, e esta skill também não o fará.

Ao redigir o run de um passo tipado, às vezes se cai no formato não migrado run(ctx, args) antes de ser completamente desestruturado. nuka steps --json já nomeia quais fixtures esse rascunho toca, como needs_inferred: um palpite léxico lido dos acessos de membros do próprio ctx, junto com needs: null e um needs_error explicando por que o contrato real ainda não pode ser lido. Isso evita reler todo o corpo manualmente para saber o que adicionar à assinatura desestruturada, mas é um palpite, não o contrato: ele perde um alias (const c = ctx) e nunca nomeia needs_browser, então termine a desestruturação em vez de tratá-la como concluída.

A aparência de um passo promovido

Um exemplo. Antes, cola compat:

const { Given } = require("@cucumber/cucumber");

Given("um projeto {string} existe", async function (name) {
  const res = await this.request.post("/projects", { data: { name } });
  this.projectId = (await res.json()).id;
});

Depois, um passo tipado (features/steps/create-project.ts):

import { defineStep } from "nukadoko";
import { z } from "zod";

export default defineStep({
  pattern: "um projeto {name:string} existe",
  description: "Criar um projeto e retornar seu id",
  args: z.object({
    name: z.string().describe("o nome de exibição do projeto"),
  }),
  returns: z.object({
    id: z.string().describe("o id do projeto criado, para passos posteriores citarem"),
  }),
  mutates: true,
  async run({ request }, args) {
    const res = await request.post("/projects", { data: args });
    return res.json();
  },
});

O que mudou:

  • this.projectId desapareceu; o id retorna através de returns. Um passo posterior declara from: { projectId: [createProjectStep, "id"] } para lê-lo por chave — nuka check verifica a ordem de vinculação antes de qualquer execução, e a leitura aparece no recibo desse passo posterior (a fixture resultOf permanece disponível para a leitura mais rara que um nome de chave não pode expressar).
  • O argumento é uma captura nomeada vinculada a uma chave de esquema ({name:string} → args.name), então o padrão sozinho mostra qual texto se torna qual campo.
  • mutates: true agora é uma declaração, não apenas um fato sobre o que o código faz — nuka check avisa se este passo for vinculado a Then, e um ambiente somente leitura se recusa a executá-lo completamente. O recibo ainda registra o que a execução realmente enviou, para revisão, mas essa contagem não faz o passo ser rejeitado.
  • this.request se torna a fixture request, nomeada pela desestruturação do primeiro argumento: apenas o que o executor realmente injeta pode ser nomeado lá, nada implícito, e apenas os nomes realmente nomeados são construídos. Um passo que não nomeia nem page nem context nunca inicia um navegador.

Este é o único exemplo trabalhado aqui. Não é um catálogo de todas as lacunas entre compat e tipado — se outro padrão surgir com frequência suficiente para merecer um, isso é uma adição separada, não algo a ser improvisado a partir deste único caso.

Quando um hook After global era a limpeza

Uma suíte cucumber-js geralmente depende de um hook After global para limpar o que um passo criou, um inquilino, uma linha semeada, um arquivo temporário. Vale a pena revisitar isso assim que o passo que o criou for promovido: declare o recurso como uma fixture sob fixtures do próprio nukadoko.config.ts, com a limpeza escrita logo após await use(...) na mesma função que o construiu, em vez de deixada em um hook que precisa adivinhar, apenas por tags, quais cenários realmente precisam dela. defineFixtures (do pacote nukadoko) mantém a fixture totalmente tipada; qualquer passo tipado a alcança da mesma forma que alcança page ou request, desestruturando o nome. Isso é aditivo, não obrigatório: um hook After que limpa algo que nenhum passo tipado reivindicou ainda é exatamente tão válido quanto era no Estágio 1.

Vindo de uma DSL moldada como passos tipados

Se não houver arquivos de feature e nenhuma cola cucumber, a porta compat do Estágio 1 acima não é relevante — pule direto para além dela.

O que torna isso possível é que pattern é opcional em defineStep: um passo pode ser definido sem nenhum padrão e ainda ser uma peça completa de vocabulário apenas via CLI, executável com nuka do e inspecionável com nuka describe. Isso é um par diferente de estágios produzidos pelo mesmo princípio acima: mova cada passo para um defineStep tipado primeiro e agrupe-o em um pattern Gherkin depois, sempre que um arquivo de feature tornar isso válido.

Se a DSL de origem já carrega algo como description, args, returns, mutates e uma função run, a tradução para defineStep é direta — cada um tem um correspondente em defineStep para recebê-lo.

O que não fazer

  • Não faça o Estágio 1 e o Estágio 2 ao mesmo tempo. Misturar uma troca de import com tipagem adicionada na mesma alteração é exatamente o que torna uma falha não atribuível — veja "Dois estágios, nunca de uma só vez" acima.
  • Não deixe a suíte parar de funcionar enquanto está parcialmente migrada. Passos compat e tipados coexistem no mesmo arquivo de feature; uma suíte com alguns passos promovidos e outros ainda compat deve continuar passando durante todo o processo. Nunca torne "totalmente tipado" uma pré-condição para "funciona".
  • Não exclua a cola compat antes de seu substituto tipado estar escrito e passando. Escreva o novo passo, faça-o funcionar com nuka do, só então remova o antigo — nunca a ordem inversa.
  • Não tente adivinhar uma correção que nuka check ou nuka run não pediram. A saída deles é a evidência do que está errado; alterar algo que eles não sinalizaram é uma alteração feita por palpite, não no que realmente quebrou.

Habilidades Relacionadas