协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
test-cli-application【开发与工程】Write integration tests for a Node.js CLI application using the built-in node:test module. Covers the exec helper pattern, output assertions, filesystem state verification, cleanup hooks, JSON output parsing, error case testing, and state restoration after destructive tests. Use when adding tests to an existing CLI, testing a new command, verifying a…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Escribir pruebas de integración para una CLI Node.js usando el módulo built-in node:test con execSync.
cli/index.js)import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { execSync } from 'child_process';
import { existsSync, rmSync } from 'fs';
import { resolve } from 'path';
const CLI = 'node cli/index.js';
const ROOT = process.cwd();
function run(args) {
return execSync(`${CLI} ${args}`, {
cwd: ROOT,
encoding: 'utf8',
timeout: 10000,
});
}
Decisiones clave de diseño:
node:test está built-in — no se necesita dependencia de test runnerexecSync ejecuta la CLI como subproceso — prueba el binario real, no funciones internasencoding: 'utf8' da salida string para coincidencia con regexROOT para reproducibilidadEsperado: Un archivo de pruebas que importa de node:test y tiene un helper run() funcional.
En caso de fallo: Si node:test no está disponible, tu versión de Node.js está debajo de 18. Actualizar o usar un polyfill.
Las pruebas smoke verifican que la CLI inicia, parsea argumentos y produce las formas de salida esperadas:
describe('meta', () => {
it('shows version', () => {
const out = run('--version');
assert.match(out, /\d+\.\d+\.\d+/);
});
it('shows help with all commands', () => {
const out = run('--help');
assert.match(out, /install/);
assert.match(out, /list/);
assert.match(out, /detect/);
});
});
describe('registry', () => {
it('list shows expected counts', () => {
const out = run('list --domains');
assert.match(out, /\d+ domains/);
});
it('search finds known items', () => {
const out = run('search "docker"');
assert.match(out, /result\(s\) for "docker"/);
});
it('search returns 0 for nonsense', () => {
const out = run('search "xyzzy-nonexistent"');
assert.match(out, /0 result/);
});
});
Patrones de prueba smoke:
--version y --help siempre funcionanEsperado: Las pruebas smoke confirman que la CLI es funcional y los datos están cargados.
En caso de fallo: Si los conteos del registry cambian frecuentemente, usar \d+ en lugar de números hardcoded.
Las pruebas de lifecycle verifican secuencias create → verify → delete con limpieza:
describe('install', () => {
const testPath = resolve(ROOT, '.agents/skills/commit-changes');
after(() => {
// Always clean up, even if tests fail
try { rmSync(testPath); } catch {}
try { rmSync(resolve(ROOT, '.agents/skills'), { recursive: true }); } catch {}
try { rmSync(resolve(ROOT, '.agents'), { recursive: true }); } catch {}
});
it('dry-run does not create files', () => {
const out = run('install commit-changes --dry-run');
assert.match(out, /DRY RUN/);
assert.ok(!existsSync(testPath));
});
it('installs creates the target', () => {
run('install commit-changes');
assert.ok(existsSync(testPath));
});
it('skips already installed', () => {
const out = run('install commit-changes');
assert.match(out, /skipped/);
});
it('uninstall removes the target', () => {
run('uninstall commit-changes');
assert.ok(!existsSync(testPath));
});
});
Reglas de limpieza:
after(), no afterEach() — las pruebas de lifecycle se construyen unas sobre otrastry/catch — la limpieza no debe fallar la suite de pruebasEsperado: Las pruebas corren en secuencia dentro del bloque describe, la limpieza corre incluso en caso de fallo.
En caso de fallo: Si las pruebas corren en paralelo (no-predeterminado en node:test), forzar secuencial con { concurrency: 1 }.
Probar la ruta objetivo de cada adaptador sin hacer cambios:
describe('adapter: cursor (dry-run)', () => {
it('targets .cursor/skills/ path', () => {
const out = run('install commit-changes --framework cursor --dry-run');
assert.match(out, /\.cursor\/skills/i);
});
});
describe('adapter: copilot (dry-run)', () => {
it('targets .github/ path', () => {
const out = run('install commit-changes --framework copilot --dry-run');
assert.match(out, /\.github/i);
});
});
Este patrón escala a cualquier número de adaptadores. Cada prueba:
--framework para sortear la auto-detección--dry-run para que no se creen archivosEsperado: Un bloque describe por adaptador, cada uno con al menos una assertion de ruta.
En caso de fallo: Si el adaptador no existe en el proyecto, la prueba fallará con "Unknown framework." Esto es correcto — las pruebas de adaptador solo deben existir para adaptadores implementados.
describe('errors', () => {
it('rejects unknown items', () => {
assert.throws(
() => run('install nonexistent-skill-xyz'),
/No matching items|Unknown/,
);
});
it('rejects unknown framework', () => {
assert.throws(
() => run('install commit-changes --framework nonexistent'),
/Unknown framework/,
);
});
it('handles missing state gracefully', () => {
assert.throws(
() => run('scatter nonexistent-team'),
/not burning|Unknown/,
);
});
});
Patrones de prueba de errores:
assert.throws captura códigos de salida no-cero de execSyncEsperado: Todos los caminos de error producen códigos de salida no-cero y mensajes útiles.
En caso de fallo: execSync lanza en caso de salida no-cero. El stderr o stdout del error contiene el mensaje. Verificar error.stdout si la regex de assert.throws no coincide.
describe('json output', () => {
it('campfire --json outputs valid JSON', () => {
const out = run('campfire --json');
const data = JSON.parse(out);
assert.ok(typeof data.totalTeams === 'number');
assert.ok(Array.isArray(data.fires));
});
it('gather --dry-run --json outputs structured data', () => {
const out = run('gather tending --dry-run --json');
// JSON may follow a DRY RUN header — extract from first '{'
const jsonStart = out.indexOf('{');
assert.ok(jsonStart >= 0, 'Should contain JSON');
const data = JSON.parse(out.slice(jsonStart));
assert.equal(data.team, 'tending');
});
});
Trampas de prueba JSON:
{Esperado: La salida JSON es parseable y contiene las claves esperadas.
En caso de fallo: Si JSON.parse falla, el comando puede estar mezclando texto humano con JSON. O arreglar el comando para emitir JSON puro en modo --json, o extraer el substring JSON.
describe('stateful commands', () => {
const stateDir = resolve(ROOT, '.agent-almanac');
after(() => {
// Remove state file created by tests
try { rmSync(stateDir, { recursive: true }); } catch {}
});
// Tests that create/modify state...
});
// Restore symlinks that destructive tests may remove
describe('destructive tests', () => {
after(() => {
// Restore symlinks that scatter/uninstall removed
const skills = ['heal', 'meditate', 'remote-viewing'];
for (const skill of skills) {
const link = resolve(ROOT, `.claude/skills/${skill}`);
if (!existsSync(link)) {
try {
execSync(`ln -s ../../skills/${skill} ${link}`, { cwd: ROOT });
} catch {}
}
}
});
});
Reglas de restauración de estado:
.agent-almanac/state.json) deben limpiarse después de las pruebasscatter/uninstall deben ser restauradosagent-almanac.yml) creados por init deben eliminarseafter() corren en orden inverso de declaración — declarar hooks de restauración al finalEsperado: La suite de pruebas deja el proyecto en el mismo estado en que lo encontró.
En caso de fallo: Si CI reporta archivos sobrantes después de las ejecuciones de prueba, añadir la limpieza al after(). Usar git status después de las ejecuciones de prueba para detectar estado filtrado.
node --test cli/test/cli.test.js--version, --help y carga del registry\d+ o leer el conteo dinámicamente en lugar de assertear 329 skills.node:test corre suites en orden de declaración por defecto, pero las pruebas dentro de una suite pueden no. Usar suites de lifecycle (create → verify → delete) dentro de un solo describe para garantizar el orden.after() aún corre. Pero si lanzas en before(), las pruebas subsiguientes y after() pueden no correr. Mantener before() mínimo.execSync. O hacer pipe de echo y | o asegurar que --yes siempre se pase en pruebas..claude/skills/ o .agents/skills/ modifican el árbol de trabajo. CI puede fallar en verificaciones de "directorio de trabajo sucio". Siempre limpiar.scaffold-cli-command — construir los comandos que estas pruebas verificanbuild-cli-plugin — construir los adaptadores probados en el Paso 4design-cli-output — patrones de salida contra los que las pruebas assertan