diff --git a/.vscode/settings.json b/.vscode/settings.json index 6776bb9..fc19b34 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,37 +1,11 @@ { "chat.modeFilesLocations": { - "chatmodes": true + ".awesome-copilot/chatmodes": true }, "chat.promptFilesLocations": { - "prompts": true + ".awesome-copilot/prompts": true }, "chat.instructionsFilesLocations": { - "instructions": true - }, - "files.eol": "\n", - "files.insertFinalNewline": true, - "files.trimTrailingWhitespace": true, - "[markdown]": { - "files.trimTrailingWhitespace": false, - "editor.formatOnSave": true - }, - "editor.rulers": [ - 120 - ], - "files.associations": { - "*.chatmode.md": "markdown", - "*.instructions.md": "markdown", - "*.prompt.md": "markdown" - }, - "github.copilot.chat.agent.thinkingTool": true, - "github.copilot.chat.alternateGptPrompt.enabled": true, - "github.copilot.chat.editor.temporalContext.enabled": true, - "github.copilot.chat.edits.temporalContext.enabled": true, - "github.copilot.chat.generateTests.codeLens": true, - "github.copilot.chat.languageContext.fix.typescript.enabled": true, - "github.copilot.chat.languageContext.inline.typescript.enabled": true, - "github.copilot.chat.languageContext.typescript.enabled": true, - "github.copilot.chat.newWorkspace.useContext7": true, - "github.copilot.chat.notebook.enhancedNextEditSuggestions.enabled": true, - "github.copilot.chat.notebook.followCellExecution.enabled": true, -} + ".awesome-copilot/instructions": true + } +} \ No newline at end of file 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/apply-config.js b/apply-config.js index 675acfd..fa27c64 100755 --- a/apply-config.js +++ b/apply-config.js @@ -100,8 +100,28 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") { collections: 0 }; - // Process collections first (they can enable individual items) - const enabledItems = new Set(); + // Import config manager for effective state computation + const { computeEffectiveItemStates } = require("./config-manager"); + + // Compute effective states using precedence rules + const effectiveStates = computeEffectiveItemStates(config); + + // Create sets of effectively enabled items for performance + const effectivelyEnabledSets = { + prompts: new Set(), + instructions: new Set(), + chatmodes: new Set() + }; + + for (const section of ["prompts", "instructions", "chatmodes"]) { + for (const [itemName, state] of Object.entries(effectiveStates[section])) { + if (state.enabled) { + effectivelyEnabledSets[section].add(itemName); + } + } + } + + // Count enabled collections for summary if (config.collections) { for (const [collectionName, enabled] of Object.entries(config.collections)) { if (enabled) { @@ -109,9 +129,6 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") { if (fs.existsSync(collectionPath)) { const collection = parseCollectionYaml(collectionPath); if (collection && collection.items) { - collection.items.forEach(item => { - enabledItems.add(item.path); - }); summary.collections++; console.log(`โœ“ Enabled collection: ${collectionName} (${collection.items.length} items)`); } @@ -120,70 +137,36 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") { } } - // Process prompts - if (config.prompts) { - for (const [promptName, enabled] of Object.entries(config.prompts)) { - if (enabled) { - const sourcePath = path.join(rootDir, "prompts", `${promptName}.prompt.md`); - if (fs.existsSync(sourcePath)) { - const destPath = path.join(outputDir, "prompts", `${promptName}.prompt.md`); - copyFile(sourcePath, destPath); - copiedCount++; - summary.prompts++; - } - } - } - } - - // Process instructions - if (config.instructions) { - for (const [instructionName, enabled] of Object.entries(config.instructions)) { - if (enabled) { - const sourcePath = path.join(rootDir, "instructions", `${instructionName}.instructions.md`); - if (fs.existsSync(sourcePath)) { - const destPath = path.join(outputDir, "instructions", `${instructionName}.instructions.md`); - copyFile(sourcePath, destPath); - copiedCount++; - summary.instructions++; - } - } - } - } - - // Process chat modes - if (config.chatmodes) { - for (const [chatmodeName, enabled] of Object.entries(config.chatmodes)) { - if (enabled) { - const sourcePath = path.join(rootDir, "chatmodes", `${chatmodeName}.chatmode.md`); - if (fs.existsSync(sourcePath)) { - const destPath = path.join(outputDir, "chatmodes", `${chatmodeName}.chatmode.md`); - copyFile(sourcePath, destPath); - copiedCount++; - summary.chatmodes++; - } - } - } - } - - // Process items from enabled collections - for (const itemPath of enabledItems) { - const sourcePath = path.join(rootDir, itemPath); + // Process prompts using effective states + for (const promptName of effectivelyEnabledSets.prompts) { + const sourcePath = path.join(rootDir, "prompts", `${promptName}.prompt.md`); if (fs.existsSync(sourcePath)) { - const fileName = path.basename(itemPath); - let destPath; - - if (fileName.endsWith('.prompt.md')) { - destPath = path.join(outputDir, "prompts", fileName); - } else if (fileName.endsWith('.chatmode.md')) { - destPath = path.join(outputDir, "chatmodes", fileName); - } else if (fileName.endsWith('.instructions.md')) { - destPath = path.join(outputDir, "instructions", fileName); - } - - if (destPath && !fs.existsSync(destPath)) { - copyFile(sourcePath, destPath); - copiedCount++; - } + const destPath = path.join(outputDir, "prompts", `${promptName}.prompt.md`); + copyFile(sourcePath, destPath); + copiedCount++; + summary.prompts++; + } + } + + // Process instructions using effective states + for (const instructionName of effectivelyEnabledSets.instructions) { + const sourcePath = path.join(rootDir, "instructions", `${instructionName}.instructions.md`); + if (fs.existsSync(sourcePath)) { + const destPath = path.join(outputDir, "instructions", `${instructionName}.instructions.md`); + copyFile(sourcePath, destPath); + copiedCount++; + summary.instructions++; + } + } + + // Process chat modes using effective states + for (const chatmodeName of effectivelyEnabledSets.chatmodes) { + const sourcePath = path.join(rootDir, "chatmodes", `${chatmodeName}.chatmode.md`); + if (fs.existsSync(sourcePath)) { + const destPath = path.join(outputDir, "chatmodes", `${chatmodeName}.chatmode.md`); + copyFile(sourcePath, destPath); + copiedCount++; + summary.chatmodes++; } } diff --git a/awesome-copilot.js b/awesome-copilot.js index a64eed5..4913c01 100755 --- a/awesome-copilot.js +++ b/awesome-copilot.js @@ -116,14 +116,22 @@ function handleListCommand(rawArgs) { const { config } = loadConfig(configPath); const sanitizedConfig = ensureConfigStructure(config); + // Import computeEffectiveItemStates + const { computeEffectiveItemStates } = require("./config-manager"); + const effectiveStates = computeEffectiveItemStates(sanitizedConfig); + console.log(`๐Ÿ“„ Configuration: ${configPath}`); sectionsToShow.forEach(section => { const availableItems = getAllAvailableItems(section); - const enabledCount = countEnabledItems(sanitizedConfig[section]); + + // Count effectively enabled items + const effectivelyEnabled = Object.values(effectiveStates[section] || {}) + .filter(state => state.enabled).length; + const { totalCharacters } = calculateSectionFootprint(section, sanitizedConfig[section]); const headingParts = [ - `${SECTION_METADATA[section].label} (${enabledCount}/${availableItems.length} enabled)` + `${SECTION_METADATA[section].label} (${effectivelyEnabled}/${availableItems.length} enabled)` ]; if (totalCharacters > 0 && section !== "collections") { @@ -137,10 +145,28 @@ function handleListCommand(rawArgs) { return; } - availableItems.forEach(itemName => { - const isEnabled = Boolean(sanitizedConfig[section]?.[itemName]); - console.log(` [${isEnabled ? "โœ“" : " "}] ${itemName}`); - }); + // Show items with effective state and reason + if (section === "collections") { + // Collections show simple enabled/disabled + availableItems.forEach(itemName => { + const isEnabled = Boolean(sanitizedConfig[section]?.[itemName]); + console.log(` [${isEnabled ? "โœ“" : " "}] ${itemName}`); + }); + } else { + // Other sections show effective state with reason + availableItems.forEach(itemName => { + const effectiveState = effectiveStates[section]?.[itemName]; + if (effectiveState) { + const symbol = effectiveState.enabled ? "โœ“" : " "; + const reasonText = effectiveState.enabled + ? ` (${effectiveState.reason})` + : ""; + console.log(` [${symbol}] ${itemName}${reasonText}`); + } else { + console.log(` [ ] ${itemName}`); + } + }); + } }); console.log("\nUse 'awesome-copilot toggle' to enable or disable specific items."); @@ -171,7 +197,66 @@ function handleToggleCommand(rawArgs) { }; const sectionState = configCopy[section]; - if (itemName === "all") { + // Special handling for collections to show delta summary + if (section === "collections") { + if (!availableSet.has(itemName)) { + const suggestion = findClosestMatch(itemName, availableItems); + if (suggestion) { + throw new Error(`Unknown ${SECTION_METADATA[section].singular} '${itemName}'. Did you mean '${suggestion}'?`); + } + throw new Error(`Unknown ${SECTION_METADATA[section].singular} '${itemName}'.`); + } + + const currentState = Boolean(sectionState[itemName]); + const newState = desiredState === null ? !currentState : desiredState; + + // Show delta summary for collections + if (currentState !== newState) { + const { computeEffectiveItemStates } = require("./config-manager"); + + // Compute effective states before change + const effectiveStatesBefore = computeEffectiveItemStates(configCopy); + + // Simulate the change + configCopy[section][itemName] = newState; + const effectiveStatesAfter = computeEffectiveItemStates(configCopy); + + // Calculate delta + const delta = { enabled: [], disabled: [] }; + for (const sectionName of ["prompts", "instructions", "chatmodes"]) { + for (const item of getAllAvailableItems(sectionName)) { + const beforeState = effectiveStatesBefore[sectionName]?.[item]?.enabled || false; + const afterState = effectiveStatesAfter[sectionName]?.[item]?.enabled || false; + + if (!beforeState && afterState) { + delta.enabled.push(`${sectionName}/${item}`); + } else if (beforeState && !afterState) { + delta.disabled.push(`${sectionName}/${item}`); + } + } + } + + console.log(`${newState ? "Enabled" : "Disabled"} collection '${itemName}'.`); + + if (delta.enabled.length > 0 || delta.disabled.length > 0) { + console.log("\nDelta summary:"); + if (delta.enabled.length > 0) { + console.log(` ๐Ÿ“ˆ ${delta.enabled.length} items will be enabled:`); + delta.enabled.forEach(item => console.log(` + ${item}`)); + } + if (delta.disabled.length > 0) { + console.log(` ๐Ÿ“‰ ${delta.disabled.length} items will be disabled:`); + delta.disabled.forEach(item => console.log(` - ${item}`)); + } + } else { + console.log(" No effective changes (items may have explicit overrides)"); + } + } else { + console.log(`Collection '${itemName}' is already ${currentState ? "enabled" : "disabled"}.`); + } + + sectionState[itemName] = newState; + } else if (itemName === "all") { if (desiredState === null) { throw new Error("Specify 'on' or 'off' when toggling all items."); } @@ -205,7 +290,7 @@ function handleToggleCommand(rawArgs) { const totalAvailable = availableItems.length; const { totalCharacters } = calculateSectionFootprint(section, sanitizedConfig[section]); - console.log(`${SECTION_METADATA[section].label}: ${enabledCount}/${totalAvailable} enabled.`); + console.log(`\n${SECTION_METADATA[section].label}: ${enabledCount}/${totalAvailable} enabled.`); if (totalCharacters > 0 && section !== "collections") { console.log(`Estimated ${SECTION_METADATA[section].label.toLowerCase()} context size: ${formatNumber(totalCharacters)} characters.`); } diff --git a/config-manager.js b/config-manager.js index 628837f..5ffe602 100644 --- a/config-manager.js +++ b/config-manager.js @@ -158,6 +158,113 @@ 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 + * @returns {Object} Effective states for each section with { itemName: { enabled: boolean, reason: string } } + */ +function computeEffectiveItemStates(config) { + const { parseCollectionYaml } = require("./yaml-parser"); + + const effectiveStates = { + prompts: {}, + instructions: {}, + chatmodes: {} + }; + + // First, collect all items enabled by collections + const collectionEnabledItems = { + prompts: new Set(), + instructions: new Set(), + chatmodes: new Set() + }; + + if (config.collections) { + for (const [collectionName, enabled] of Object.entries(config.collections)) { + if (enabled === true) { + const collectionPath = path.join(__dirname, "collections", `${collectionName}.collection.yml`); + if (fs.existsSync(collectionPath)) { + const collection = parseCollectionYaml(collectionPath); + if (collection && collection.items) { + collection.items.forEach(item => { + // Extract item name from path - remove directory and all extensions + const itemName = path.basename(item.path).replace(/\.(prompt|instructions|chatmode)\.md$/, ''); + + if (item.kind === "prompt") { + collectionEnabledItems.prompts.add(itemName); + } else if (item.kind === "instruction") { + collectionEnabledItems.instructions.add(itemName); + } else if (item.kind === "chat-mode") { + collectionEnabledItems.chatmodes.add(itemName); + } + }); + } + } + } + } + } + + // For each section, compute effective states + for (const section of ["prompts", "instructions", "chatmodes"]) { + const sectionConfig = config[section] || {}; + const collectionEnabled = collectionEnabledItems[section]; + + // Get all available items for this section + const availableItems = getAllAvailableItems(section); + + for (const itemName of availableItems) { + const explicitValue = sectionConfig[itemName]; + const isEnabledByCollection = collectionEnabled.has(itemName); + + // Precedence rules: + // 1. If explicitly set to true or false, use that value + // 2. If undefined and enabled by collection, use true + // 3. Otherwise, use false + + let enabled = false; + let reason = "disabled"; + + if (explicitValue === true) { + enabled = true; + reason = "explicit"; + } else if (explicitValue === false) { + enabled = false; + reason = "explicit"; + } else if (explicitValue === undefined && isEnabledByCollection) { + enabled = true; + reason = "collection"; + } + + effectiveStates[section][itemName] = { enabled, reason }; + } + } + + return effectiveStates; +} + module.exports = { DEFAULT_CONFIG_PATH, CONFIG_SECTIONS, @@ -168,5 +275,7 @@ module.exports = { ensureConfigStructure, sortObjectKeys, countEnabledItems, - getAllAvailableItems + getAllAvailableItems, + 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-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 diff --git a/test-effective-config.yml b/test-effective-config.yml new file mode 100644 index 0000000..5bed12b --- /dev/null +++ b/test-effective-config.yml @@ -0,0 +1,17 @@ +# Awesome Copilot Configuration File +# Manual test for effective state computation +# +# Testing precedence rules with undefined values + +version: "1.0" +project: + name: "Test Project" + description: "Testing effective state precedence" + output_directory: ".awesome-copilot" +collections: + testing-automation: false +prompts: + ai-prompt-engineering-safety-review: false + playwright-generate-test: true +instructions: +chatmodes: 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