CommunityRedacción y edicióngithub.com

meganemura/nukadoko

Contratos tipados para pasos de Gherkin, permitiendo detectar escenarios incorrectos antes de ejecutarlos. El vocabulario es legible por agentes, y cada ejecución deja un recibo medido por la herramienta, no reportado por el agente.

¿Qué es nukadoko?

Nukadoko es una herramienta de contratos tipados para pasos de Gherkin que permite migrar gradualmente suites de pruebas existentes (como cucumber-js) hacia un sistema donde cada paso tiene un esquema de entrada, salida y dependencias validado. La palabra clave es 'migración en dos etapas nunca simultáneas' para garantizar trazabilidad de errores: primero se cambia la importación para que la suite funcione bajo nukadoko (compatibilidad), y luego se añaden contratos tipados paso a paso. Soporta tanto suites basadas en archivos .feature como DSLs propios con forma de pasos tipados. Además, ejecuta cada paso de forma aislada (nuka do), genera recibos de ejecución (receipts) que registran qué se ejecutó realmente y cómo, y expone un vocabulario legible por máquina para agentes de IA. Es útil para equipos que quieren migrar progresivamente sin reescribir toda la suite, manteniendo la suite verde durante la transición.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add meganemura/nukadoko

Installed? Explore more Redacción y edición skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Migrando a nukadoko

Para qué sirve

Migrar una suite existente a nukadoko, una pieza a la vez, sin reescribirla. El punto de partida varía:

  • Una suite de cucumber-js — archivos feature más pegamento (glue), normalmente usando Playwright.
  • Una suite ya estructurada como pasos tipados — su propio DSL, no el de cucumber, pero lo suficientemente parecido a defineStep de nukadoko como para que la migración sea principalmente traducción.

En cualquier caso, la migración ocurre en etapas, nunca de golpe; el resto de esta habilidad explica por qué y luego recorre cada punto de partida por turno.

Todo lo siguiente asume que el proyecto ya está inicializado. Si no lo está aún (sin nukadoko.config.ts), ejecuta nuka init primero.

Dos etapas, nunca a la vez

Cambia una cosa y luego la siguiente, nunca ambas a la vez. Si un paso se rompe después de que dos cambios lleguen juntos, no hay forma de saber cuál lo rompió: las causas están enredadas en un solo fallo. Dividir el trabajo en etapas mantiene cada fallo trazable hasta exactamente un cambio. Ese es el objetivo principal — las etapas en sí mismas son secundarias.

Si las etapas a continuación no encajan con tu punto de partida, deriva tus propias dos etapas a partir de esto. Lo que debe cumplirse es la trazabilidad, no estos nombres particulares de etapas.

Dos aplicaciones siguen: una suite de cucumber-js se divide en "cambiar la importación" y "añadir tipado" (abajo); una suite ya estructurada como pasos tipados se divide de forma diferente, descrita más adelante.

Viniendo de una suite de cucumber-js

Este es el caso al que se refieren las dos etapas anteriores.

Etapa 1 — haz que funcione

Cambia la importación que usa cada archivo de pegamento:

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

La extensión determina lo que descubre el sistema de lectura, no el historial de la suite: los archivos .ts, .mts, .js y .mjs se leen de la misma manera, por lo que una suite que siempre fue JavaScript plano no necesita reescribirse a TypeScript solo para empezar esta etapa. Un archivo .cjs es la única excepción: nukadoko es solo ESM, por lo que el descubrimiento nunca lo importa; nuka check lo nombra en su lugar (step-file-unsupported-extension) en lugar de dejarlo desaparecer como un undefined-step no explicado. Renombrarlo a .js solo ayuda si el código del archivo ya es sintaxis de módulo ES; si todavía usa require(...), fallará en la misma importación por la misma razón de ESM, y nuka check lo indicará como step-file-import-failed.

Luego ejecuta nuka check <feature> y nuka run <feature> y lee lo que dicen. Arregla lo que señalen y ejecútalos de nuevo. Repite hasta que la suite existente esté en verde — esa es la condición de finalización de la Etapa 1, nada más. Esa condición es que nuka check y nuka run estén en verde, no que tsc verifique tipos sin errores; los dos pueden diferir en cualquier dirección.

nuka steps y nuka describe siguen siendo utilizables durante toda la Etapa 1, incluso mientras algunos archivos de pegamento aún no se importan correctamente: ambos leen archivos de pasos uno a la vez, por lo que un archivo que aún no se importa se nombra (import_failures en --json, stderr en caso contrario) en lugar de vaciar todo el vocabulario. Lee el vocabulario sobre la marcha en lugar de esperar a que todos los archivos se importen limpios primero; nuka run <feature> y nuka do <step> aún se niegan rotundamente ante un archivo de pegamento ilegible, a propósito, ya que están a punto de ejecutarse.

No busques una lista de lo que compat no soporta antes de empezar. Cualquier cosa que no funcione fallará ruidosamente, ya sea en la importación o en el primer nuka run — el fallo nombra lo que se rompió, y eso es lo que hay que arreglar. Una lista escrita aquí quedaría obsoleta en cuanto la cobertura de compat crezca; la salida del CLI nunca lo hace.

Etapa 2 — dale contratos

Una vez que la suite funciona, algunos pasos merecen ser tipados. Esto es lo que cambia cuando uno lo es:

paso compatpaso tipado
Entradasolo captura de patrón, sin verificarvalidada contra un esquema args, cada campo con su .describe()
Salidadescartada — el result del recibo es nullvalidada contra un esquema returns y almacenada en el recibo
Dependenciasefectos secundarios en el World, invisibles en la firma de la funcióndeclaradas con from, nombradas en una importación, verificadas por nuka check antes de ejecutar nada, y registradas como used en el recibo
Palabra clavedecorativa — un paso vinculado a Then aún puede mutarmutates es una declaración en la que nukadoko confía: declarar mutates: true y un entorno de solo lectura se niega a ejecutarlo, y nuka check advierte si está vinculado a Then; lo que realmente se ejecutó se registra en los contadores observed del recibo
Ejecución aisladano posible (el World está vacío fuera de un escenario)nuka do <step> lo ejecuta directamente, recibo impreso en stdout; una clave from viene de --args como cualquier otra, o de --use <receipt-id> para una obtenida de una ejecución anterior; una llamada a fixture resultOf dentro de run aún no encuentra nada, ya que no hay escenario para recorrer

Esa última fila es un hecho separado de la fila "Dependencias" anterior, no una consecuencia de ella: un paso compat no puede ejecutarse solo porque lo que necesita vive en un World que nadie ha poblado aún, y el World no es parte de su firma — no hay nada que inspeccionar para saber qué configurar primero. Las dependencias de un paso tipado se nombran como entradas from, visibles en una importación, por lo que una llamada nuka do puede proporcionarlas manualmente: --args para una clave normal, o --use <receipt-id> para una obtenida del resultado de una ejecución anterior — el nombre del paso ascendente nunca tiene que aparecer en la línea de comandos, porque el recibo citado ya lo lleva. Un paso cuyas claves lleguen todas de esa manera no necesita --args en absoluto: --use por sí solo es una invocación completa. Lo que aún no encuentra nada fuera de un escenario es una dependencia leída a través de la fixture resultOf desde dentro de run: esa llamada no tiene cadena que recorrer cuando no hay escenario para construir una, con --use o sin él.

Promociona primero los pasos que ejecutas con más frecuencia, uno a la vez — no toda la suite de una sola vez. Cómo reescribir cualquier pieza de pegamento es una decisión que se toma en el momento; nukadoko no prescribe una receta única, y esta habilidad tampoco lo hará.

Al redactar el run de un paso tipado, a veces se aterriza en la forma no migrada run(ctx, args) antes de desestructurarla completamente. nuka steps --json ya nombra qué fixtures toca ese borrador, como needs_inferred: una suposición léxica leída de los accesos a miembros de ctx, junto con needs: null y un needs_error que explica por qué el contrato real aún no puede leerse. Ahorra tener que releer todo el cuerpo manualmente para saber qué añadir a la firma desestructurada, pero es una suposición, no el contrato: se pierde un alias (const c = ctx) y nunca nombra needs_browser, así que termina de desestructurar en lugar de tratarlo como terminado.

Cómo se ve un paso promocionado

Un ejemplo. Antes, pegamento compat:

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

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

Después, un paso tipado (features/steps/create-project.ts):

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

export default defineStep({
  pattern: "un proyecto {name:string} existe",
  description: "Crear un proyecto y devolver su id",
  args: z.object({
    name: z.string().describe("el nombre visible del proyecto"),
  }),
  returns: z.object({
    id: z.string().describe("el id del proyecto creado, para que pasos posteriores lo citen"),
  }),
  mutates: true,
  async run({ request }, args) {
    const res = await request.post("/projects", { data: args });
    return res.json();
  },
});

Lo que cambió:

  • this.projectId desaparece; el id vuelve a través de returns. Un paso posterior declara from: { projectId: [createProjectStep, "id"] } para leerlo por clave — nuka check verifica el orden de vinculación antes de ejecutar nada, y la lectura aparece en el recibo de ese paso posterior (la fixture resultOf sigue disponible para la lectura más rara que un nombre de clave no puede expresar).
  • El argumento es una captura nombrada vinculada a una clave del esquema ({name:string} → args.name), por lo que el patrón solo muestra qué texto se convierte en qué campo.
  • mutates: true ahora es una declaración, no solo un hecho sobre lo que el código hace — nuka check advierte si este paso se vincula alguna vez a Then, y un entorno de solo lectura se niega a ejecutarlo por completo. El recibo aún registra lo que la ejecución realmente envió, para revisión, pero ese recuento no hace que el paso sea rechazado.
  • this.request se convierte en la fixture request, nombrada al desestructurar el primer argumento: solo lo que el ejecutor realmente inyecta puede nombrarse allí, nada implícito, y solo los nombres realmente nombrados se construyen. Un paso que no nombra ni page ni context nunca inicia un navegador.

Este es el único ejemplo desarrollado aquí. No es un catálogo de todas las brechas entre compat y tipado — si otro patrón surge con suficiente frecuencia como para merecer uno, esa es una adición separada, no algo que improvisar a partir de este único caso.

Cuando un hook global After era la limpieza

Una suite de cucumber-js a menudo se apoya en un hook global After para limpiar lo que un paso creó: un inquilino, una fila sembrada, un archivo temporal. Vale la pena revisarlo una vez que el paso que lo creó se promociona: declara el recurso como una fixture bajo fixtures en nukadoko.config.ts propiamente, con la limpieza escrita justo después de await use(...) en la misma función que lo construyó, en lugar de dejarlo en un hook que tiene que adivinar, solo a partir de etiquetas, qué escenarios realmente lo necesitan. defineFixtures (del paquete nukadoko) mantiene la fixture completamente tipada; cualquier paso tipado la alcanza de la misma manera que alcanza page o request, desestructurando el nombre. Esto es aditivo, no obligatorio: un hook After que limpia algo que ningún paso tipado ha reclamado aún sigue siendo exactamente tan válido como lo era en la Etapa 1.

Viniendo de un DSL con forma de pasos tipados

Si no hay archivos feature ni pegamento cucumber, la puerta compat de la Etapa 1 anterior no es relevante — salta directamente.

Lo que hace posible esto es que pattern es opcional en defineStep: un paso puede definirse sin patrón alguno y seguir siendo una pieza completa de vocabulario solo para CLI, ejecutable con nuka do e inspeccionable con nuka describe. Es un par diferente de etapas producido por el mismo principio anterior: mueve cada paso a un defineStep tipado primero, y agrúpalo en un pattern de Gherkin más tarde, cuando un archivo feature haga que valga la pena.

Si el DSL fuente ya lleva algo como una description, args, returns, mutates y una función run, la traducción a defineStep es directa — cada uno tiene un contraparte en defineStep para recibirlo.

Lo que no debes hacer

  • No hagas la Etapa 1 y la Etapa 2 a la vez. Mezclar un cambio de importación con tipado añadido en el mismo cambio es exactamente lo que hace que un fallo sea no atribuible — consulta "Dos etapas, nunca a la vez" arriba.
  • No dejes que la suite deje de funcionar mientras está parcialmente migrada. Los pasos compat y tipados coexisten en el mismo archivo feature; una suite con algunos pasos promocionados y otros aún compat debe seguir pasando durante todo el proceso. Nunca hagas de "completamente tipado" un requisito previo para "funciona".
  • No elimines el pegamento compat antes de que su reemplazo tipado esté escrito y pasando. Escribe el nuevo paso, hazlo funcionar con nuka do, solo entonces elimina el viejo — nunca en el orden inverso.
  • No adivines una solución que nuka check o nuka run no pidieron. Su salida es la evidencia de lo que está mal; cambiar algo que no señalaron es un cambio basado en una corazonada, no en lo que realmente se rompió.

Skills relacionados