From 661554741a434751055660d6fa9d5d738de2862e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Sep 2025 22:49:18 +0000 Subject: [PATCH] Complete core functionality: tests, config generation fixes, and hash stability Co-authored-by: AstroSteveo <34114851+AstroSteveo@users.noreply.github.com> --- config-manager.js | 26 ++++- generate-config.js | 33 +++--- test-effective-states.js | 183 ++++++++++++++++++++++++++++++++ test-integration.js | 219 +++++++++++++++++++++++++++++++++++++++ test-new-config.yml | 35 +++++++ 5 files changed, 477 insertions(+), 19 deletions(-) create mode 100644 test-effective-states.js create mode 100644 test-integration.js create mode 100644 test-new-config.yml diff --git a/config-manager.js b/config-manager.js index 0b535a3..5ffe602 100644 --- a/config-manager.js +++ b/config-manager.js @@ -158,6 +158,29 @@ function getAllAvailableItems(type) { return getAvailableItems(path.join(__dirname, meta.dir), meta.ext); } +/** + * Generate a stable hash of configuration for comparison + * @param {Object} config - Configuration object + * @returns {string} Stable hash string + */ +function generateConfigHash(config) { + const crypto = require('crypto'); + + // Create a stable representation by sorting all keys recursively + function stableStringify(obj) { + if (obj === null || obj === undefined) return 'null'; + if (typeof obj !== 'object') return JSON.stringify(obj); + if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']'; + + const keys = Object.keys(obj).sort(); + const pairs = keys.map(key => `"${key}":${stableStringify(obj[key])}`); + return '{' + pairs.join(',') + '}'; + } + + const stableJson = stableStringify(config); + return crypto.createHash('sha256').update(stableJson).digest('hex').substring(0, 16); +} + /** * Compute effective item states respecting explicit overrides over collections * @param {Object} config - Configuration object with sections @@ -253,5 +276,6 @@ module.exports = { sortObjectKeys, countEnabledItems, getAllAvailableItems, - computeEffectiveItemStates + computeEffectiveItemStates, + generateConfigHash }; diff --git a/generate-config.js b/generate-config.js index 7ebfbaa..eef66d3 100755 --- a/generate-config.js +++ b/generate-config.js @@ -19,7 +19,7 @@ function generateConfig(outputPath = "awesome-copilot.config.yml") { const config = { version: "1.0", project: { - name: "My Project", + name: "My Project", description: "A project using awesome-copilot customizations", output_directory: ".awesome-copilot" }, @@ -29,23 +29,16 @@ function generateConfig(outputPath = "awesome-copilot.config.yml") { collections: {} }; - // Populate with all items disabled by default (user can enable what they want) - prompts.forEach(item => { - config.prompts[item] = false; - }); - - instructions.forEach(item => { - config.instructions[item] = false; - }); - - chatmodes.forEach(item => { - config.chatmodes[item] = false; - }); - + // Only populate collections with defaults (set to false) + // Individual items are left undefined to allow collection precedence collections.forEach(item => { config.collections[item] = false; }); + // Note: prompts, instructions, and chatmodes are left empty + // Users can explicitly enable items they want, or enable collections + // to get groups of items. Undefined items respect collection settings. + const yamlContent = objectToYaml(config); const fullContent = generateConfigHeader() + yamlContent; @@ -95,11 +88,15 @@ function generateConfigHeader(date = new Date()) { return `# Awesome Copilot Configuration File # Generated on ${date.toISOString()} # -# This file allows you to enable/disable specific prompts, instructions, -# chat modes, and collections for your project. +# This file uses effective state precedence: +# 1. Explicit item settings (true/false) override everything +# 2. Items not listed inherit from enabled collections +# 3. Otherwise items are disabled # -# Set items to 'true' to include them in your project -# Set items to 'false' to exclude them +# To use: +# - Enable collections for curated sets of related items +# - Explicitly set individual items to true/false to override collections +# - Items not mentioned will follow collection settings # # After configuring, run: awesome-copilot apply # diff --git a/test-effective-states.js b/test-effective-states.js new file mode 100644 index 0000000..5139061 --- /dev/null +++ b/test-effective-states.js @@ -0,0 +1,183 @@ +#!/usr/bin/env node + +/** + * Unit tests for computeEffectiveItemStates function + */ + +const path = require('path'); +const { computeEffectiveItemStates } = require('./config-manager'); + +// Change to project directory for tests +process.chdir(__dirname); + +function assert(condition, message) { + if (!condition) { + throw new Error(`Assertion failed: ${message}`); + } +} + +function runTests() { + console.log('Running unit tests for computeEffectiveItemStates...\n'); + + let passedTests = 0; + let totalTests = 0; + + function test(name, testFn) { + totalTests++; + try { + testFn(); + console.log(`✅ ${name}`); + passedTests++; + } catch (error) { + console.log(`❌ ${name}: ${error.message}`); + } + } + + // Test 1: Empty config returns all disabled + test("Empty config returns all disabled", () => { + const config = {}; + const result = computeEffectiveItemStates(config); + + assert(typeof result === 'object', 'Should return object'); + assert('prompts' in result, 'Should have prompts section'); + assert('instructions' in result, 'Should have instructions section'); + assert('chatmodes' in result, 'Should have chatmodes section'); + + // Check a few known items are disabled + const playwrightPrompt = result.prompts['playwright-generate-test']; + assert(playwrightPrompt && !playwrightPrompt.enabled && playwrightPrompt.reason === 'disabled', + 'Playwright prompt should be disabled'); + }); + + // Test 2: Explicit true overrides everything + test("Explicit true overrides everything", () => { + const config = { + prompts: { + 'playwright-generate-test': true + }, + collections: {} + }; + const result = computeEffectiveItemStates(config); + + const item = result.prompts['playwright-generate-test']; + assert(item && item.enabled && item.reason === 'explicit', + 'Explicitly enabled item should show as explicit'); + }); + + // Test 3: Explicit false overrides collections + test("Explicit false overrides collections", () => { + const config = { + prompts: { + 'playwright-generate-test': false + }, + collections: { + 'testing-automation': true + } + }; + const result = computeEffectiveItemStates(config); + + const item = result.prompts['playwright-generate-test']; + assert(item && !item.enabled && item.reason === 'explicit', + 'Explicitly disabled item should override collection'); + }); + + // Test 4: Collection enables items + test("Collection enables items", () => { + const config = { + collections: { + 'testing-automation': true + } + }; + const result = computeEffectiveItemStates(config); + + // These should be enabled by collection + const items = [ + 'playwright-generate-test', + 'csharp-nunit', + 'playwright-explore-website' + ]; + + items.forEach(itemName => { + const item = result.prompts[itemName]; + assert(item && item.enabled && item.reason === 'collection', + `${itemName} should be enabled by collection`); + }); + }); + + // Test 5: Undefined items respect collections + test("Undefined items respect collections", () => { + const config = { + prompts: { + 'some-other-prompt': true // explicit + // playwright-generate-test is undefined + }, + collections: { + 'testing-automation': true + } + }; + const result = computeEffectiveItemStates(config); + + const explicitItem = result.prompts['some-other-prompt']; + if (explicitItem) { + // If the item exists, it should be explicit (might not exist if not a real prompt) + // This test is just to ensure undefined vs explicit behavior + } + + const collectionItem = result.prompts['playwright-generate-test']; + assert(collectionItem && collectionItem.enabled && collectionItem.reason === 'collection', + 'Undefined item should respect collection setting'); + }); + + // Test 6: Multiple collections + test("Multiple collections work together", () => { + const config = { + collections: { + 'testing-automation': true, + 'frontend-web-dev': true // if this exists + } + }; + const result = computeEffectiveItemStates(config); + + // Items from testing-automation should be enabled + const testingItem = result.prompts['playwright-generate-test']; + assert(testingItem && testingItem.enabled && testingItem.reason === 'collection', + 'Testing automation items should be enabled'); + }); + + // Test 7: Instructions and chatmodes work correctly + test("Instructions and chatmodes work correctly", () => { + const config = { + collections: { + 'testing-automation': true + } + }; + const result = computeEffectiveItemStates(config); + + // Check instructions + const instruction = result.instructions['playwright-typescript']; + assert(instruction && instruction.enabled && instruction.reason === 'collection', + 'Instruction should be enabled by collection'); + + // Check chatmodes + const chatmode = result.chatmodes['tdd-red']; + assert(chatmode && chatmode.enabled && chatmode.reason === 'collection', + 'Chat mode should be enabled by collection'); + }); + + console.log(`\nTest Results: ${passedTests}/${totalTests} passed`); + + if (passedTests === totalTests) { + console.log('🎉 All tests passed!'); + return true; + } else { + console.log('💥 Some tests failed!'); + return false; + } +} + +if (require.main === module) { + const success = runTests(); + process.exit(success ? 0 : 1); +} + +module.exports = { runTests }; \ No newline at end of file diff --git a/test-integration.js b/test-integration.js new file mode 100644 index 0000000..9e5aad1 --- /dev/null +++ b/test-integration.js @@ -0,0 +1,219 @@ +#!/usr/bin/env node + +/** + * Integration tests for toggle+apply idempotency + */ + +const fs = require('fs'); +const path = require('path'); +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-integration.yml'; +const TEST_OUTPUT_DIR = '.test-awesome-copilot'; + +function assert(condition, message) { + if (!condition) { + throw new Error(`Assertion failed: ${message}`); + } +} + +function cleanup() { + // Remove test files + 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.relative(dir, 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('Running integration tests for toggle+apply idempotency...\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: Basic toggle+apply idempotency + await test("Basic toggle+apply idempotency", async () => { + // Create initial config + const result1 = await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + assert(result1.success, 'Should create initial config'); + setTestOutputDir(TEST_CONFIG); + + // Enable a collection + const result2 = await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + assert(result2.success, 'Should enable collection'); + + // First apply + const result3 = await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`); + assert(result3.success, 'First apply should succeed'); + + const files1 = getFilesList(TEST_OUTPUT_DIR); + assert(files1.length > 0, 'Should have copied files'); + + // Second apply (idempotency test) + const result4 = await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`); + assert(result4.success, 'Second apply should succeed'); + + const files2 = getFilesList(TEST_OUTPUT_DIR); + assert(JSON.stringify(files1) === JSON.stringify(files2), 'File lists should be identical'); + }); + + // Test 2: Toggle collection off then on restores same state + await test("Toggle collection off then on restores same state", async () => { + // Create initial config and enable collection + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + setTestOutputDir(TEST_CONFIG); + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + + // First apply + await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`); + const files1 = getFilesList(TEST_OUTPUT_DIR); + + // Toggle collection off + await runCommand(`node awesome-copilot.js toggle collections testing-automation off --config ${TEST_CONFIG}`); + + // Apply (should remove files) + fs.rmSync(TEST_OUTPUT_DIR, { recursive: true, force: true }); + await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`); + const files2 = getFilesList(TEST_OUTPUT_DIR); + assert(files2.length === 0, 'Should have no files when collection disabled'); + + // Toggle collection back on + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + + // Apply again + fs.rmSync(TEST_OUTPUT_DIR, { recursive: true, force: true }); + await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`); + const files3 = getFilesList(TEST_OUTPUT_DIR); + + assert(JSON.stringify(files1) === JSON.stringify(files3), 'File lists should be restored'); + }); + + // Test 3: Explicit overrides are maintained across collection toggles + await test("Explicit overrides are maintained across collection toggles", async () => { + // Create initial config and enable collection + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + setTestOutputDir(TEST_CONFIG); + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + + // Add explicit override + await runCommand(`node awesome-copilot.js toggle prompts playwright-generate-test off --config ${TEST_CONFIG}`); + + // Apply and count files + await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`); + const files1 = getFilesList(TEST_OUTPUT_DIR); + + // Toggle collection off and on + await runCommand(`node awesome-copilot.js toggle collections testing-automation off --config ${TEST_CONFIG}`); + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + + // Apply again + fs.rmSync(TEST_OUTPUT_DIR, { recursive: true, force: true }); + await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`); + const files2 = getFilesList(TEST_OUTPUT_DIR); + + assert(JSON.stringify(files1) === JSON.stringify(files2), 'Explicit overrides should be maintained'); + + // Verify playwright-generate-test is still not present + const hasPlaywrightTest = files2.some(f => f.includes('playwright-generate-test')); + assert(!hasPlaywrightTest, 'Explicitly disabled item should stay disabled'); + }); + + // Test 4: Multiple apply operations are truly idempotent + await test("Multiple apply operations are truly idempotent", async () => { + // Setup + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + setTestOutputDir(TEST_CONFIG); + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + await runCommand(`node awesome-copilot.js toggle prompts create-readme on --config ${TEST_CONFIG}`); + + // Apply multiple times + for (let i = 0; i < 3; i++) { + await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`); + } + + const files1 = getFilesList(TEST_OUTPUT_DIR); + + // Apply once more + await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`); + const files2 = getFilesList(TEST_OUTPUT_DIR); + + assert(JSON.stringify(files1) === JSON.stringify(files2), 'Multiple applies should be idempotent'); + }); + + console.log(`\nIntegration Test Results: ${passedTests}/${totalTests} passed`); + + cleanup(); // Final cleanup + + if (passedTests === totalTests) { + console.log('🎉 All integration tests passed!'); + return true; + } else { + console.log('💥 Some integration 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-new-config.yml b/test-new-config.yml new file mode 100644 index 0000000..d8051d0 --- /dev/null +++ b/test-new-config.yml @@ -0,0 +1,35 @@ +# Awesome Copilot Configuration File +# Generated on 2025-09-21T22:44:51.675Z +# +# This file uses effective state precedence: +# 1. Explicit item settings (true/false) override everything +# 2. Items not listed inherit from enabled collections +# 3. Otherwise items are disabled +# +# To use: +# - Enable collections for curated sets of related items +# - Explicitly set individual items to true/false to override collections +# - Items not mentioned will follow collection settings +# +# After configuring, run: awesome-copilot apply +# + +version: "1.0" +project: + name: "My Project" + description: "A project using awesome-copilot customizations" + output_directory: ".awesome-copilot" +prompts: + playwright-generate-test: false +instructions: +chatmodes: +collections: + azure-cloud-development: false + csharp-dotnet-development: false + database-data-management: false + devops-oncall: false + frontend-web-dev: false + project-planning: false + security-best-practices: false + technical-spike: false + testing-automation: true