claude-flow
Version:
Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration
104 lines (91 loc) • 4.49 kB
JavaScript
/**
* Regression guard for ruvnet/ruflo#1863 (and the crash class it represents).
*
* #1863: `task status <id>` threw `TypeError: Cannot read properties of
* undefined (reading 'join')` because the command formatter did
* `result.dependencies.join(', ')` where `dependencies` is typed
* `string[]` by the callMCPTool<{...}>() generic but the MCP server can
* legitimately omit it (older store schema, task with no deps). The TS
* compiler can't see the risk; only running the command does.
*
* This smoke drives a handful of CLI command paths that format MCP
* responses and asserts none of them crash with an unhandled exception
* (exit code 1 *with* a TypeError/ReferenceError stack — graceful "not
* found" errors are fine, only unhandled crashes fail the gate).
*
* Each command runs with a 30s per-command timeout. The whole script
* has a 90s process-level watchdog so CI never hangs.
*/
import { spawnSync } from 'node:child_process';
import { resolve, dirname } from 'node:path';
import { existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(process.argv[2] ?? resolve(__dirname, '..', '..', '..'));
const CLI = resolve(REPO_ROOT, 'v3/@claude-flow/cli/bin/cli.js');
if (!existsSync(CLI)) {
console.error(`FAIL: ${CLI} not found — run \`npm --prefix v3/@claude-flow/cli run build\` first`);
process.exit(1);
}
let failed = 0;
const fail = (m) => { console.error(`FAIL: ${m}`); failed++; };
const pass = (m) => console.log(`ok: ${m}`);
// 90s process-level watchdog.
const watchdog = setTimeout(() => {
console.log(`\n[watchdog] cli-no-crash exceeded 90s — exiting clean (CI safety). Checks completed so far stand.`);
process.exit(failed > 0 ? 1 : 0);
}, 90_000);
watchdog.unref();
const CRASH_RE = /TypeError|ReferenceError|Cannot read propert|is not a function|is not defined|Unhandled|UnhandledPromiseRejection/i;
function runCli(args, label) {
const r = spawnSync('node', [CLI, ...args], {
cwd: REPO_ROOT,
timeout: 30_000,
encoding: 'utf-8',
env: { ...process.env, CI: 'true' },
});
const out = `${r.stdout ?? ''}\n${r.stderr ?? ''}`;
if (r.error && r.error.code === 'ETIMEDOUT') {
console.log(`note: ${label} — timed out after 30s (env-dependent backend); not failing on this`);
return;
}
// A crash is: an exception stack in the output. A graceful error (exit 1
// with a clean "not found" message and no stack) is acceptable.
if (CRASH_RE.test(out)) {
fail(`${label} — command crashed with an unhandled exception:\n${out.split('\n').filter(l => CRASH_RE.test(l)).slice(0, 3).join('\n')}`);
} else {
pass(`${label} — no unhandled exception (exit ${r.status})`);
}
}
// --- #1863 — task create then task status must not crash on a fresh task ---
// Create a task; capture an ID if printed.
const createRes = spawnSync('node', [CLI, 'task', 'create', '-t', 'research', '-d', 'cli-no-crash smoke task'], {
cwd: REPO_ROOT, timeout: 30_000, encoding: 'utf-8', env: { ...process.env, CI: 'true' },
});
const createOut = `${createRes.stdout ?? ''}\n${createRes.stderr ?? ''}`;
if (CRASH_RE.test(createOut)) {
fail(`#1863-create — \`task create\` crashed:\n${createOut.split('\n').filter(l => CRASH_RE.test(l)).slice(0, 3).join('\n')}`);
} else {
pass(`#1863-create — \`task create\` did not crash (exit ${createRes.status})`);
// Try to extract a task id (formats: "task-...", "Task ... created", uuid-ish)
const idMatch = createOut.match(/\btask[-_][\w-]+\b/i) || createOut.match(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i);
if (idMatch) {
runCli(['task', 'status', idMatch[0]], `#1863-status — \`task status ${idMatch[0]}\``);
} else {
// No ID extracted — fall back to `task list` which also formats the same arrays.
runCli(['task', 'list'], '#1863-list — `task list` (formats the same arrays)');
}
}
// --- A few other command formatters that touch MCP-typed arrays ---
runCli(['task', 'list'], 'task list');
runCli(['agent', 'list'], 'agent list');
runCli(['memory', 'list'], 'memory list');
runCli(['swarm', 'status'], 'swarm status');
clearTimeout(watchdog);
if (failed > 0) {
console.error(`\n${failed} CLI command(s) crashed with an unhandled exception — see above. (#1863 class)`);
process.exit(1);
}
console.log(`\nall cli-no-crash checks green (#1863 class)`);
process.exit(0);