diff --git a/packages/cli/src/__tests__/cli.test.ts b/packages/cli/src/__tests__/cli.test.ts index eab5038..78b5d9d 100644 --- a/packages/cli/src/__tests__/cli.test.ts +++ b/packages/cli/src/__tests__/cli.test.ts @@ -119,11 +119,11 @@ describe('agentspec generate', () => { expect(combined).toContain('ANTHROPIC_API_KEY') }) - it('exits 1 with --dry-run when ANTHROPIC_API_KEY is missing', async () => { + it('exits 0 with --dry-run when ANTHROPIC_API_KEY is missing (validation-only mode)', async () => { const result = await runCli( ['generate', exampleManifest, '--framework', 'langgraph', '--dry-run'], { ANTHROPIC_API_KEY: '' }, ) - expect(result.exitCode).toBe(1) + expect(result.exitCode).toBe(0) }) }) diff --git a/packages/cli/src/__tests__/e2e-workflow.test.ts b/packages/cli/src/__tests__/e2e-workflow.test.ts new file mode 100644 index 0000000..3bceecf --- /dev/null +++ b/packages/cli/src/__tests__/e2e-workflow.test.ts @@ -0,0 +1,100 @@ +/** + * E2E workflow test for the AgentSpec CLI. + * + * Exercises the full user workflow in sequence: + * init -> validate -> health -> audit -> generate + * + * Each step depends on the output of the previous step, + * so tests run sequentially within a single shared temp directory. + */ + +import { execa } from 'execa' +import { fileURLToPath } from 'node:url' +import { dirname, join, resolve } from 'node:path' +import { mkdtempSync, rmSync, existsSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { describe, it, expect, beforeAll, afterAll } from 'vitest' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const repoRoot = resolve(__dirname, '../../../..') +const tsxBin = join(repoRoot, 'node_modules/.bin/tsx') +const cliSrc = join(repoRoot, 'packages/cli/src/cli.ts') + +async function runCli( + args: string[], + opts?: { env?: Record; cwd?: string }, +) { + return execa(tsxBin, [cliSrc, ...args], { + cwd: opts?.cwd ?? repoRoot, + reject: false, + env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1', ...opts?.env }, + }) +} + +describe('CLI workflow E2E: init -> validate -> health -> audit -> generate', { timeout: 30_000 }, () => { + let tmpDir: string + let manifestPath: string + + beforeAll(() => { + tmpDir = mkdtempSync(join(tmpdir(), 'agentspec-e2e-')) + manifestPath = join(tmpDir, 'agent.yaml') + }) + + afterAll(() => { + rmSync(tmpDir, { recursive: true, force: true }) + }) + + // ── Step 1: init --yes ──────────────────────────────────────────────────── + + it('init --yes creates agent.yaml and prompts/system.md', async () => { + const result = await runCli(['init', tmpDir, '--yes']) + + expect(result.exitCode).toBe(0) + expect(existsSync(manifestPath)).toBe(true) + expect(existsSync(join(tmpDir, 'prompts', 'system.md'))).toBe(true) + }) + + // ── Step 2: validate ────────────────────────────────────────────────────── + + it('validate exits 0 for the generated manifest', async () => { + const result = await runCli(['validate', manifestPath]) + + expect(result.exitCode).toBe(0) + }) + + // ── Step 3: health (skip network checks) ────────────────────────────────── + + it('health --no-model --no-mcp --no-memory exits 0', async () => { + const result = await runCli( + ['health', manifestPath, '--no-model', '--no-mcp', '--no-memory'], + { env: { OPENAI_API_KEY: 'test-dummy-key' } }, + ) + + expect(result.exitCode).toBe(0) + }) + + // ── Step 4: audit --json ────────────────────────────────────────────────── + + it('audit --json exits 0 and reports a positive score', async () => { + const result = await runCli(['audit', manifestPath, '--json']) + + expect(result.exitCode).toBe(0) + + const report = JSON.parse(result.stdout) + expect(report.overallScore).toBeGreaterThan(0) + }) + + // ── Step 5: generate --dry-run (no API key required) ────────────────────── + + it('generate --dry-run exits 0 without an API key', async () => { + const outDir = join(tmpDir, 'out') + const result = await runCli( + ['generate', manifestPath, '--framework', 'langgraph', '--output', outDir, '--dry-run'], + { env: { ANTHROPIC_API_KEY: '' } }, + ) + + expect(result.exitCode).toBe(0) + }) +}) diff --git a/packages/cli/src/commands/generate.ts b/packages/cli/src/commands/generate.ts index 8cb6771..6ef6af3 100644 --- a/packages/cli/src/commands/generate.ts +++ b/packages/cli/src/commands/generate.ts @@ -5,7 +5,7 @@ import chalk from 'chalk' import { spinner } from '../utils/spinner.js' import { loadManifest } from '@agentspec/sdk' import { generateWithClaude, listFrameworks } from '@agentspec/adapter-claude' -import { printHeader, printError, printSuccess } from '../utils/output.js' +import { printHeader, printError, printSuccess, symbols } from '../utils/output.js' import { generateK8sManifests } from '../deploy/k8s.js' const DEPLOY_TARGETS = ['k8s', 'helm'] as const @@ -226,6 +226,16 @@ export function registerGenerateCommand(program: Command): void { // ── LLM-driven generation (framework code or helm chart) ───────────── if (!process.env['ANTHROPIC_API_KEY']) { + if (opts.dryRun) { + printHeader(`AgentSpec Generate - ${opts.framework} (dry-run)`) + printSuccess(`Manifest validated - ${chalk.cyan(parsed.manifest.metadata.name)} v${parsed.manifest.metadata.version}`) + console.log(chalk.gray(` Framework : ${opts.framework}`)) + console.log(chalk.gray(` Output : ${opts.output}`)) + console.log() + console.log(` ${symbols.info} ${chalk.gray('ANTHROPIC_API_KEY not set - set the key to preview generated files.')}`) + console.log() + return + } printError( 'ANTHROPIC_API_KEY is not set. AgentSpec generates code using Claude.\n' + ' Get a key at https://console.anthropic.com and add it to your environment.',