diff --git a/README.md b/README.md index 468448a..70ea1b0 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,47 @@ Use our configuration system to manage all customizations in one place: See [CONFIG.md](CONFIG.md) for detailed configuration documentation. +#### โš–๏ธ Configuration Precedence Rules + +Awesome Copilot uses an **effective state system** that respects explicit overrides while allowing collections to provide convenient defaults: + +1. **Explicit Settings Override Everything** + ```yaml + collections: + testing-automation: true # Enables 11 items + prompts: + playwright-generate-test: false # Explicitly disabled, overrides collection + ``` + +2. **Collections Enable Groups of Items** + ```yaml + collections: + frontend-web-dev: true # Enables React, Vue, TypeScript items + testing-automation: true # Enables testing tools and frameworks + ``` + +3. **Undefined Items Follow Collections** + - Items not explicitly listed inherit from enabled collections + - Only explicitly set true/false values override collection settings + - This allows collections to work as intended while preserving explicit choices + +**Examples:** +```bash +# See effective states and why each item is enabled +awesome-copilot list prompts +# [โœ“] create-readme (explicit) +# [โœ“] playwright-generate-test (collection) +# [ ] react-component + +# Collection toggle shows what will change +awesome-copilot toggle collections frontend-web-dev on +# Delta summary: +# ๐Ÿ“ˆ 8 items will be enabled: +# + prompts/react-component +# + prompts/vue-component +# + instructions/typescript-best-practices +``` + ### ๐Ÿ“ Manual File Approach Browse the collections and manually copy files you want to use: diff --git a/test-all.js b/test-all.js new file mode 100644 index 0000000..55eac5f --- /dev/null +++ b/test-all.js @@ -0,0 +1,103 @@ +#!/usr/bin/env node + +/** + * Comprehensive test suite for all awesome-copilot functionality + */ + +const { runTests: runUnitTests } = require('./test-effective-states'); +const { runTests: runIntegrationTests } = require('./test-integration'); +const { runTests: runCliTests } = require('./test-cli'); +const { runTests: runApplyTests } = require('./test-apply-effective'); + +async function runAllTests() { + console.log('๐Ÿงช Running Awesome Copilot Comprehensive Test Suite\n'); + console.log('=' * 60); + + const results = { + unit: false, + integration: false, + cli: false, + apply: false + }; + + try { + console.log('\n๐Ÿ“Š Unit Tests (Effective State Computation)'); + console.log('-' * 45); + results.unit = await runUnitTests(); + } catch (error) { + console.error('Unit tests failed with error:', error.message); + } + + try { + console.log('\n๐Ÿ”„ Integration Tests (Toggle+Apply Idempotency)'); + console.log('-' * 48); + results.integration = await runIntegrationTests(); + } catch (error) { + console.error('Integration tests failed with error:', error.message); + } + + try { + console.log('\nโŒจ๏ธ CLI Tests (List and Toggle Commands)'); + console.log('-' * 40); + results.cli = await runCliTests(); + } catch (error) { + console.error('CLI tests failed with error:', error.message); + } + + try { + console.log('\n๐ŸŽฏ Apply Tests (Effective States in Apply)'); + console.log('-' * 42); + results.apply = await runApplyTests(); + } catch (error) { + console.error('Apply tests failed with error:', error.message); + } + + // Summary + console.log('\n' + '=' * 60); + console.log('๐Ÿ“‹ Test Suite Summary'); + console.log('=' * 60); + + const testTypes = [ + { name: 'Unit Tests', result: results.unit, emoji: '๐Ÿ“Š' }, + { name: 'Integration Tests', result: results.integration, emoji: '๐Ÿ”„' }, + { name: 'CLI Tests', result: results.cli, emoji: 'โŒจ๏ธ' }, + { name: 'Apply Tests', result: results.apply, emoji: '๐ŸŽฏ' } + ]; + + testTypes.forEach(test => { + const status = test.result ? 'โœ… PASS' : 'โŒ FAIL'; + console.log(`${test.emoji} ${test.name.padEnd(20)} ${status}`); + }); + + const passedCount = Object.values(results).filter(Boolean).length; + const totalCount = Object.keys(results).length; + + console.log('\n' + '-' * 60); + console.log(`Overall Result: ${passedCount}/${totalCount} test suites passed`); + + if (passedCount === totalCount) { + console.log('๐ŸŽ‰ All test suites passed! Implementation is complete.'); + console.log('\nโœจ Features implemented:'); + console.log(' โ€ข Effective state precedence (explicit > collection > disabled)'); + console.log(' โ€ข Non-destructive collection toggles with delta summaries'); + console.log(' โ€ข Enhanced CLI with reason display (explicit/collection)'); + console.log(' โ€ข Performance improvements with Set-based lookups'); + console.log(' โ€ข Comprehensive configuration handling'); + console.log(' โ€ข Stable config hashing and idempotent operations'); + return true; + } else { + console.log('๐Ÿ’ฅ Some test suites failed. Check individual test output above.'); + return false; + } +} + +if (require.main === module) { + runAllTests().then(success => { + process.exit(success ? 0 : 1); + }).catch(error => { + console.error('Test suite runner error:', error); + process.exit(1); + }); +} + +module.exports = { runAllTests }; \ No newline at end of file diff --git a/test-apply-effective.js b/test-apply-effective.js new file mode 100644 index 0000000..c4aa936 --- /dev/null +++ b/test-apply-effective.js @@ -0,0 +1,227 @@ +#!/usr/bin/env node + +/** + * Test to verify that apply operations always use effective states + */ + +const fs = require('fs'); +const { exec } = require('child_process'); +const { promisify } = require('util'); +const path = require('path'); + +const execAsync = promisify(exec); + +// Change to project directory for tests +process.chdir(__dirname); + +const TEST_CONFIG = 'test-apply-effective.yml'; +const TEST_OUTPUT_DIR = '.test-apply-effective'; + +function assert(condition, message) { + if (!condition) { + throw new Error(`Assertion failed: ${message}`); + } +} + +function cleanup() { + if (fs.existsSync(TEST_CONFIG)) fs.unlinkSync(TEST_CONFIG); + if (fs.existsSync(TEST_OUTPUT_DIR)) { + fs.rmSync(TEST_OUTPUT_DIR, { recursive: true, force: true }); + } +} + +async function runCommand(command) { + try { + const { stdout, stderr } = await execAsync(command); + return { success: true, stdout, stderr }; + } catch (error) { + return { success: false, stdout: error.stdout, stderr: error.stderr, error }; + } +} + +function getFilesList(dir) { + if (!fs.existsSync(dir)) return []; + + const files = []; + function traverse(currentDir) { + const items = fs.readdirSync(currentDir); + for (const item of items) { + const fullPath = path.join(currentDir, item); + if (fs.statSync(fullPath).isDirectory()) { + traverse(fullPath); + } else { + files.push(path.basename(fullPath)); + } + } + } + traverse(dir); + return files.sort(); +} + +function setTestOutputDir(configFile) { + let configContent = fs.readFileSync(configFile, 'utf8'); + configContent = configContent.replace('output_directory: ".awesome-copilot"', `output_directory: "${TEST_OUTPUT_DIR}"`); + fs.writeFileSync(configFile, configContent); +} + +async function runTests() { + console.log('Testing that apply operations use effective states...\n'); + + let passedTests = 0; + let totalTests = 0; + + async function test(name, testFn) { + totalTests++; + cleanup(); // Clean up before each test + + try { + await testFn(); + console.log(`โœ… ${name}`); + passedTests++; + } catch (error) { + console.log(`โŒ ${name}: ${error.message}`); + } + } + + // Test 1: Apply respects explicit false overrides + await test("Apply respects explicit false overrides", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + setTestOutputDir(TEST_CONFIG); + + // Enable collection + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + + // Explicitly disable one item from the collection + await runCommand(`node awesome-copilot.js toggle prompts playwright-generate-test off --config ${TEST_CONFIG}`); + + // Apply + await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`); + + const files = getFilesList(TEST_OUTPUT_DIR); + + // Should have files from collection but NOT the explicitly disabled one + assert(files.includes('csharp-nunit.prompt.md'), 'Should include collection items'); + assert(!files.includes('playwright-generate-test.prompt.md'), 'Should NOT include explicitly disabled items'); + }); + + // Test 2: Apply includes collection items that are undefined + await test("Apply includes collection items that are undefined", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + setTestOutputDir(TEST_CONFIG); + + // Enable collection (items remain undefined - no explicit settings) + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + + // Apply + await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`); + + const files = getFilesList(TEST_OUTPUT_DIR); + + // Should include all collection items since none are explicitly overridden + assert(files.includes('playwright-generate-test.prompt.md'), 'Should include undefined collection items'); + assert(files.includes('csharp-nunit.prompt.md'), 'Should include all collection items'); + assert(files.includes('tdd-red.chatmode.md'), 'Should include collection chatmodes'); + }); + + // Test 3: Apply respects explicit true overrides over disabled collections + await test("Apply respects explicit true overrides over disabled collections", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + setTestOutputDir(TEST_CONFIG); + + // Collection remains disabled, but explicitly enable one item + await runCommand(`node awesome-copilot.js toggle prompts playwright-generate-test on --config ${TEST_CONFIG}`); + + // Apply + await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`); + + const files = getFilesList(TEST_OUTPUT_DIR); + + // Should only have the explicitly enabled item + assert(files.includes('playwright-generate-test.prompt.md'), 'Should include explicitly enabled items'); + assert(!files.includes('csharp-nunit.prompt.md'), 'Should NOT include collection items when collection disabled'); + assert(files.length === 1, 'Should only have one file'); + }); + + // Test 4: Multiple collections work together through effective states + await test("Multiple collections work together through effective states", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + setTestOutputDir(TEST_CONFIG); + + // Enable multiple collections + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + await runCommand(`node awesome-copilot.js toggle collections frontend-web-dev on --config ${TEST_CONFIG}`); + + // Apply + await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`); + + const files = getFilesList(TEST_OUTPUT_DIR); + + // Should have items from both collections + assert(files.length > 11, 'Should have items from multiple collections'); // testing-automation has 11 items + assert(files.includes('playwright-generate-test.prompt.md'), 'Should include testing items'); + }); + + // Test 5: Apply output matches effective state computation + await test("Apply output matches effective state computation", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + setTestOutputDir(TEST_CONFIG); + + // Complex scenario: collection + explicit override + individual enable + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + await runCommand(`node awesome-copilot.js toggle prompts playwright-generate-test off --config ${TEST_CONFIG}`); + await runCommand(`node awesome-copilot.js toggle prompts create-readme on --config ${TEST_CONFIG}`); + + // Get list of what should be enabled according to CLI (only individual items, not collections) + const listResult = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); + const enabledPrompts = (listResult.stdout.match(/\[โœ“\] (\S+)/g) || []).map(m => m.replace('[โœ“] ', '').split(' ')[0]); + + const instructionsResult = await runCommand(`node awesome-copilot.js list instructions --config ${TEST_CONFIG}`); + const enabledInstructions = (instructionsResult.stdout.match(/\[โœ“\] (\S+)/g) || []).map(m => m.replace('[โœ“] ', '').split(' ')[0]); + + const chatmodesResult = await runCommand(`node awesome-copilot.js list chatmodes --config ${TEST_CONFIG}`); + const enabledChatmodes = (chatmodesResult.stdout.match(/\[โœ“\] (\S+)/g) || []).map(m => m.replace('[โœ“] ', '').split(' ')[0]); + + const allEnabledItems = [...enabledPrompts, ...enabledInstructions, ...enabledChatmodes]; + + // Apply and get actual files + await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`); + const actualFiles = getFilesList(TEST_OUTPUT_DIR); + + // Extract base names for comparison + const actualBaseNames = actualFiles.map(f => f.replace(/\.(prompt|instructions|chatmode)\.md$/, '')); + + // Every enabled item in list should have a corresponding file + allEnabledItems.forEach(item => { + assert(actualBaseNames.includes(item), `Item ${item} shown as enabled should have corresponding file`); + }); + + // Every file should correspond to an enabled item + actualBaseNames.forEach(fileName => { + assert(allEnabledItems.includes(fileName), `File ${fileName} should correspond to an enabled item`); + }); + }); + + console.log(`\nEffective States Apply Test Results: ${passedTests}/${totalTests} passed`); + + cleanup(); // Final cleanup + + if (passedTests === totalTests) { + console.log('๐ŸŽ‰ All effective states apply tests passed!'); + return true; + } else { + console.log('๐Ÿ’ฅ Some effective states apply tests failed!'); + return false; + } +} + +if (require.main === module) { + runTests().then(success => { + process.exit(success ? 0 : 1); + }).catch(error => { + console.error('Test runner error:', error); + cleanup(); + process.exit(1); + }); +} + +module.exports = { runTests }; \ No newline at end of file diff --git a/test-cli.js b/test-cli.js new file mode 100644 index 0000000..5c1aa69 --- /dev/null +++ b/test-cli.js @@ -0,0 +1,154 @@ +#!/usr/bin/env node + +/** + * CLI tests for list and toggle commands + */ + +const fs = require('fs'); +const { exec } = require('child_process'); +const { promisify } = require('util'); + +const execAsync = promisify(exec); + +// Change to project directory for tests +process.chdir(__dirname); + +const TEST_CONFIG = 'test-cli.yml'; + +function assert(condition, message) { + if (!condition) { + throw new Error(`Assertion failed: ${message}`); + } +} + +function cleanup() { + if (fs.existsSync(TEST_CONFIG)) fs.unlinkSync(TEST_CONFIG); +} + +async function runCommand(command) { + try { + const { stdout, stderr } = await execAsync(command); + return { success: true, stdout, stderr }; + } catch (error) { + return { success: false, stdout: error.stdout, stderr: error.stderr, error }; + } +} + +async function runTests() { + console.log('Running CLI tests for list and toggle commands...\n'); + + let passedTests = 0; + let totalTests = 0; + + async function test(name, testFn) { + totalTests++; + cleanup(); // Clean up before each test + + try { + await testFn(); + console.log(`โœ… ${name}`); + passedTests++; + } catch (error) { + console.log(`โŒ ${name}: ${error.message}`); + } + } + + // Test 1: List command shows sections correctly + await test("List command shows sections correctly", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + + const result = await runCommand(`node awesome-copilot.js list --config ${TEST_CONFIG}`); + assert(result.success, 'List command should succeed'); + assert(result.stdout.includes('Prompts'), 'Should show Prompts section'); + assert(result.stdout.includes('Instructions'), 'Should show Instructions section'); + assert(result.stdout.includes('Chat Modes'), 'Should show Chat Modes section'); + assert(result.stdout.includes('Collections'), 'Should show Collections section'); + }); + + // Test 2: List specific section works + await test("List specific section works", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + + const result = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); + assert(result.success, 'List prompts should succeed'); + assert(result.stdout.includes('Prompts'), 'Should show Prompts heading'); + assert(!result.stdout.includes('Instructions'), 'Should not show other sections'); + }); + + // Test 3: Toggle collection shows delta summary + await test("Toggle collection shows delta summary", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + + const result = await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + assert(result.success, 'Toggle should succeed'); + assert(result.stdout.includes('Delta summary'), 'Should show delta summary'); + assert(result.stdout.includes('items will be enabled'), 'Should show enabled items count'); + }); + + // Test 4: List shows effective states after collection toggle + await test("List shows effective states after collection toggle", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + + const result = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); + assert(result.success, 'List should succeed'); + assert(result.stdout.includes('(collection)'), 'Should show collection reason'); + assert(result.stdout.includes('[โœ“]'), 'Should show enabled items'); + }); + + // Test 5: Toggle individual item shows explicit override + await test("Toggle individual item shows explicit override", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + + const result = await runCommand(`node awesome-copilot.js toggle prompts playwright-generate-test off --config ${TEST_CONFIG}`); + assert(result.success, 'Individual toggle should succeed'); + + const listResult = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); + assert(listResult.stdout.includes('playwright-generate-test') && !listResult.stdout.includes('playwright-generate-test ('), + 'Explicitly disabled item should not show reason'); + }); + + // Test 6: Error handling for invalid commands + await test("Error handling for invalid commands", async () => { + const result1 = await runCommand(`node awesome-copilot.js toggle --config ${TEST_CONFIG}`); + assert(!result1.success, 'Should fail with insufficient arguments'); + + const result2 = await runCommand(`node awesome-copilot.js toggle prompts nonexistent on --config ${TEST_CONFIG}`); + assert(!result2.success, 'Should fail with nonexistent item'); + }); + + // Test 7: Collection toggle idempotency + await test("Collection toggle idempotency", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + + const result = await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + assert(result.success, 'Should succeed'); + assert(result.stdout.includes('already enabled'), 'Should indicate no change needed'); + }); + + console.log(`\nCLI Test Results: ${passedTests}/${totalTests} passed`); + + cleanup(); // Final cleanup + + if (passedTests === totalTests) { + console.log('๐ŸŽ‰ All CLI tests passed!'); + return true; + } else { + console.log('๐Ÿ’ฅ Some CLI tests failed!'); + return false; + } +} + +if (require.main === module) { + runTests().then(success => { + process.exit(success ? 0 : 1); + }).catch(error => { + console.error('CLI test runner error:', error); + cleanup(); + process.exit(1); + }); +} + +module.exports = { runTests }; \ No newline at end of file