This commit is contained in:
AstroSteveo 2025-09-23 21:43:49 -05:00
commit b639155ec6
12 changed files with 703 additions and 114 deletions

View File

@ -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. * [ ] 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. * [ ] Strongly encourage the use of `model` to specify the model that the chat mode is optimised for.
## Branching Policy ## Branching Policy
- Always create a new branch for each task, feature, or fix. * [ ] Always create a new branch for each task or issue you are working on.
- Use the following naming conventions: * [ ] Use descriptive branch names following the convention: `feature/description`, `fix/description`, or `docs/description`.
- `feature/<short-description>` * [ ] Never commit directly to the `main` branch.
- `bugfix/<short-description>` * [ ] Always open a pull request for code changes, even for small updates.
- `task/<short-description>` * [ ] Ensure your branch is up to date with `main` before opening a pull request.
- Never commit changes directly to the `main` or default branch. * [ ] Delete the branch after the pull request is merged.
- After completing changes, push the branch and open a pull request for review and merging.

11
.vscode/settings.json vendored
View File

@ -1,11 +1,14 @@
{ {
"chat.modeFilesLocations": { "chat.modeFilesLocations": {
"chatmodes": true "chatmodes": true,
".github/chatmodes": true
}, },
"chat.promptFilesLocations": { "chat.promptFilesLocations": {
"prompts": true "prompts": true,
".github/prompts": true
}, },
"chat.instructionsFilesLocations": { "chat.instructionsFilesLocations": {
"instructions": true "instructions": true,
".github/instructions": true
} }
} }

View File

@ -101,36 +101,40 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") {
}; };
// Import config manager for effective state computation // Import config manager for effective state computation
const { computeEffectiveItemStates } = require("./config-manager"); const { getEffectivelyEnabledItems } = require("./config-manager");
// Compute effective states using precedence rules // Get precomputed sets of effectively enabled items for O(1) performance
const effectiveStates = computeEffectiveItemStates(config); const effectivelyEnabledSets = getEffectivelyEnabledItems(config);
// Create sets of effectively enabled items for performance // Count effectively enabled collections for summary
const effectivelyEnabledSets = { // A collection is effectively enabled if it contributes any enabled items
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) { if (config.collections) {
for (const [collectionName, enabled] of Object.entries(config.collections)) { for (const [collectionName, configEnabled] of Object.entries(config.collections)) {
if (enabled) { if (configEnabled) {
const collectionPath = path.join(rootDir, "collections", `${collectionName}.collection.yml`); const collectionPath = path.join(rootDir, "collections", `${collectionName}.collection.yml`);
if (fs.existsSync(collectionPath)) { if (fs.existsSync(collectionPath)) {
const collection = parseCollectionYaml(collectionPath); const collection = parseCollectionYaml(collectionPath);
if (collection && collection.items) { if (collection && collection.items) {
summary.collections++; // Check if this collection contributes any effectively enabled items
console.log(`✓ Enabled collection: ${collectionName} (${collection.items.length} 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)`);
}
} }
} }
} }

View File

@ -149,8 +149,16 @@ function handleListCommand(rawArgs) {
const availableItems = getAllAvailableItems(section); const availableItems = getAllAvailableItems(section);
// Count effectively enabled items // Count effectively enabled items
const effectivelyEnabled = Object.values(effectiveStates[section] || {}) let effectivelyEnabled;
.filter(state => state.enabled).length; 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 { totalCharacters } = calculateSectionFootprint(section, sanitizedConfig[section]);
const headingParts = [ const headingParts = [
@ -170,20 +178,40 @@ function handleListCommand(rawArgs) {
// Show items with effective state and reason // Show items with effective state and reason
if (section === "collections") { if (section === "collections") {
// Collections show simple enabled/disabled // Collections show simple enabled/disabled with count of effectively enabled items
availableItems.forEach(itemName => { availableItems.forEach(itemName => {
const isEnabled = Boolean(sanitizedConfig[section]?.[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 { } else {
// Other sections show effective state with reason // Other sections show effective state with detailed reason
availableItems.forEach(itemName => { availableItems.forEach(itemName => {
const effectiveState = effectiveStates[section]?.[itemName]; const effectiveState = effectiveStates[section]?.[itemName];
if (effectiveState) { if (effectiveState) {
const symbol = effectiveState.enabled ? "✓" : " "; const symbol = effectiveState.enabled ? "✓" : " ";
const reasonText = effectiveState.reason === "explicit" let reasonText = "";
? ` (${effectiveState.reason})`
: effectiveState.enabled ? ` (${effectiveState.reason})` : ""; 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}`); console.log(` [${symbol}] ${itemName}${reasonText}`);
} else { } else {
console.log(` [ ] ${itemName}`); console.log(` [ ] ${itemName}`);

View File

@ -101,6 +101,11 @@ function toBoolean(value) {
if (normalized === "false") return false; if (normalized === "false") return false;
} }
// Preserve undefined as undefined for "no explicit override"
if (value === undefined) {
return undefined;
}
return Boolean(value); return Boolean(value);
} }
@ -185,11 +190,17 @@ function generateConfigHash(config) {
* Compute effective item states respecting explicit overrides over collections * Compute effective item states respecting explicit overrides over collections
* *
* This function builds membership maps per section and returns effectively enabled items with reasons. * This function builds membership maps per section and returns effectively enabled items with reasons.
* It uses the following precedence rules: * It uses strict comparisons to ensure undefined values are never treated as explicitly disabled.
* 1. Explicit true/false overrides everything (highest priority) *
* 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 * 2. If undefined and enabled by collection, use true
* 3. Otherwise, use false (disabled) * 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 * @param {Object} config - Configuration object with sections
* @returns {Object} Effective states for each section with { itemName: { enabled: boolean, reason: string } } * @returns {Object} Effective states for each section with { itemName: { enabled: boolean, reason: string } }
* Reason can be: 'explicit', 'collection', or 'disabled' * Reason can be: 'explicit', 'collection', or 'disabled'
@ -203,14 +214,21 @@ function computeEffectiveItemStates(config) {
chatmodes: {} chatmodes: {}
}; };
// Build membership maps: Map<itemName, Set<collectionName>> per section for O(1) lookups // Build detailed membership maps: Map<itemName, Set<collectionName>> per section
const collectionMemberships = {
prompts: new Map(),
instructions: new Map(),
chatmodes: new Map()
};
// Build simple enabled sets for O(1) lookups
const collectionEnabledItems = { const collectionEnabledItems = {
prompts: new Set(), prompts: new Set(),
instructions: new Set(), instructions: new Set(),
chatmodes: new Set() chatmodes: new Set()
}; };
// Identify enabled collections per section // Identify enabled collections per section and track memberships
if (config.collections) { if (config.collections) {
for (const [collectionName, enabled] of Object.entries(config.collections)) { for (const [collectionName, enabled] of Object.entries(config.collections)) {
if (enabled === true) { if (enabled === true) {
@ -222,12 +240,24 @@ function computeEffectiveItemStates(config) {
// Extract item name from path - remove directory and all extensions // Extract item name from path - remove directory and all extensions
const itemName = path.basename(item.path).replace(/\.(prompt|instructions|chatmode)\.md$/, ''); const itemName = path.basename(item.path).replace(/\.(prompt|instructions|chatmode)\.md$/, '');
let sectionName;
if (item.kind === "prompt") { if (item.kind === "prompt") {
collectionEnabledItems.prompts.add(itemName); sectionName = "prompts";
} else if (item.kind === "instruction") { } else if (item.kind === "instruction") {
collectionEnabledItems.instructions.add(itemName); sectionName = "instructions";
} else if (item.kind === "chat-mode") { } 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) { for (const itemName of availableItems) {
const explicitValue = sectionConfig[itemName]; const explicitValue = sectionConfig[itemName];
const isEnabledByCollection = collectionEnabled.has(itemName); const isEnabledByCollection = collectionEnabled.has(itemName);
const enablingCollections = collectionMemberships[section].get(itemName) || new Set();
// Precedence rules: // Precedence rules with strict undefined handling:
// 1. If explicitly set to true or false, use that value // 1. If explicitly set to true or false, use that value (highest priority)
// 2. If undefined and enabled by collection, use true // 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 enabled = false;
let reason = "disabled"; let reason = "disabled";
let collections = [];
if (explicitValue === true) { if (explicitValue === true) {
enabled = true; enabled = true;
reason = "explicit"; reason = "explicit";
} else if (explicitValue === false) { } else if (explicitValue === false) {
// Strict comparison ensures only explicit false disables items
enabled = false; enabled = false;
reason = "explicit"; reason = "explicit";
} else if (explicitValue === undefined && isEnabledByCollection) { } else if (explicitValue === undefined && isEnabledByCollection) {
// undefined values can be enabled by collections (not treated as disabled)
enabled = true; enabled = true;
reason = "collection"; 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; 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 = { module.exports = {
DEFAULT_CONFIG_PATH, DEFAULT_CONFIG_PATH,
CONFIG_SECTIONS, CONFIG_SECTIONS,
@ -316,5 +441,6 @@ module.exports = {
getAllAvailableItems, getAllAvailableItems,
computeEffectiveItemStates, computeEffectiveItemStates,
getEffectivelyEnabledItems, getEffectivelyEnabledItems,
generateConfigHash generateConfigHash,
toggleCollection
}; };

View File

@ -8,6 +8,7 @@ const { runTests: runUnitTests } = require('./test-effective-states');
const { runTests: runIntegrationTests } = require('./test-integration'); const { runTests: runIntegrationTests } = require('./test-integration');
const { runTests: runCliTests } = require('./test-cli'); const { runTests: runCliTests } = require('./test-cli');
const { runTests: runApplyTests } = require('./test-apply-effective'); const { runTests: runApplyTests } = require('./test-apply-effective');
const { runTests: runToggleCollectionTests } = require('./test-toggle-collection');
async function runAllTests() { async function runAllTests() {
console.log('🧪 Running Awesome Copilot Comprehensive Test Suite\n'); console.log('🧪 Running Awesome Copilot Comprehensive Test Suite\n');
@ -17,7 +18,8 @@ async function runAllTests() {
unit: false, unit: false,
integration: false, integration: false,
cli: false, cli: false,
apply: false apply: false,
toggleCollection: false
}; };
try { try {
@ -52,6 +54,14 @@ async function runAllTests() {
console.error('Apply tests failed with error:', error.message); 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 { try {
console.log('\n🤖 Repository Instructions Tests'); console.log('\n🤖 Repository Instructions Tests');
console.log('-'.repeat(33)); console.log('-'.repeat(33));
@ -71,6 +81,7 @@ async function runAllTests() {
{ name: 'Integration Tests', result: results.integration, emoji: '🔄' }, { name: 'Integration Tests', result: results.integration, emoji: '🔄' },
{ name: 'CLI Tests', result: results.cli, emoji: '⌨️' }, { name: 'CLI Tests', result: results.cli, emoji: '⌨️' },
{ name: 'Apply Tests', result: results.apply, emoji: '🎯' }, { name: 'Apply Tests', result: results.apply, emoji: '🎯' },
{ name: 'Toggle Collection', result: results.toggleCollection, emoji: '🔧' },
{ name: 'Repo Instructions', result: results.repoInstructions, emoji: '🤖' } { name: 'Repo Instructions', result: results.repoInstructions, emoji: '🤖' }
]; ];

View File

@ -92,7 +92,7 @@ async function runTests() {
const result = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); const result = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`);
assert(result.success, 'List should succeed'); 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'); assert(result.stdout.includes('[✓]'), 'Should show enabled items');
}); });
@ -105,7 +105,7 @@ async function runTests() {
assert(result.success, 'Individual toggle should succeed'); assert(result.success, 'Individual toggle should succeed');
const listResult = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); 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'); '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'); 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`); console.log(`\nCLI Test Results: ${passedTests}/${totalTests} passed`);
cleanup(); // Final cleanup cleanup(); // Final cleanup

View File

@ -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:

View File

@ -164,7 +164,50 @@ function runTests() {
'Chat mode should be enabled by collection'); '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", () => { test("getEffectivelyEnabledItems returns Sets format", () => {
const config = { const config = {
prompts: { prompts: {
@ -214,6 +257,37 @@ function runTests() {
assert(result.prompts.size > 0, 'Should have enabled prompts'); 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`); console.log(`\nTest Results: ${passedTests}/${totalTests} passed`);
if (passedTests === totalTests) { if (passedTests === totalTests) {

View File

@ -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

View File

@ -119,14 +119,14 @@ async function runTests() {
// Check it's explicitly disabled // Check it's explicitly disabled
const listResult1 = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); 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 // Force enable all with --all flag
await runCommand(`node awesome-copilot.js toggle prompts all on --all --config ${TEST_CONFIG}`); await runCommand(`node awesome-copilot.js toggle prompts all on --all --config ${TEST_CONFIG}`);
// Check it's now explicitly enabled // Check it's now explicitly enabled
const listResult2 = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); 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 // Test 4: File cleanup on disable
@ -180,7 +180,7 @@ async function runTests() {
// Double-check with list command // Double-check with list command
const listResult = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); 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)"); assert(!listResult.stdout.includes('[✓] playwright-generate-test'), "'[✓] playwright-generate-test' should NOT be present in the list output (should remain explicitly disabled)");
}); });

194
test-toggle-collection.js Normal file
View File

@ -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 };