协作头脑风暴
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 快照 · 自动分类 · 以原作者文档为准
Integrations-Tests fuer eine Node.js-CLI mit dem eingebauten node:test-Modul mit execSync schreiben.
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,
});
}
Schluessel-Design-Entscheidungen:
node:test ist eingebaut — keine Test-Runner-Abhaengigkeit noetigexecSync fuehrt die CLI als Subprozess aus — testet das tatsaechliche Binary, nicht interne Funktionenencoding: 'utf8' gibt String-Ausgabe fuer Regex-MatchingROOT fuer ReproduzierbarkeitErwartet: Eine Test-Datei die aus node:test importiert und einen funktionierenden run()-Helper hat.
Bei Fehler: Wenn node:test nicht verfuegbar ist, ist deine Node.js-Version unter 18. Upgraden oder einen Polyfill nutzen.
Smoke-Tests verifizieren dass die CLI startet, Argumente parst und erwartete Ausgabe-Formen produziert:
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/);
});
});
Smoke-Test-Muster:
--version und --help funktionieren immerErwartet: Smoke-Tests bestaetigen dass die CLI funktional ist und Daten geladen sind.
Bei Fehler: Wenn Registry-Anzahlen sich haeufig aendern, \d+ statt hartcodierter Zahlen nutzen.
Lifecycle-Tests verifizieren Erstellen → Verifizieren → Loeschen-Sequenzen mit Cleanup:
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));
});
});
Cleanup-Regeln:
after()-Hooks nutzen, nicht afterEach() — Lifecycle-Tests bauen aufeinander auftry/catch wickeln — Cleanup darf die Test-Suite nicht scheitern lassenErwartet: Tests laufen sequenziell innerhalb des describe-Blocks, Cleanup laeuft auch bei Versagen.
Bei Fehler: Wenn Tests parallel laufen (Nicht-Default in node:test), sequenziell mit { concurrency: 1 } erzwingen.
Den Ziel-Pfad jedes Adapters testen ohne Aenderungen zu machen:
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);
});
});
Dieses Muster skaliert auf jede Anzahl Adapter. Jeder Test:
--framework um Auto-Detection zu umgehen--dry-run damit keine Dateien erstellt werdenErwartet: Ein describe-Block pro Adapter, jeder mit mindestens einer Pfad-Behauptung.
Bei Fehler: Wenn der Adapter im Projekt nicht existiert, wird der Test mit "Unknown framework" scheitern. Das ist korrekt — Adapter-Tests sollten nur fuer implementierte Adapter existieren.
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/,
);
});
});
Fehler-Test-Muster:
assert.throws faengt Nicht-Null-Exit-Codes von execSyncErwartet: Alle Fehler-Pfade produzieren Nicht-Null-Exit-Codes und hilfreiche Nachrichten.
Bei Fehler: execSync wirft bei Nicht-Null-Exit. Der stderr oder stdout des Fehlers enthaelt die Nachricht. error.stdout pruefen wenn assert.throws Regex nicht matcht.
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');
});
});
JSON-Test-Fallstricke:
{-Zeichen gefunden wirdErwartet: JSON-Ausgabe ist parsebar und enthaelt erwartete Schluessel.
Bei Fehler: Wenn JSON.parse scheitert, mischt der Befehl moeglicherweise menschlichen Text mit JSON. Entweder den Befehl reparieren um pures JSON in --json-Modus auszugeben oder den JSON-Substring extrahieren.
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 {}
}
}
});
});
State-Wiederherstellungs-Regeln:
.agent-almanac/state.json) muessen nach Tests aufgeraeumt werdenscatter/uninstall entfernt wurden muessen wiederhergestellt werdenagent-almanac.yml) die durch init erstellt wurden muessen entfernt werdenafter()-Hooks laufen in umgekehrter Deklarations-Reihenfolge — Restore-Hooks zuletzt deklarierenErwartet: Die Test-Suite hinterlaesst das Projekt in demselben Zustand in dem sie es vorgefunden hat.
Bei Fehler: Wenn CI Restdateien nach Test-Laeufen berichtet, das Cleanup zu after() hinzufuegen. git status nach Test-Laeufen nutzen um geleckten State zu erkennen.
node --test cli/test/cli.test.js--version, --help und Registry-Laden ab\d+-Regex nutzen oder die Anzahl dynamisch lesen statt 329 skills zu behaupten.node:test laeuft Suites in Deklarations-Reihenfolge per Default, aber Tests innerhalb einer Suite koennen das nicht. Lifecycle-Suites (Erstellen → Verifizieren → Loeschen) innerhalb eines einzelnen describe nutzen um Reihenfolge zu garantieren.after() immer noch. Aber wenn man in before() wirft, koennen nachfolgende Tests und after() nicht laufen. before() minimal halten.execSync haengen. Entweder echo y | pipen oder sicherstellen dass --yes immer in Tests uebergeben wird..claude/skills/ oder .agents/skills/ erstellen modifizieren den Working-Tree. CI kann an "dirty working directory"-Pruefungen scheitern. Immer aufraeumen.scaffold-cli-command — die Befehle bauen die diese Tests verifizierenbuild-cli-plugin — die in Schritt 4 getesteten Adapter bauendesign-cli-output — Ausgabe-Muster gegen die Tests behaupten