@stillrivercode/agentic-workflow-template
Version:
NPM package to create AI-powered GitHub workflow automation projects
411 lines (348 loc) • 12.3 kB
JavaScript
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);
}
});
});
});