Complete implementation with idempotent apply, comprehensive tests, and final polish

Co-authored-by: AstroSteveo <34114851+AstroSteveo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-09-21 20:41:15 +00:00
parent 6bdda3841a
commit 099ba3bbe7
3 changed files with 232 additions and 30 deletions

View File

@ -165,8 +165,21 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") {
ensureDirectoryExists(path.join(outputDir, "instructions"));
ensureDirectoryExists(path.join(outputDir, "chatmodes"));
// Check if this is a subsequent run by looking for existing state
const stateFilePath = path.join(outputDir, ".awesome-copilot-state.json");
let previousState = {};
if (fs.existsSync(stateFilePath)) {
try {
previousState = JSON.parse(fs.readFileSync(stateFilePath, 'utf8'));
} catch (error) {
// If state file is corrupted, treat as first run
previousState = {};
}
}
let copiedCount = 0;
let removedCount = 0;
let skippedCount = 0;
const summary = {
prompts: 0,
instructions: 0,
@ -204,9 +217,13 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") {
const destPath = path.join(outputDir, destDir, `${itemName}${fileExtension}`);
if (enabled && fs.existsSync(sourcePath)) {
copyFile(sourcePath, destPath);
copiedCount++;
summary[sectionName]++;
const copyResult = copyFileWithTracking(sourcePath, destPath);
if (copyResult.copied) {
copiedCount++;
summary[sectionName]++;
} else if (copyResult.skipped) {
skippedCount++;
}
enabledInSection.add(itemName);
} else if (!enabled && fs.existsSync(destPath)) {
// Remove file if it's disabled
@ -235,6 +252,29 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") {
}
}
// Helper function to copy files with tracking
function copyFileWithTracking(sourcePath, destPath) {
// Check if file already exists and is identical
if (fs.existsSync(destPath)) {
try {
const sourceContent = fs.readFileSync(sourcePath);
const destContent = fs.readFileSync(destPath);
if (sourceContent.equals(destContent)) {
// Files are identical, no need to copy
console.log(`⚡ Skipped (up to date): ${path.basename(sourcePath)}`);
return { copied: false, skipped: true };
}
} catch (error) {
// If we can't read files for comparison, just proceed with copy
}
}
fs.copyFileSync(sourcePath, destPath);
console.log(`✓ Copied: ${path.basename(sourcePath)}`);
return { copied: true, skipped: false };
}
// Helper function to check if an item is in an enabled collection
function isItemInEnabledCollection(filename, enabledItems) {
for (const itemPath of enabledItems) {
@ -277,9 +317,13 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") {
// Only copy if not explicitly disabled in individual settings
const isExplicitlyDisabled = config[section] && config[section][itemName] === false;
if (destPath && !fs.existsSync(destPath) && !isExplicitlyDisabled) {
copyFile(sourcePath, destPath);
copiedCount++;
if (destPath && !isExplicitlyDisabled) {
const copyResult = copyFileWithTracking(sourcePath, destPath);
if (copyResult.copied) {
copiedCount++;
} else if (copyResult.skipped) {
skippedCount++;
}
}
}
}
@ -290,6 +334,9 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") {
console.log("=".repeat(50));
console.log(`📂 Output directory: ${outputDir}`);
console.log(`📝 Total files copied: ${copiedCount}`);
if (skippedCount > 0) {
console.log(`⚡ Files skipped (up to date): ${skippedCount}`);
}
if (removedCount > 0) {
console.log(`🗑️ Total files removed: ${removedCount}`);
}
@ -301,6 +348,20 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") {
if (config.project?.name) {
console.log(`🏷️ Project: ${config.project.name}`);
}
// Save current state for future idempotency checks
const currentState = {
lastApplied: new Date().toISOString(),
configHash: Buffer.from(JSON.stringify(config)).toString('base64'),
outputDir: outputDir
};
try {
fs.writeFileSync(stateFilePath, JSON.stringify(currentState, null, 2));
} catch (error) {
// State saving failure is not critical
console.log("⚠️ Warning: Could not save state file for future optimization");
}
console.log("\nNext steps:");
console.log("1. Add the files to your version control system");
@ -319,14 +380,6 @@ function ensureDirectoryExists(dirPath) {
}
}
/**
* Copy file from source to destination
*/
function copyFile(sourcePath, destPath) {
fs.copyFileSync(sourcePath, destPath);
console.log(`✓ Copied: ${path.basename(sourcePath)}`);
}
// CLI usage
if (require.main === module) {
const configPath = process.argv[2] || "awesome-copilot.config.yml";

165
test-functionality.js Executable file
View File

@ -0,0 +1,165 @@
#!/usr/bin/env node
/**
* Simple test script to validate enhanced awesome-copilot functionality
* This script tests the key features implemented in the enhancement
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const TEST_CONFIG = 'test-functionality.yml';
const TEST_OUTPUT_DIR = '.test-output';
console.log('🧪 Testing enhanced awesome-copilot functionality...\n');
// Cleanup function
function cleanup() {
if (fs.existsSync(TEST_CONFIG)) {
fs.unlinkSync(TEST_CONFIG);
}
if (fs.existsSync(TEST_OUTPUT_DIR)) {
execSync(`rm -rf ${TEST_OUTPUT_DIR}`);
}
if (fs.existsSync('.awesome-copilot')) {
execSync(`rm -rf .awesome-copilot`);
}
}
function runCommand(cmd, description) {
console.log(`📋 ${description}`);
try {
const output = execSync(cmd, { encoding: 'utf8', cwd: __dirname });
console.log(`✅ Success: ${description}`);
return output;
} catch (error) {
console.log(`❌ Failed: ${description}`);
console.log(` Error: ${error.message}`);
throw error;
}
}
function checkFileExists(filePath, shouldExist = true) {
const exists = fs.existsSync(filePath);
if (shouldExist && exists) {
console.log(`✅ File exists: ${filePath}`);
return true;
} else if (!shouldExist && !exists) {
console.log(`✅ File correctly removed: ${filePath}`);
return true;
} else {
console.log(`❌ File ${exists ? 'exists' : 'missing'}: ${filePath} (expected ${shouldExist ? 'to exist' : 'to be removed'})`);
return false;
}
}
try {
// Test 1: Initialize a test configuration
console.log('\n🔧 Test 1: Initialize test configuration');
runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`, 'Initialize test config');
// Test 2: Validate configuration file was created
console.log('\n🔧 Test 2: Validate configuration file');
if (!checkFileExists(TEST_CONFIG)) {
throw new Error('Configuration file was not created');
}
// Test 3: Test enhanced list command (should show collections first)
console.log('\n🔧 Test 3: Test enhanced list command');
const listOutput = runCommand(`node awesome-copilot.js list --config ${TEST_CONFIG}`, 'List all items with enhanced display');
if (!listOutput.includes('Collections (')) {
throw new Error('Enhanced list should show Collections section first');
}
if (!listOutput.includes('📦 indicates items that are part of collections')) {
throw new Error('Enhanced list should show collection indicators help text');
}
// Test 4: Test collection toggle with cascading
console.log('\n🔧 Test 4: Test collection toggle with cascading');
const toggleOutput = runCommand(`node awesome-copilot.js toggle collections project-planning on --config ${TEST_CONFIG}`, 'Enable collection with cascading');
if (!toggleOutput.includes('individual items from collection')) {
throw new Error('Collection toggle should report cascading to individual items');
}
if (!toggleOutput.includes('Applying configuration automatically')) {
throw new Error('Collection toggle should auto-apply configuration');
}
// Test 5: Test individual item override
console.log('\n🔧 Test 5: Test individual item override');
runCommand(`node awesome-copilot.js toggle prompts breakdown-epic-arch off --config ${TEST_CONFIG}`, 'Disable individual item in collection');
// Check that the file was removed
const promptPath = path.join('.awesome-copilot', 'prompts', 'breakdown-epic-arch.prompt.md');
if (!checkFileExists(promptPath, false)) {
throw new Error('Individual item override did not remove file');
}
// Test 6: Test idempotency (running apply twice should skip files)
console.log('\n🔧 Test 6: Test idempotency');
const applyOutput1 = runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`, 'First apply run');
const applyOutput2 = runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`, 'Second apply run (should be idempotent)');
if (!applyOutput2.includes('Skipped (up to date)')) {
throw new Error('Second apply should skip files that are up to date');
}
// Test 7: Test config validation with invalid config
console.log('\n🔧 Test 7: Test config validation');
const invalidConfig = `
version: 1.0
prompts:
test-prompt: invalid_boolean_value
unknown_section:
test: true
`;
fs.writeFileSync('test-invalid-config.yml', invalidConfig);
try {
execSync(`node awesome-copilot.js apply test-invalid-config.yml`, { encoding: 'utf8' });
throw new Error('Invalid config should have been rejected');
} catch (error) {
if (error.stderr && error.stderr.includes('Configuration validation errors')) {
console.log('✅ Config validation correctly rejected invalid configuration');
} else if (error.message && error.message.includes('Configuration validation failed')) {
console.log('✅ Config validation correctly rejected invalid configuration');
} else {
throw new Error('Config validation did not provide proper error message');
}
} finally {
if (fs.existsSync('test-invalid-config.yml')) {
fs.unlinkSync('test-invalid-config.yml');
}
}
// Test 8: Check that state file is created for idempotency
console.log('\n🔧 Test 8: Check state file creation');
const stateFilePath = path.join('.awesome-copilot', '.awesome-copilot-state.json');
if (!checkFileExists(stateFilePath)) {
throw new Error('State file should be created for idempotency tracking');
}
console.log('\n🎉 All tests passed! Enhanced functionality is working correctly.');
console.log('\n✨ Features validated:');
console.log(' • Collection toggle with item cascading');
console.log(' • Enhanced list display with collection indicators');
console.log(' • Auto-apply after toggle operations');
console.log(' • File removal when items are disabled');
console.log(' • Individual item overrides');
console.log(' • Idempotent apply operations');
console.log(' • Configuration validation with error reporting');
console.log(' • State tracking for optimization');
} catch (error) {
console.log(`\n💥 Test failed: ${error.message}`);
process.exit(1);
} finally {
// Cleanup
cleanup();
console.log('\n🧹 Cleanup completed.');
}

View File

@ -1,16 +0,0 @@
# Invalid config for testing
version: 1.0
project:
name: "Test Project"
output_directory: ".awesome-copilot"
prompts:
breakdown-epic-arch: invalid_value # This should be boolean
breakdown-epic-pm: true
invalid_section:
some_key: true
instructions:
task-implementation: "yes" # This should be boolean