Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/cli/src/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
100 changes: 100 additions & 0 deletions packages/cli/src/__tests__/e2e-workflow.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>; 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)
})
})
12 changes: 11 additions & 1 deletion packages/cli/src/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.',
Expand Down
Loading