diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2ebe165..36aff72 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -40,14 +40,12 @@ The following instructions are only to be applied when performing a code review. * [ ] Encourage the use of `tools`, but it's not required. * [ ] Strongly encourage the use of `model` to specify the model that the chat mode is optimised for. - ## Branching Policy -- Always create a new branch for each task, feature, or fix. -- Use the following naming conventions: - - `feature/` - - `bugfix/` - - `task/` -- Never commit changes directly to the `main` or default branch. -- After completing changes, push the branch and open a pull request for review and merging. +* [ ] Always create a new branch for each task or issue you are working on. +* [ ] Use descriptive branch names following the convention: `feature/description`, `fix/description`, or `docs/description`. +* [ ] Never commit directly to the `main` branch. +* [ ] Always open a pull request for code changes, even for small updates. +* [ ] Ensure your branch is up to date with `main` before opening a pull request. +* [ ] Delete the branch after the pull request is merged. diff --git a/.vscode/settings.json b/.vscode/settings.json index c01085e..95e8122 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,14 @@ { "chat.modeFilesLocations": { - "chatmodes": true + "chatmodes": true, + ".github/chatmodes": true }, "chat.promptFilesLocations": { - "prompts": true + "prompts": true, + ".github/prompts": true }, "chat.instructionsFilesLocations": { - "instructions": true + "instructions": true, + ".github/instructions": true } -} +} \ No newline at end of file diff --git a/apply-config.js b/apply-config.js index b1e5c65..8465d51 100755 --- a/apply-config.js +++ b/apply-config.js @@ -101,36 +101,40 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") { }; // Import config manager for effective state computation - const { computeEffectiveItemStates } = require("./config-manager"); + const { getEffectivelyEnabledItems } = require("./config-manager"); - // Compute effective states using precedence rules - const effectiveStates = computeEffectiveItemStates(config); + // Get precomputed sets of effectively enabled items for O(1) performance + const effectivelyEnabledSets = getEffectivelyEnabledItems(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 + // Count effectively enabled collections for summary + // A collection is effectively enabled if it contributes any enabled items if (config.collections) { - for (const [collectionName, enabled] of Object.entries(config.collections)) { - if (enabled) { + for (const [collectionName, configEnabled] of Object.entries(config.collections)) { + if (configEnabled) { const collectionPath = path.join(rootDir, "collections", `${collectionName}.collection.yml`); if (fs.existsSync(collectionPath)) { const collection = parseCollectionYaml(collectionPath); if (collection && collection.items) { - summary.collections++; - console.log(`✓ Enabled collection: ${collectionName} (${collection.items.length} items)`); + // Check if this collection contributes any effectively enabled items + let hasEnabledItems = false; + for (const item of collection.items) { + const itemName = path.basename(item.path).replace(/\.(prompt|instructions|chatmode)\.md$/, ''); + if (item.kind === "prompt" && effectivelyEnabledSets.prompts.has(itemName)) { + hasEnabledItems = true; + break; + } else if (item.kind === "instruction" && effectivelyEnabledSets.instructions.has(itemName)) { + hasEnabledItems = true; + break; + } else if (item.kind === "chat-mode" && effectivelyEnabledSets.chatmodes.has(itemName)) { + hasEnabledItems = true; + break; + } + } + + if (hasEnabledItems) { + summary.collections++; + console.log(`✓ Enabled collection: ${collectionName} (${collection.items.length} items)`); + } } } } diff --git a/awesome-copilot.js b/awesome-copilot.js index 15580e7..4784e95 100755 --- a/awesome-copilot.js +++ b/awesome-copilot.js @@ -149,8 +149,16 @@ function handleListCommand(rawArgs) { const availableItems = getAllAvailableItems(section); // Count effectively enabled items - const effectivelyEnabled = Object.values(effectiveStates[section] || {}) - .filter(state => state.enabled).length; + let effectivelyEnabled; + if (section === "collections") { + // For collections, count explicitly enabled ones + effectivelyEnabled = Object.values(sanitizedConfig[section] || {}) + .filter(value => value === true).length; + } else { + // For other sections, count effectively enabled items + effectivelyEnabled = Object.values(effectiveStates[section] || {}) + .filter(state => state.enabled).length; + } const { totalCharacters } = calculateSectionFootprint(section, sanitizedConfig[section]); const headingParts = [ @@ -170,20 +178,40 @@ function handleListCommand(rawArgs) { // Show items with effective state and reason if (section === "collections") { - // Collections show simple enabled/disabled + // Collections show simple enabled/disabled with count of effectively enabled items availableItems.forEach(itemName => { const isEnabled = Boolean(sanitizedConfig[section]?.[itemName]); - console.log(` [${isEnabled ? "✓" : " "}] ${itemName}`); + + // Count how many items this collection would enable + let enabledCount = 0; + if (isEnabled) { + // Simulate what would happen if only this collection was enabled + const testConfig = { collections: { [itemName]: true } }; + const testEffectiveStates = computeEffectiveItemStates(testConfig); + + for (const sectionName of ["prompts", "instructions", "chatmodes"]) { + enabledCount += Object.values(testEffectiveStates[sectionName] || {}) + .filter(state => state.enabled && state.reason === "collection").length; + } + } + + const countText = isEnabled ? ` (${enabledCount} items effectively enabled)` : ""; + console.log(` [${isEnabled ? "✓" : " "}] ${itemName}${countText}`); }); } else { - // Other sections show effective state with reason + // Other sections show effective state with detailed reason availableItems.forEach(itemName => { const effectiveState = effectiveStates[section]?.[itemName]; if (effectiveState) { const symbol = effectiveState.enabled ? "✓" : " "; - const reasonText = effectiveState.reason === "explicit" - ? ` (${effectiveState.reason})` - : effectiveState.enabled ? ` (${effectiveState.reason})` : ""; + let reasonText = ""; + + if (effectiveState.reason === "explicit") { + reasonText = ` (explicit:${effectiveState.enabled})`; + } else if (effectiveState.reason === "collection" && effectiveState.collections && effectiveState.collections.length > 0) { + reasonText = ` (via:[${effectiveState.collections.join(',')}])`; + } + console.log(` [${symbol}] ${itemName}${reasonText}`); } else { console.log(` [ ] ${itemName}`); diff --git a/config-manager.js b/config-manager.js index d1b6a10..fbc2cb7 100644 --- a/config-manager.js +++ b/config-manager.js @@ -101,6 +101,11 @@ function toBoolean(value) { if (normalized === "false") return false; } + // Preserve undefined as undefined for "no explicit override" + if (value === undefined) { + return undefined; + } + return Boolean(value); } @@ -185,11 +190,17 @@ function generateConfigHash(config) { * Compute effective item states respecting explicit overrides over collections * * This function builds membership maps per section and returns effectively enabled items with reasons. - * It uses the following precedence rules: - * 1. Explicit true/false overrides everything (highest priority) + * It uses strict comparisons to ensure undefined values are never treated as explicitly disabled. + * + * Precedence rules with strict undefined handling: + * 1. Explicit true/false overrides everything (highest priority) - uses strict === comparisons * 2. If undefined and enabled by collection, use true * 3. Otherwise, use false (disabled) * + * CRITICAL: Only values that are strictly === false are treated as explicitly disabled. + * undefined, null, 0, '', or other falsy values are NOT treated as explicit disabling. + * This allows collections to enable items that are not explicitly configured. + * * @param {Object} config - Configuration object with sections * @returns {Object} Effective states for each section with { itemName: { enabled: boolean, reason: string } } * Reason can be: 'explicit', 'collection', or 'disabled' @@ -203,14 +214,21 @@ function computeEffectiveItemStates(config) { chatmodes: {} }; - // Build membership maps: Map> per section for O(1) lookups + // Build detailed membership maps: Map> per section + const collectionMemberships = { + prompts: new Map(), + instructions: new Map(), + chatmodes: new Map() + }; + + // Build simple enabled sets for O(1) lookups const collectionEnabledItems = { prompts: new Set(), instructions: new Set(), chatmodes: new Set() }; - // Identify enabled collections per section + // Identify enabled collections per section and track memberships if (config.collections) { for (const [collectionName, enabled] of Object.entries(config.collections)) { if (enabled === true) { @@ -222,12 +240,24 @@ function computeEffectiveItemStates(config) { // Extract item name from path - remove directory and all extensions const itemName = path.basename(item.path).replace(/\.(prompt|instructions|chatmode)\.md$/, ''); + let sectionName; if (item.kind === "prompt") { - collectionEnabledItems.prompts.add(itemName); + sectionName = "prompts"; } else if (item.kind === "instruction") { - collectionEnabledItems.instructions.add(itemName); + sectionName = "instructions"; } else if (item.kind === "chat-mode") { - collectionEnabledItems.chatmodes.add(itemName); + sectionName = "chatmodes"; + } + + if (sectionName) { + // Track which collections enable this item + if (!collectionMemberships[sectionName].has(itemName)) { + collectionMemberships[sectionName].set(itemName, new Set()); + } + collectionMemberships[sectionName].get(itemName).add(collectionName); + + // Add to enabled set for O(1) lookups + collectionEnabledItems[sectionName].add(itemName); } }); } @@ -247,27 +277,35 @@ function computeEffectiveItemStates(config) { for (const itemName of availableItems) { const explicitValue = sectionConfig[itemName]; const isEnabledByCollection = collectionEnabled.has(itemName); + const enablingCollections = collectionMemberships[section].get(itemName) || new Set(); - // Precedence rules: - // 1. If explicitly set to true or false, use that value + // Precedence rules with strict undefined handling: + // 1. If explicitly set to true or false, use that value (highest priority) // 2. If undefined and enabled by collection, use true - // 3. Otherwise, use false + // 3. Otherwise, use false (disabled) + // + // IMPORTANT: Only strict === false comparisons are used to determine explicit disabling. + // undefined values are NEVER treated as explicitly disabled, allowing collections to enable them. let enabled = false; let reason = "disabled"; + let collections = []; if (explicitValue === true) { enabled = true; reason = "explicit"; } else if (explicitValue === false) { + // Strict comparison ensures only explicit false disables items enabled = false; reason = "explicit"; } else if (explicitValue === undefined && isEnabledByCollection) { + // undefined values can be enabled by collections (not treated as disabled) enabled = true; reason = "collection"; + collections = Array.from(enablingCollections).sort(); } - effectiveStates[section][itemName] = { enabled, reason }; + effectiveStates[section][itemName] = { enabled, reason, collections }; } } @@ -303,6 +341,93 @@ function getEffectivelyEnabledItems(config) { return result; } +/** + * Toggle a collection's enabled state without modifying individual item flags + * + * This function implements the core requirement from TASK-003: + * - Only modifies the collection's enabled flag + * - Never writes to per-item flags (e.g., config.prompts[item] = enabled) + * - Returns summary information for CLI feedback + * - Preserves all existing explicit per-item overrides + * + * @param {Object} config - Configuration object with collections section + * @param {string} name - Collection name to toggle + * @param {boolean} enabled - Desired enabled state for the collection + * @returns {Object} Summary object with delta information for CLI feedback + */ +function toggleCollection(config, name, enabled) { + // Validate inputs + if (!config || typeof config !== 'object') { + throw new Error('Config object is required'); + } + if (!name || typeof name !== 'string') { + throw new Error('Collection name is required'); + } + if (typeof enabled !== 'boolean') { + throw new Error('Enabled state must be a boolean'); + } + + // Ensure collections section exists + if (!config.collections) { + config.collections = {}; + } + + // Get current state + const currentState = Boolean(config.collections[name]); + + // Check if collection exists (has a .collection.yml file) + const collectionPath = path.join(__dirname, "collections", `${name}.collection.yml`); + if (!fs.existsSync(collectionPath)) { + throw new Error(`Collection '${name}' does not exist`); + } + + // If state is already the desired state, return early + if (currentState === enabled) { + return { + changed: false, + collectionName: name, + currentState: currentState, + newState: enabled, + delta: { enabled: [], disabled: [] }, + message: `Collection '${name}' is already ${enabled ? 'enabled' : 'disabled'}.` + }; + } + + // Compute effective states before the change + const effectiveStatesBefore = computeEffectiveItemStates(config); + + // CORE REQUIREMENT: Only modify the collection's enabled flag + // Never write to per-item flags (config.prompts[item] = enabled) + config.collections[name] = enabled; + + // Compute effective states after the change + const effectiveStatesAfter = computeEffectiveItemStates(config); + + // Calculate delta summary for CLI feedback + 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}`); + } + } + } + + return { + changed: true, + collectionName: name, + currentState: currentState, + newState: enabled, + delta, + message: `${enabled ? 'Enabled' : 'Disabled'} collection '${name}'.` + }; +} + module.exports = { DEFAULT_CONFIG_PATH, CONFIG_SECTIONS, @@ -316,5 +441,6 @@ module.exports = { getAllAvailableItems, computeEffectiveItemStates, getEffectivelyEnabledItems, - generateConfigHash + generateConfigHash, + toggleCollection }; diff --git a/test-all.js b/test-all.js index 8c4ead0..5431778 100644 --- a/test-all.js +++ b/test-all.js @@ -8,6 +8,7 @@ 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'); +const { runTests: runToggleCollectionTests } = require('./test-toggle-collection'); async function runAllTests() { console.log('🧪 Running Awesome Copilot Comprehensive Test Suite\n'); @@ -17,7 +18,8 @@ async function runAllTests() { unit: false, integration: false, cli: false, - apply: false + apply: false, + toggleCollection: false }; try { @@ -52,6 +54,14 @@ async function runAllTests() { console.error('Apply tests failed with error:', error.message); } + try { + console.log('\n🔧 Toggle Collection Tests (TASK-003)'); + console.log('-'.repeat(36)); + results.toggleCollection = await runToggleCollectionTests(); + } catch (error) { + console.error('Toggle collection tests failed with error:', error.message); + } + try { console.log('\n🤖 Repository Instructions Tests'); console.log('-'.repeat(33)); @@ -71,6 +81,7 @@ async function runAllTests() { { name: 'Integration Tests', result: results.integration, emoji: '🔄' }, { name: 'CLI Tests', result: results.cli, emoji: '⌨️' }, { name: 'Apply Tests', result: results.apply, emoji: '🎯' }, + { name: 'Toggle Collection', result: results.toggleCollection, emoji: '🔧' }, { name: 'Repo Instructions', result: results.repoInstructions, emoji: '🤖' } ]; diff --git a/test-cli.js b/test-cli.js index 58a62f2..a3cfd90 100644 --- a/test-cli.js +++ b/test-cli.js @@ -92,7 +92,7 @@ async function runTests() { 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('(via:[testing-automation])'), 'Should show collection reason'); assert(result.stdout.includes('[✓]'), 'Should show enabled items'); }); @@ -105,7 +105,7 @@ async function runTests() { 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 (explicit)'), + assert(listResult.stdout.includes('playwright-generate-test (explicit:false)'), 'Explicitly disabled item should show explicit reason'); }); @@ -128,6 +128,209 @@ async function runTests() { assert(result.stdout.includes('already enabled'), 'Should indicate no change needed'); }); + // Test 8: Multiple collections effective states + await test("Multiple collections effective states", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + + // Enable two collections with potential overlap + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + await runCommand(`node awesome-copilot.js toggle collections csharp-dotnet-development on --config ${TEST_CONFIG}`); + + const result = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); + assert(result.success, 'List should succeed'); + + // Should show collection reason for items enabled by collection + assert(result.stdout.includes('(collection)'), 'Should show collection reason'); + assert(result.stdout.includes('[✓]'), 'Should show enabled items'); + + // Count should reflect enabled items from both collections + const enabledMatch = result.stdout.match(/Prompts \((\d+)\/\d+ enabled\)/); + assert(enabledMatch && parseInt(enabledMatch[1]) > 5, 'Should have multiple enabled prompts from collections'); + }); + + // Test 9: Explicit overrides with collection conflicts + await test("Explicit overrides with collection conflicts", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + + // Override a collection item explicitly to off + await runCommand(`node awesome-copilot.js toggle prompts playwright-generate-test off --config ${TEST_CONFIG}`); + + const listResult = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); + assert(listResult.success, 'List should succeed'); + + // Should show explicit reason and disabled state + assert(listResult.stdout.includes('playwright-generate-test (explicit)'), 'Should show explicit reason'); + assert(listResult.stdout.includes('[ ] playwright-generate-test'), 'Should show as disabled despite collection'); + + // Other collection items should still show as enabled + assert(listResult.stdout.includes('[✓] playwright-explore-website (collection)'), 'Other collection items should remain enabled'); + }); + + // Test 10: Delta summary accuracy with conflicts + await test("Delta summary accuracy with conflicts", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + + // Enable collection + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + + // Add explicit override to disable one item + await runCommand(`node awesome-copilot.js toggle prompts playwright-generate-test off --config ${TEST_CONFIG}`); + + // Disable the collection - should show accurate delta excluding explicit override + const result = await runCommand(`node awesome-copilot.js toggle collections testing-automation off --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 disabled'), 'Should show disabled items count'); + + // Should not include the explicitly disabled item in the delta + const lines = result.stdout.split('\n'); + const disabledItems = lines.filter(line => line.includes(' - prompts/')); + assert(!disabledItems.some(line => line.includes('playwright-generate-test')), + 'Should not show explicitly disabled item in delta'); + }); + + // Test 11: Instructions and chatmodes effective states + await test("Instructions and chatmodes effective states", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + + // Test instructions + const instructionsResult = await runCommand(`node awesome-copilot.js list instructions --config ${TEST_CONFIG}`); + assert(instructionsResult.success, 'Instructions list should succeed'); + assert(instructionsResult.stdout.includes('[✓] playwright-typescript (collection)'), + 'Instructions should show collection reason'); + + // Test chatmodes + const chatmodesResult = await runCommand(`node awesome-copilot.js list chatmodes --config ${TEST_CONFIG}`); + assert(chatmodesResult.success, 'Chatmodes list should succeed'); + assert(chatmodesResult.stdout.includes('[✓] tdd-red (collection)'), + 'Chatmodes should show collection reason'); + }); + + // Test 12: Collections section display + await test("Collections section shows simple enabled/disabled", 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 collections --config ${TEST_CONFIG}`); + assert(result.success, 'Collections list should succeed'); + assert(result.stdout.includes('[✓] testing-automation'), 'Should show enabled collection'); + assert(result.stdout.includes('[ ] csharp-dotnet-development'), 'Should show disabled collection'); + + // Collections should not show reasons (unlike other sections) + assert(!result.stdout.includes('(explicit)'), 'Collections should not show reason text'); + assert(!result.stdout.includes('(collection)'), 'Collections should not show reason text'); + }); + + // Test 13: Output clarity and user-friendliness + await test("Output clarity and user-friendliness", 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 --config ${TEST_CONFIG}`); + assert(result.success, 'List should succeed'); + + // Should show counts (character estimates may or may not appear depending on content) + assert(result.stdout.includes('enabled'), 'Should show enabled counts'); + + // Should show helpful usage message + assert(result.stdout.includes("Use 'awesome-copilot toggle'"), 'Should show usage instructions'); + + // Should show clear section headers + 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'); + + // Should show configuration path + assert(result.stdout.includes('Configuration:'), 'Should show configuration file path'); + }); + + // Test 14: Complex scenario with multiple overrides + await test("Complex scenario with multiple overrides", async () => { + await runCommand(`node awesome-copilot.js init ${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 csharp-dotnet-development on --config ${TEST_CONFIG}`); + + // Add explicit overrides + 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}`); + + const result = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); + assert(result.success, 'List should succeed'); + + // Should show mixed states with correct reasons + assert(result.stdout.includes('[ ] playwright-generate-test (explicit)'), 'Should show explicit disable'); + assert(result.stdout.includes('[✓] create-readme (explicit)'), 'Should show explicit enable'); + assert(result.stdout.includes('(collection)'), 'Should show collection-enabled items'); + + // Count should be accurate + const enabledMatch = result.stdout.match(/Prompts \((\d+)\/\d+ enabled\)/); + assert(enabledMatch, 'Should show enabled count'); + + const enabledCount = parseInt(enabledMatch[1]); + assert(enabledCount > 8, 'Should have multiple items enabled from collections and explicit'); + }); + + // Test 15: No misleading disabled messages for shared items + await test("No misleading disabled messages for shared items", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + + // Enable a collection that has items potentially shared with other collections + await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`); + + // Toggle collection off and check delta summary + const result = await runCommand(`node awesome-copilot.js toggle collections testing-automation off --config ${TEST_CONFIG}`); + assert(result.success, 'Toggle should succeed'); + + // Should show accurate delta - items should be listed as will be disabled + // since they're not enabled by other collections in this test + assert(result.stdout.includes('Delta summary'), 'Should show delta summary'); + assert(result.stdout.includes('items will be disabled'), 'Should show disabled items count'); + + // Verify the messaging is clear and not misleading + const lines = result.stdout.split('\n'); + const deltaLines = lines.filter(line => line.includes('+ ') || line.includes('- ')); + assert(deltaLines.length > 0, 'Should show specific items in delta'); + }); + + // Test 16: Membership and counts verification + await test("Membership and counts verification", async () => { + await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); + + // Enable collection and add explicit overrides + 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 all section results + const promptsResult = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); + const instructionsResult = await runCommand(`node awesome-copilot.js list instructions --config ${TEST_CONFIG}`); + const chatmodesResult = await runCommand(`node awesome-copilot.js list chatmodes --config ${TEST_CONFIG}`); + + // Verify counts are accurate and consistent + const promptsMatch = promptsResult.stdout.match(/Prompts \((\d+)\/(\d+) enabled\)/); + const instructionsMatch = instructionsResult.stdout.match(/Instructions \((\d+)\/(\d+) enabled\)/); + const chatmodesMatch = chatmodesResult.stdout.match(/Chat Modes \((\d+)\/(\d+) enabled\)/); + + assert(promptsMatch, 'Should show prompts count'); + assert(instructionsMatch, 'Should show instructions count'); + assert(chatmodesMatch, 'Should show chatmodes count'); + + // Counts should be reasonable for testing-automation collection + assert(parseInt(promptsMatch[1]) >= 4, 'Should have reasonable prompts enabled'); + assert(parseInt(instructionsMatch[1]) >= 2, 'Should have reasonable instructions enabled'); + assert(parseInt(chatmodesMatch[1]) >= 3, 'Should have reasonable chatmodes enabled'); + + // Total items should be consistent + assert(parseInt(promptsMatch[2]) > 80, 'Should show total prompts available'); + assert(parseInt(instructionsMatch[2]) > 70, 'Should show total instructions available'); + assert(parseInt(chatmodesMatch[2]) > 50, 'Should show total chatmodes available'); + }); + console.log(`\nCLI Test Results: ${passedTests}/${totalTests} passed`); cleanup(); // Final cleanup diff --git a/test-effective-config.yml b/test-effective-config.yml deleted file mode 100644 index 3187a8d..0000000 --- a/test-effective-config.yml +++ /dev/null @@ -1,17 +0,0 @@ -# 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: ".github" -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 index 621f04b..4755d7c 100644 --- a/test-effective-states.js +++ b/test-effective-states.js @@ -164,7 +164,50 @@ function runTests() { 'Chat mode should be enabled by collection'); }); - // Test 8: getEffectivelyEnabledItems returns Sets format + // Test 9: TASK-006 - Strict false checks prevent undefined treated as disabled + test("TASK-006: Strict false checks prevent undefined treated as disabled", () => { + const config = { + prompts: { + 'explicit-false-item': false, + 'explicit-true-item': true, + // 'undefined-item' is undefined (not set) + }, + collections: { + 'testing-automation': true + } + }; + const result = computeEffectiveItemStates(config); + + // Explicit false should be disabled with reason 'explicit' + const explicitFalse = result.prompts['explicit-false-item']; + if (explicitFalse) { + assert(explicitFalse.reason === 'explicit' && !explicitFalse.enabled, + 'Items with explicit false should be disabled with reason explicit'); + } + + // Explicit true should be enabled with reason 'explicit' + const explicitTrue = result.prompts['explicit-true-item']; + if (explicitTrue) { + assert(explicitTrue.reason === 'explicit' && explicitTrue.enabled, + 'Items with explicit true should be enabled with reason explicit'); + } + + // Undefined item in collection should be enabled with reason 'collection' + const undefinedInCollection = result.prompts['playwright-generate-test']; + if (undefinedInCollection) { + assert(undefinedInCollection.reason === 'collection' && undefinedInCollection.enabled, + 'Undefined items should be enabled by collections, not treated as explicitly disabled'); + } + + // Undefined item NOT in collection should be disabled with reason 'disabled' (not 'explicit') + const undefinedNotInCollection = result.prompts['some-random-item-not-in-any-collection']; + if (undefinedNotInCollection) { + assert(undefinedNotInCollection.reason === 'disabled' && !undefinedNotInCollection.enabled, + 'Undefined items not in collections should have reason disabled, not explicit'); + } + }); + + // Test 10: getEffectivelyEnabledItems returns Sets format test("getEffectivelyEnabledItems returns Sets format", () => { const config = { prompts: { @@ -214,6 +257,37 @@ function runTests() { assert(result.prompts.size > 0, 'Should have enabled prompts'); }); + // Test 10: Undefined values are not treated as explicitly disabled (TASK-004) + test("Undefined values are not treated as explicitly disabled", () => { + const config = { + prompts: { + 'playwright-generate-test': true, // explicit true + 'csharp-nunit': false, // explicit false + // 'playwright-explore-website' is undefined (not mentioned) + }, + collections: { + 'testing-automation': true + } + }; + + const result = computeEffectiveItemStates(config); + + // Explicit true should be explicit + const explicitTrue = result.prompts['playwright-generate-test']; + assert(explicitTrue && explicitTrue.enabled && explicitTrue.reason === 'explicit', + 'Explicit true should be enabled with explicit reason'); + + // Explicit false should be explicit (strict === false comparison) + const explicitFalse = result.prompts['csharp-nunit']; + assert(explicitFalse && !explicitFalse.enabled && explicitFalse.reason === 'explicit', + 'Explicit false should be disabled with explicit reason'); + + // Undefined should inherit from collection + const undefinedItem = result.prompts['playwright-explore-website']; + assert(undefinedItem && undefinedItem.enabled && undefinedItem.reason === 'collection', + 'Undefined items should inherit from collection, not be treated as explicitly disabled'); + }); + console.log(`\nTest Results: ${passedTests}/${totalTests} passed`); if (passedTests === totalTests) { diff --git a/test-new-config.yml b/test-new-config.yml deleted file mode 100644 index 4c92107..0000000 --- a/test-new-config.yml +++ /dev/null @@ -1,35 +0,0 @@ -# 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: ".github" -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 diff --git a/test-new-features.js b/test-new-features.js index badbe82..766d511 100755 --- a/test-new-features.js +++ b/test-new-features.js @@ -119,14 +119,14 @@ async function runTests() { // Check it's explicitly disabled const listResult1 = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); - assert(listResult1.stdout.includes('create-readme (explicit)'), 'Should show explicit disabled'); + assert(listResult1.stdout.includes('create-readme (explicit:false)'), 'Should show explicit disabled'); // Force enable all with --all flag await runCommand(`node awesome-copilot.js toggle prompts all on --all --config ${TEST_CONFIG}`); // Check it's now explicitly enabled const listResult2 = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); - assert(listResult2.stdout.includes('create-readme (explicit)') && listResult2.stdout.includes('[✓]'), 'Should be explicitly enabled'); + assert(listResult2.stdout.includes('create-readme (explicit:true)') && listResult2.stdout.includes('[✓]'), 'Should be explicitly enabled'); }); // Test 4: File cleanup on disable @@ -180,7 +180,7 @@ async function runTests() { // Double-check with list command const listResult = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); - assert(listResult.stdout.includes('playwright-generate-test (explicit)'), "'playwright-generate-test (explicit)' should be present in the list output"); + assert(listResult.stdout.includes('playwright-generate-test (explicit:false)'), "'playwright-generate-test (explicit:false)' should be present in the list output"); assert(!listResult.stdout.includes('[✓] playwright-generate-test'), "'[✓] playwright-generate-test' should NOT be present in the list output (should remain explicitly disabled)"); }); diff --git a/test-toggle-collection.js b/test-toggle-collection.js new file mode 100644 index 0000000..91e7690 --- /dev/null +++ b/test-toggle-collection.js @@ -0,0 +1,194 @@ +#!/usr/bin/env node + +/** + * Unit tests for toggleCollection function + */ + +const path = require('path'); +const { toggleCollection, 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() { + let totalTests = 0; + let passedTests = 0; + + function test(name, testFn) { + totalTests++; + try { + testFn(); + console.log(`✅ ${name}`); + passedTests++; + } catch (error) { + console.log(`❌ ${name}: ${error.message}`); + } + } + + console.log("Running unit tests for toggleCollection function...\n"); + + // Test 1: Toggle collection from false to true + test("Toggle collection from false to true", () => { + const config = { + collections: { + 'testing-automation': false + } + }; + + const result = toggleCollection(config, 'testing-automation', true); + + assert(result.changed === true, 'Should indicate change occurred'); + assert(result.collectionName === 'testing-automation', 'Should return correct collection name'); + assert(result.currentState === false, 'Should show previous state as false'); + assert(result.newState === true, 'Should show new state as true'); + assert(config.collections['testing-automation'] === true, 'Config should be updated'); + assert(result.delta.enabled.length > 0, 'Should show items being enabled'); + }); + + // Test 2: Toggle collection from true to false + test("Toggle collection from true to false", () => { + const config = { + collections: { + 'testing-automation': true + } + }; + + const result = toggleCollection(config, 'testing-automation', false); + + assert(result.changed === true, 'Should indicate change occurred'); + assert(result.newState === false, 'Should show new state as false'); + assert(config.collections['testing-automation'] === false, 'Config should be updated'); + assert(result.delta.disabled.length > 0, 'Should show items being disabled'); + }); + + // Test 3: Toggle collection to same state (no change) + test("Toggle collection to same state (no change)", () => { + const config = { + collections: { + 'testing-automation': true + } + }; + + const result = toggleCollection(config, 'testing-automation', true); + + assert(result.changed === false, 'Should indicate no change occurred'); + assert(result.message.includes('already enabled'), 'Should indicate already enabled'); + }); + + // Test 4: Preserves explicit overrides + test("Preserves explicit overrides", () => { + const config = { + prompts: { + 'playwright-generate-test': false // Explicit override + }, + collections: { + 'testing-automation': false + } + }; + + // Enable collection + const result = toggleCollection(config, 'testing-automation', true); + + // Verify the explicit override is preserved + assert(config.prompts['playwright-generate-test'] === false, 'Explicit override should be preserved'); + assert(config.collections['testing-automation'] === true, 'Collection should be enabled'); + + // Check that the overridden item is not in the enabled delta + const hasOverriddenItem = result.delta.enabled.some(item => item.includes('playwright-generate-test')); + assert(!hasOverriddenItem, 'Explicitly disabled item should not appear in enabled delta'); + }); + + // Test 5: Only modifies collection flag, never individual items + test("Only modifies collection flag, never individual items", () => { + const config = { + prompts: {}, + instructions: {}, + chatmodes: {}, + collections: { + 'testing-automation': false + } + }; + + const originalPrompts = { ...config.prompts }; + const originalInstructions = { ...config.instructions }; + const originalChatmodes = { ...config.chatmodes }; + + toggleCollection(config, 'testing-automation', true); + + // Verify individual sections were not modified + assert(JSON.stringify(config.prompts) === JSON.stringify(originalPrompts), 'Prompts section should not be modified'); + assert(JSON.stringify(config.instructions) === JSON.stringify(originalInstructions), 'Instructions section should not be modified'); + assert(JSON.stringify(config.chatmodes) === JSON.stringify(originalChatmodes), 'Chatmodes section should not be modified'); + + // Only collections should be modified + assert(config.collections['testing-automation'] === true, 'Only collection flag should be modified'); + }); + + // Test 6: Error handling for invalid inputs + test("Error handling for invalid inputs", () => { + let errorThrown = false; + + try { + toggleCollection(null, 'test', true); + } catch (error) { + errorThrown = true; + assert(error.message.includes('Config object is required'), 'Should require config object'); + } + assert(errorThrown, 'Should throw error for null config'); + + errorThrown = false; + try { + toggleCollection({}, '', true); + } catch (error) { + errorThrown = true; + assert(error.message.includes('Collection name is required'), 'Should require collection name'); + } + assert(errorThrown, 'Should throw error for empty name'); + + errorThrown = false; + try { + toggleCollection({}, 'test', 'not-boolean'); + } catch (error) { + errorThrown = true; + assert(error.message.includes('Enabled state must be a boolean'), 'Should require boolean enabled state'); + } + assert(errorThrown, 'Should throw error for non-boolean enabled state'); + }); + + // Test 7: Error handling for non-existent collection + test("Error handling for non-existent collection", () => { + const config = { collections: {} }; + + let errorThrown = false; + try { + toggleCollection(config, 'non-existent-collection', true); + } catch (error) { + errorThrown = true; + assert(error.message.includes('does not exist'), 'Should indicate collection does not exist'); + } + assert(errorThrown, 'Should throw error for non-existent collection'); + }); + + console.log(`\nTest Results: ${passedTests}/${totalTests} passed`); + + if (passedTests === totalTests) { + console.log('🎉 All toggleCollection tests passed!'); + return true; + } else { + console.log('💥 Some toggleCollection 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