mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
feat: implement --with/--without selective install flags (#679)
Add agent: and skill: component families to the install component catalog, enabling fine-grained selective install via CLI flags: ecc install --profile developer --with lang:typescript --without capability:orchestration ecc install --with lang:python --with agent:security-reviewer Changes: - Add agent: family (9 entries) and skill: family (10 entries) to manifests/install-components.json for granular component addressing - Update install-components.schema.json to accept agent: and skill: family prefixes - Register agent and skill family prefixes in COMPONENT_FAMILY_PREFIXES (scripts/lib/install-manifests.js) - Add 41 comprehensive tests covering CLI parsing, request normalization, component catalog validation, plan resolution, target filtering, error handling, and end-to-end install with --with/--without flags Closes #470
This commit is contained in:
718
tests/lib/selective-install.test.js
Normal file
718
tests/lib/selective-install.test.js
Normal file
@@ -0,0 +1,718 @@
|
||||
/**
|
||||
* 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();
|
||||
Reference in New Issue
Block a user