UNPKG

@stillrivercode/agentic-workflow-template

Version:

NPM package to create AI-powered GitHub workflow automation projects

411 lines (348 loc) 12.3 kB
const fs = require('fs-extra'); const path = require('path'); const { spawn } = require('child_process'); const { createProject } = require('../create-project'); describe('CLI Integration Tests', () => { const testDir = path.join(__dirname, '../../test-projects'); const cliPath = path.join(__dirname, '../index.js'); beforeAll(async () => { // Ensure test directory exists and is clean await fs.ensureDir(testDir); }); afterEach(async () => { // Clean up test projects if (await fs.pathExists(testDir)) { await fs.emptyDir(testDir); } }); afterAll(async () => { // Remove test directory if (await fs.pathExists(testDir)) { await fs.remove(testDir); } }); describe('Non-Interactive Mode', () => { test('should create project with all CLI options', async () => { const projectName = 'test-cli-project'; const projectPath = path.join(testDir, projectName); const options = { force: true, nonInteractive: true, githubOrg: 'test-org', repoName: 'test-repo', description: 'Test project description', template: 'default', features: 'ai-tasks,ai-pr-review,security', }; await createProject(projectName, options); // Move project to test directory for verification if (await fs.pathExists(projectName)) { await fs.move(projectName, projectPath); } // Verify project structure expect(await fs.pathExists(projectPath)).toBe(true); expect(await fs.pathExists(path.join(projectPath, 'README.md'))).toBe( true ); expect(await fs.pathExists(path.join(projectPath, 'CLAUDE.md'))).toBe( true ); expect(await fs.pathExists(path.join(projectPath, 'package.json'))).toBe( true ); expect(await fs.pathExists(path.join(projectPath, '.github'))).toBe(true); // Verify configuration const config = await fs.readJson( path.join(projectPath, '.github/repo-config.json') ); expect(config.name).toBe('test-repo'); expect(config.owner).toBe('test-org'); expect(config.description).toBe('Test project description'); expect(config.template).toBe('default'); expect(config.features).toEqual(['ai-tasks', 'ai-pr-review', 'security']); // Verify README customization const readme = await fs.readFile( path.join(projectPath, 'README.md'), 'utf8' ); expect(readme).toContain('test-repo'); expect(readme).toContain('AI Workflow Template'); // Check for actual content }, 15000); // Reduced from 30s for faster CI execution test('should use defaults when minimal options provided', async () => { const projectName = 'test-minimal-project'; const projectPath = path.join(testDir, projectName); const options = { force: true, nonInteractive: true, githubOrg: 'test-org', }; await createProject(projectName, options); // Move project to test directory if (await fs.pathExists(projectName)) { await fs.move(projectName, projectPath); } // Verify defaults were applied const config = await fs.readJson( path.join(projectPath, '.github/repo-config.json') ); expect(config.name).toBe(projectName); expect(config.owner).toBe('test-org'); expect(config.description).toBe('AI-powered workflow automation project'); expect(config.template).toBe('default'); expect(config.features).toEqual([ 'ai-tasks', 'ai-pr-review', 'cost-monitoring', 'security', ]); }); test('should handle different templates', async () => { const templates = ['default', 'minimal', 'enterprise']; for (const template of templates) { const projectName = `test-${template}-project`; const projectPath = path.join(testDir, projectName); const options = { force: true, nonInteractive: true, githubOrg: 'test-org', template: template, }; await createProject(projectName, options); // Move project to test directory if (await fs.pathExists(projectName)) { await fs.move(projectName, projectPath); } // Verify template was applied const config = await fs.readJson( path.join(projectPath, '.github/repo-config.json') ); expect(config.template).toBe(template); // Clean up for next iteration await fs.remove(projectPath); } }); test('should parse features correctly', async () => { const projectName = 'test-features-project'; const projectPath = path.join(testDir, projectName); const options = { force: true, nonInteractive: true, githubOrg: 'test-org', features: 'ai-tasks, security, cost-monitoring', // Test with spaces }; await createProject(projectName, options); // Move project to test directory if (await fs.pathExists(projectName)) { await fs.move(projectName, projectPath); } const config = await fs.readJson( path.join(projectPath, '.github/repo-config.json') ); expect(config.features).toEqual([ 'ai-tasks', 'security', 'cost-monitoring', ]); }); }); describe('CLI Command Execution', () => { test('should execute CLI with --help flag', (done) => { const child = spawn('node', [cliPath, '--help'], { stdio: 'pipe', }); let stdout = ''; child.stdout.on('data', (data) => { stdout += data.toString(); }); child.on('close', (code) => { expect(code).toBe(0); expect(stdout).toContain( 'Create AI-powered GitHub workflow automation projects' ); expect(stdout).toContain('--non-interactive'); expect(stdout).toContain('--github-org'); expect(stdout).toContain('--template'); done(); }); }); test('should execute CLI with --version flag', (done) => { const child = spawn('node', [cliPath, '--version'], { stdio: 'pipe', }); let stdout = ''; child.stdout.on('data', (data) => { stdout += data.toString(); }); child.on('close', (code) => { expect(code).toBe(0); expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/); done(); }); }); test('should execute CLI in non-interactive mode', (done) => { const projectName = 'test-cli-execution'; const child = spawn( 'node', [ cliPath, projectName, '--force', '--non-interactive', '--github-org', 'test-org', '--repo-name', projectName, '--description', 'CLI test project', ], { stdio: 'pipe', cwd: testDir, } ); let stdout = ''; child.stdout.on('data', (data) => { stdout += data.toString(); }); child.on('close', async (code) => { try { expect(code).toBe(0); expect(stdout).toContain('Create Agentic Workflow'); expect(stdout).toContain('Using non-interactive mode'); expect(stdout).toContain('Project created successfully'); // Verify project was created const projectPath = path.join(testDir, projectName); expect(await fs.pathExists(projectPath)).toBe(true); expect(await fs.pathExists(path.join(projectPath, 'README.md'))).toBe( true ); done(); } catch (error) { done(error); } }); child.on('error', (error) => { done(error); }); }, 15000); // Reduced from 30s for faster CI execution }); describe('Error Handling', () => { test('should handle invalid project names', async () => { const invalidNames = ['', '..', '../invalid', 'invalid/name']; for (const invalidName of invalidNames) { await expect( createProject(invalidName, { nonInteractive: true, githubOrg: 'test-org', }) ).rejects.toThrow(); } }); test('should handle invalid template names', async () => { await expect( createProject('test-project', { nonInteractive: true, githubOrg: 'test-org', template: 'invalid-template', }) ).rejects.toThrow(); }); test('should handle directory conflicts without force flag', async () => { const projectName = 'test-conflict-project'; const projectPath = path.resolve(process.cwd(), projectName); // Create directory first await fs.ensureDir(projectPath); await fs.writeFile( path.join(projectPath, 'existing-file.txt'), 'existing content' ); // Try to create project without force flag await expect( createProject(projectName, { nonInteractive: true, githubOrg: 'test-org', }) ).rejects.toThrow(); // Clean up await fs.remove(projectPath); }); }); describe('Configuration Validation', () => { test('should validate GitHub organization format', async () => { const invalidOrgs = ['', ' ', 'org with spaces', 'org/with/slashes']; for (const invalidOrg of invalidOrgs) { if (invalidOrg.trim() === '') { await expect( createProject('test-project', { nonInteractive: true, githubOrg: invalidOrg, }) ).rejects.toThrow(); } } }); }); describe('Error Boundary Testing', () => { test('should handle filesystem permission errors during project creation', async () => { // Test with invalid path characters that would cause filesystem errors const invalidPaths = [ '\0invalid', // null character ]; for (const invalidPath of invalidPaths) { if (invalidPath.includes('\0')) { // Test null character handling await expect( createProject(invalidPath, { nonInteractive: true, githubOrg: 'test-org', }) ).rejects.toThrow(); } } }); test('should handle special characters in project description', async () => { // Test configuration with special Unicode characters const projectName = 'test-special-chars-config'; const projectPath = path.join(testDir, projectName); await createProject(projectName, { force: true, nonInteractive: true, githubOrg: 'test-org', description: 'Test with special characters: \uFFFD\uFFFE', // Invalid Unicode }); // Move project to test directory if (await fs.pathExists(projectName)) { await fs.move(projectName, projectPath); } // Verify the project was created despite special characters expect(await fs.pathExists(projectPath)).toBe(true); const config = await fs.readJson( path.join(projectPath, '.github', 'repo-config.json') ); // Verify special characters were handled (sanitized or preserved) expect(typeof config.description).toBe('string'); expect(config.description.length).toBeGreaterThan(0); }); test('should handle read-only directory creation failure', async () => { // Create a test scenario where directory creation might fail const projectName = 'test-readonly-failure'; // Test attempting to create project in a path that doesn't exist // and cannot be created (simulating permission errors) const invalidBasePath = '/root/restricted-dir'; // Likely to fail on most systems const originalCwd = process.cwd(); try { // Change to a directory where we likely lack write permissions process.chdir('/'); await expect( createProject(`${invalidBasePath}/${projectName}`, { nonInteractive: true, githubOrg: 'test-org', }) ).rejects.toThrow(); } finally { // Restore original working directory process.chdir(originalCwd); } }); }); });