/** * Tests for --with / --without selective install flags (issue #470) * * Covers: * - CLI argument parsing for --with and --without * - Request normalization with include/exclude component IDs * - Component-to-module expansion via the manifest catalog * - End-to-end install plans with --with and --without * - Validation and error handling for unknown component IDs * - Combined --profile + --with + --without flows * - Standalone --with without a profile * - agent: and skill: component families */ const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { parseInstallArgs, normalizeInstallRequest, } = require('../../scripts/lib/install/request'); const { loadInstallManifests, listInstallComponents, resolveInstallPlan, } = require('../../scripts/lib/install-manifests'); function test(name, fn) { try { fn(); console.log(` \u2713 ${name}`); return true; } catch (error) { console.log(` \u2717 ${name}`); console.log(` Error: ${error.message}`); return false; } } function runTests() { console.log('\n=== Testing --with / --without selective install flags ===\n'); let passed = 0; let failed = 0; // ─── CLI Argument Parsing ─── if (test('parses single --with flag', () => { const parsed = parseInstallArgs([ 'node', 'install-apply.js', '--profile', 'core', '--with', 'lang:typescript', ]); assert.deepStrictEqual(parsed.includeComponentIds, ['lang:typescript']); assert.deepStrictEqual(parsed.excludeComponentIds, []); })) passed++; else failed++; if (test('parses single --without flag', () => { const parsed = parseInstallArgs([ 'node', 'install-apply.js', '--profile', 'developer', '--without', 'capability:orchestration', ]); assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:orchestration']); assert.deepStrictEqual(parsed.includeComponentIds, []); })) passed++; else failed++; if (test('parses multiple --with flags', () => { const parsed = parseInstallArgs([ 'node', 'install-apply.js', '--with', 'lang:typescript', '--with', 'framework:nextjs', '--with', 'capability:database', ]); assert.deepStrictEqual(parsed.includeComponentIds, [ 'lang:typescript', 'framework:nextjs', 'capability:database', ]); })) passed++; else failed++; if (test('parses multiple --without flags', () => { const parsed = parseInstallArgs([ 'node', 'install-apply.js', '--profile', 'full', '--without', 'capability:media', '--without', 'capability:social', ]); assert.deepStrictEqual(parsed.excludeComponentIds, [ 'capability:media', 'capability:social', ]); })) passed++; else failed++; if (test('parses combined --with and --without flags', () => { const parsed = parseInstallArgs([ 'node', 'install-apply.js', '--profile', 'developer', '--with', 'lang:typescript', '--with', 'framework:nextjs', '--without', 'capability:orchestration', ]); assert.strictEqual(parsed.profileId, 'developer'); assert.deepStrictEqual(parsed.includeComponentIds, ['lang:typescript', 'framework:nextjs']); assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:orchestration']); })) passed++; else failed++; if (test('ignores empty --with values', () => { const parsed = parseInstallArgs([ 'node', 'install-apply.js', '--with', '', '--with', 'lang:python', ]); assert.deepStrictEqual(parsed.includeComponentIds, ['lang:python']); })) passed++; else failed++; if (test('ignores empty --without values', () => { const parsed = parseInstallArgs([ 'node', 'install-apply.js', '--profile', 'core', '--without', '', '--without', 'capability:media', ]); assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:media']); })) passed++; else failed++; // ─── Request Normalization ─── if (test('normalizes --with-only request as manifest mode', () => { const request = normalizeInstallRequest({ target: 'claude', profileId: null, moduleIds: [], includeComponentIds: ['lang:typescript'], excludeComponentIds: [], languages: [], }); assert.strictEqual(request.mode, 'manifest'); assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript']); assert.deepStrictEqual(request.excludeComponentIds, []); })) passed++; else failed++; if (test('normalizes --profile + --with + --without as manifest mode', () => { const request = normalizeInstallRequest({ target: 'cursor', profileId: 'developer', moduleIds: [], includeComponentIds: ['lang:typescript', 'framework:nextjs'], excludeComponentIds: ['capability:orchestration'], languages: [], }); assert.strictEqual(request.mode, 'manifest'); assert.strictEqual(request.profileId, 'developer'); assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript', 'framework:nextjs']); assert.deepStrictEqual(request.excludeComponentIds, ['capability:orchestration']); })) passed++; else failed++; if (test('rejects --with combined with legacy language arguments', () => { assert.throws( () => normalizeInstallRequest({ target: 'claude', profileId: null, moduleIds: [], includeComponentIds: ['lang:typescript'], excludeComponentIds: [], languages: ['python'], }), /cannot be combined/ ); })) passed++; else failed++; if (test('rejects --without combined with legacy language arguments', () => { assert.throws( () => normalizeInstallRequest({ target: 'claude', profileId: null, moduleIds: [], includeComponentIds: [], excludeComponentIds: ['capability:media'], languages: ['typescript'], }), /cannot be combined/ ); })) passed++; else failed++; if (test('deduplicates repeated --with component IDs', () => { const request = normalizeInstallRequest({ target: 'claude', profileId: null, moduleIds: [], includeComponentIds: ['lang:typescript', 'lang:typescript', 'lang:python'], excludeComponentIds: [], languages: [], }); assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript', 'lang:python']); })) passed++; else failed++; if (test('deduplicates repeated --without component IDs', () => { const request = normalizeInstallRequest({ target: 'claude', profileId: 'full', moduleIds: [], includeComponentIds: [], excludeComponentIds: ['capability:media', 'capability:media', 'capability:social'], languages: [], }); assert.deepStrictEqual(request.excludeComponentIds, ['capability:media', 'capability:social']); })) passed++; else failed++; // ─── Component Catalog Validation ─── if (test('component catalog includes lang: family entries', () => { const components = listInstallComponents({ family: 'language' }); assert.ok(components.some(c => c.id === 'lang:typescript'), 'Should have lang:typescript'); assert.ok(components.some(c => c.id === 'lang:python'), 'Should have lang:python'); assert.ok(components.some(c => c.id === 'lang:go'), 'Should have lang:go'); assert.ok(components.some(c => c.id === 'lang:java'), 'Should have lang:java'); })) passed++; else failed++; if (test('component catalog includes framework: family entries', () => { const components = listInstallComponents({ family: 'framework' }); assert.ok(components.some(c => c.id === 'framework:react'), 'Should have framework:react'); assert.ok(components.some(c => c.id === 'framework:nextjs'), 'Should have framework:nextjs'); assert.ok(components.some(c => c.id === 'framework:django'), 'Should have framework:django'); assert.ok(components.some(c => c.id === 'framework:springboot'), 'Should have framework:springboot'); })) passed++; else failed++; if (test('component catalog includes capability: family entries', () => { const components = listInstallComponents({ family: 'capability' }); assert.ok(components.some(c => c.id === 'capability:database'), 'Should have capability:database'); assert.ok(components.some(c => c.id === 'capability:security'), 'Should have capability:security'); assert.ok(components.some(c => c.id === 'capability:orchestration'), 'Should have capability:orchestration'); })) passed++; else failed++; if (test('component catalog includes agent: family entries', () => { const components = listInstallComponents({ family: 'agent' }); assert.ok(components.length > 0, 'Should have at least one agent component'); assert.ok(components.some(c => c.id === 'agent:security-reviewer'), 'Should have agent:security-reviewer'); })) passed++; else failed++; if (test('component catalog includes skill: family entries', () => { const components = listInstallComponents({ family: 'skill' }); assert.ok(components.length > 0, 'Should have at least one skill component'); assert.ok(components.some(c => c.id === 'skill:continuous-learning'), 'Should have skill:continuous-learning'); })) passed++; else failed++; // ─── Install Plan Resolution with --with ─── if (test('--with alone resolves component modules and their dependencies', () => { const plan = resolveInstallPlan({ includeComponentIds: ['lang:typescript'], target: 'claude', }); assert.ok(plan.selectedModuleIds.includes('framework-language'), 'Should include the module behind lang:typescript'); assert.ok(plan.selectedModuleIds.includes('rules-core'), 'Should include framework-language dependency rules-core'); assert.ok(plan.selectedModuleIds.includes('platform-configs'), 'Should include framework-language dependency platform-configs'); })) passed++; else failed++; if (test('--with adds modules on top of a profile', () => { const plan = resolveInstallPlan({ profileId: 'core', includeComponentIds: ['capability:security'], target: 'claude', }); // core profile modules assert.ok(plan.selectedModuleIds.includes('rules-core')); assert.ok(plan.selectedModuleIds.includes('workflow-quality')); // added by --with assert.ok(plan.selectedModuleIds.includes('security'), 'Should include security module from --with'); })) passed++; else failed++; if (test('multiple --with flags union their modules', () => { const plan = resolveInstallPlan({ includeComponentIds: ['lang:typescript', 'capability:database'], target: 'claude', }); assert.ok(plan.selectedModuleIds.includes('framework-language'), 'Should include framework-language from lang:typescript'); assert.ok(plan.selectedModuleIds.includes('database'), 'Should include database from capability:database'); })) passed++; else failed++; // ─── Install Plan Resolution with --without ─── if (test('--without excludes modules from a profile', () => { const plan = resolveInstallPlan({ profileId: 'developer', excludeComponentIds: ['capability:orchestration'], target: 'claude', }); assert.ok(!plan.selectedModuleIds.includes('orchestration'), 'Should exclude orchestration module'); assert.ok(plan.excludedModuleIds.includes('orchestration'), 'Should report orchestration as excluded'); // rest of developer profile should remain assert.ok(plan.selectedModuleIds.includes('rules-core')); assert.ok(plan.selectedModuleIds.includes('framework-language')); assert.ok(plan.selectedModuleIds.includes('database')); })) passed++; else failed++; if (test('multiple --without flags exclude multiple modules', () => { const plan = resolveInstallPlan({ profileId: 'full', excludeComponentIds: ['capability:media', 'capability:social', 'capability:supply-chain'], target: 'claude', }); assert.ok(!plan.selectedModuleIds.includes('media-generation')); assert.ok(!plan.selectedModuleIds.includes('social-distribution')); assert.ok(!plan.selectedModuleIds.includes('supply-chain-domain')); assert.ok(plan.excludedModuleIds.includes('media-generation')); assert.ok(plan.excludedModuleIds.includes('social-distribution')); assert.ok(plan.excludedModuleIds.includes('supply-chain-domain')); })) passed++; else failed++; // ─── Combined --with + --without ─── if (test('--with and --without work together on a profile', () => { const plan = resolveInstallPlan({ profileId: 'developer', includeComponentIds: ['capability:security'], excludeComponentIds: ['capability:orchestration'], target: 'claude', }); assert.ok(plan.selectedModuleIds.includes('security'), 'Should include security from --with'); assert.ok(!plan.selectedModuleIds.includes('orchestration'), 'Should exclude orchestration from --without'); assert.ok(plan.selectedModuleIds.includes('rules-core'), 'Should keep profile base modules'); })) passed++; else failed++; if (test('--without on a dependency of --with raises an error', () => { assert.throws( () => resolveInstallPlan({ includeComponentIds: ['capability:social'], excludeComponentIds: ['capability:content'], }), /depends on excluded module/ ); })) passed++; else failed++; // ─── Validation Errors ─── if (test('throws for unknown component ID in --with', () => { assert.throws( () => resolveInstallPlan({ includeComponentIds: ['lang:brainfuck-plus-plus'], }), /Unknown install component/ ); })) passed++; else failed++; if (test('throws for unknown component ID in --without', () => { assert.throws( () => resolveInstallPlan({ profileId: 'core', excludeComponentIds: ['capability:teleportation'], }), /Unknown install component/ ); })) passed++; else failed++; if (test('throws when all modules are excluded', () => { assert.throws( () => resolveInstallPlan({ profileId: 'core', excludeComponentIds: [ 'baseline:rules', 'baseline:agents', 'baseline:commands', 'baseline:hooks', 'baseline:platform', 'baseline:workflow', ], target: 'claude', }), /excludes every requested install module/ ); })) passed++; else failed++; // ─── Target-Specific Behavior ─── if (test('--with respects target compatibility filtering', () => { const plan = resolveInstallPlan({ includeComponentIds: ['capability:orchestration'], target: 'cursor', }); // orchestration module only supports claude, codex, opencode assert.ok(!plan.selectedModuleIds.includes('orchestration'), 'Should skip orchestration for cursor target'); assert.ok(plan.skippedModuleIds.includes('orchestration'), 'Should report orchestration as skipped for cursor'); })) passed++; else failed++; if (test('--without with agent: component excludes the agent module', () => { const plan = resolveInstallPlan({ profileId: 'core', excludeComponentIds: ['agent:security-reviewer'], target: 'claude', }); // agent:security-reviewer maps to agents-core module // Since core profile includes agents-core and it is excluded, it should be gone assert.ok(!plan.selectedModuleIds.includes('agents-core'), 'Should exclude agents-core when agent:security-reviewer is excluded'); assert.ok(plan.excludedModuleIds.includes('agents-core'), 'Should report agents-core as excluded'); })) passed++; else failed++; if (test('--with agent: component includes the agents-core module', () => { const plan = resolveInstallPlan({ includeComponentIds: ['agent:security-reviewer'], target: 'claude', }); assert.ok(plan.selectedModuleIds.includes('agents-core'), 'Should include agents-core module from agent:security-reviewer'); })) passed++; else failed++; if (test('--with skill: component includes the parent skill module', () => { const plan = resolveInstallPlan({ includeComponentIds: ['skill:continuous-learning'], target: 'claude', }); assert.ok(plan.selectedModuleIds.includes('workflow-quality'), 'Should include workflow-quality module from skill:continuous-learning'); })) passed++; else failed++; // ─── Help Text ─── if (test('help text documents --with and --without flags', () => { const { execFileSync } = require('child_process'); const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js'); const result = execFileSync('node', [scriptPath, '--help'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], }); assert.ok(result.includes('--with'), 'Help should mention --with'); assert.ok(result.includes('--without'), 'Help should mention --without'); assert.ok(result.includes('component'), 'Help should describe components'); })) passed++; else failed++; // ─── End-to-End Dry-Run ─── if (test('end-to-end: --profile developer --with capability:security --without capability:orchestration --dry-run', () => { const { execFileSync } = require('child_process'); const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js'); const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-')); const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-project-')); try { const result = execFileSync('node', [ scriptPath, '--profile', 'developer', '--with', 'capability:security', '--without', 'capability:orchestration', '--dry-run', ], { cwd: projectDir, env: { ...process.env, HOME: homeDir }, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], }); assert.ok(result.includes('Mode: manifest'), 'Should be manifest mode'); assert.ok(result.includes('Profile: developer'), 'Should show developer profile'); assert.ok(result.includes('capability:security'), 'Should show included component'); assert.ok(result.includes('capability:orchestration'), 'Should show excluded component'); assert.ok(result.includes('security'), 'Selected modules should include security'); } finally { fs.rmSync(homeDir, { recursive: true, force: true }); fs.rmSync(projectDir, { recursive: true, force: true }); } })) passed++; else failed++; if (test('end-to-end: --with lang:python --with agent:security-reviewer --dry-run', () => { const { execFileSync } = require('child_process'); const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js'); const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-')); const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-project-')); try { const result = execFileSync('node', [ scriptPath, '--with', 'lang:python', '--with', 'agent:security-reviewer', '--dry-run', ], { cwd: projectDir, env: { ...process.env, HOME: homeDir }, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], }); assert.ok(result.includes('Mode: manifest'), 'Should be manifest mode'); assert.ok(result.includes('lang:python'), 'Should show lang:python as included'); assert.ok(result.includes('agent:security-reviewer'), 'Should show agent:security-reviewer as included'); } finally { fs.rmSync(homeDir, { recursive: true, force: true }); fs.rmSync(projectDir, { recursive: true, force: true }); } })) passed++; else failed++; if (test('end-to-end: --with with unknown component fails cleanly', () => { const { execFileSync } = require('child_process'); const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js'); let exitCode = 0; let stderr = ''; try { execFileSync('node', [ scriptPath, '--with', 'lang:nonexistent-language', '--dry-run', ], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], }); } catch (error) { exitCode = error.status || 1; stderr = error.stderr || ''; } assert.strictEqual(exitCode, 1, 'Should exit with error code 1'); assert.ok(stderr.includes('Unknown install component'), 'Should report unknown component'); })) passed++; else failed++; if (test('end-to-end: --without with unknown component fails cleanly', () => { const { execFileSync } = require('child_process'); const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js'); let exitCode = 0; let stderr = ''; try { execFileSync('node', [ scriptPath, '--profile', 'core', '--without', 'capability:nonexistent', '--dry-run', ], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], }); } catch (error) { exitCode = error.status || 1; stderr = error.stderr || ''; } assert.strictEqual(exitCode, 1, 'Should exit with error code 1'); assert.ok(stderr.includes('Unknown install component'), 'Should report unknown component'); })) passed++; else failed++; // ─── End-to-End Actual Install ─── if (test('end-to-end: installs --profile core --with capability:security and writes state', () => { const { execFileSync } = require('child_process'); const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js'); const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-')); const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-project-')); try { const result = execFileSync('node', [ scriptPath, '--profile', 'core', '--with', 'capability:security', ], { cwd: projectDir, env: { ...process.env, HOME: homeDir }, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], }); const claudeRoot = path.join(homeDir, '.claude'); // Security skill should be installed (from --with) assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'security-review', 'SKILL.md')), 'Should install security-review skill from --with'); // Core profile modules should be installed assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')), 'Should install core rules'); // Install state should record include/exclude const statePath = path.join(claudeRoot, 'ecc', 'install-state.json'); const state = JSON.parse(fs.readFileSync(statePath, 'utf8')); assert.strictEqual(state.request.profile, 'core'); assert.deepStrictEqual(state.request.includeComponents, ['capability:security']); assert.deepStrictEqual(state.request.excludeComponents, []); assert.ok(state.resolution.selectedModules.includes('security')); } finally { fs.rmSync(homeDir, { recursive: true, force: true }); fs.rmSync(projectDir, { recursive: true, force: true }); } })) passed++; else failed++; if (test('end-to-end: installs --profile developer --without capability:orchestration and state reflects exclusion', () => { const { execFileSync } = require('child_process'); const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js'); const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-')); const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-project-')); try { execFileSync('node', [ scriptPath, '--profile', 'developer', '--without', 'capability:orchestration', ], { cwd: projectDir, env: { ...process.env, HOME: homeDir }, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], }); const claudeRoot = path.join(homeDir, '.claude'); // Orchestration skills should NOT be installed (from --without) assert.ok(!fs.existsSync(path.join(claudeRoot, 'skills', 'dmux-workflows', 'SKILL.md')), 'Should not install orchestration skills'); // Developer profile base modules should be installed assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')), 'Should install core rules'); assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'tdd-workflow', 'SKILL.md')), 'Should install workflow skills'); const statePath = path.join(claudeRoot, 'ecc', 'install-state.json'); const state = JSON.parse(fs.readFileSync(statePath, 'utf8')); assert.strictEqual(state.request.profile, 'developer'); assert.deepStrictEqual(state.request.excludeComponents, ['capability:orchestration']); assert.ok(!state.resolution.selectedModules.includes('orchestration')); } finally { fs.rmSync(homeDir, { recursive: true, force: true }); fs.rmSync(projectDir, { recursive: true, force: true }); } })) passed++; else failed++; if (test('end-to-end: --with alone (no profile) installs just the component modules', () => { const { execFileSync } = require('child_process'); const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js'); const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-')); const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-project-')); try { execFileSync('node', [ scriptPath, '--with', 'lang:typescript', ], { cwd: projectDir, env: { ...process.env, HOME: homeDir }, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], }); const claudeRoot = path.join(homeDir, '.claude'); // framework-language skill (from lang:typescript) should be installed assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'coding-standards', 'SKILL.md')), 'Should install framework-language skills'); // Its dependencies should be installed assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')), 'Should install dependency rules-core'); const statePath = path.join(claudeRoot, 'ecc', 'install-state.json'); const state = JSON.parse(fs.readFileSync(statePath, 'utf8')); assert.strictEqual(state.request.profile, null); assert.deepStrictEqual(state.request.includeComponents, ['lang:typescript']); assert.ok(state.resolution.selectedModules.includes('framework-language')); } finally { fs.rmSync(homeDir, { recursive: true, force: true }); fs.rmSync(projectDir, { recursive: true, force: true }); } })) passed++; else failed++; // ─── JSON output mode ─── if (test('end-to-end: --dry-run --json includes component selections in output', () => { const { execFileSync } = require('child_process'); const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js'); const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-')); const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-project-')); try { const output = execFileSync('node', [ scriptPath, '--profile', 'core', '--with', 'capability:database', '--without', 'baseline:hooks', '--dry-run', '--json', ], { cwd: projectDir, env: { ...process.env, HOME: homeDir }, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], }); const json = JSON.parse(output); assert.strictEqual(json.dryRun, true); assert.ok(json.plan, 'Should include plan object'); assert.ok( json.plan.includedComponentIds.includes('capability:database'), 'JSON output should include capability:database in included components' ); assert.ok( json.plan.excludedComponentIds.includes('baseline:hooks'), 'JSON output should include baseline:hooks in excluded components' ); } finally { fs.rmSync(homeDir, { recursive: true, force: true }); fs.rmSync(projectDir, { recursive: true, force: true }); } })) passed++; else failed++; console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); } runTests();