/** * Tests for scripts/install-apply.js */ const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { execFileSync } = require('child_process'); const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js'); function createTempDir(prefix) { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); } function cleanup(dirPath) { fs.rmSync(dirPath, { recursive: true, force: true }); } function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } function run(args = [], options = {}) { const env = { ...process.env, HOME: options.homeDir || process.env.HOME, ...(options.env || {}), }; try { const stdout = execFileSync('node', [SCRIPT, ...args], { cwd: options.cwd, env, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000, }); return { code: 0, stdout, stderr: '' }; } catch (error) { return { code: error.status || 1, stdout: error.stdout || '', stderr: error.stderr || '', }; } } 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 install-apply.js ===\n'); let passed = 0; let failed = 0; if (test('shows help with --help', () => { const result = run(['--help']); assert.strictEqual(result.code, 0); assert.ok(result.stdout.includes('Usage:')); assert.ok(result.stdout.includes('--dry-run')); assert.ok(result.stdout.includes('--profile ')); assert.ok(result.stdout.includes('--modules ')); })) passed++; else failed++; if (test('rejects mixing legacy languages with manifest profile flags', () => { const result = run(['--profile', 'core', 'typescript']); assert.strictEqual(result.code, 1); assert.ok(result.stderr.includes('cannot be combined')); })) passed++; else failed++; if (test('installs Claude rules and writes install-state', () => { const homeDir = createTempDir('install-apply-home-'); const projectDir = createTempDir('install-apply-project-'); try { const result = run(['typescript'], { cwd: projectDir, homeDir }); assert.strictEqual(result.code, 0, result.stderr); const claudeRoot = path.join(homeDir, '.claude'); assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'typescript', 'testing.md'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'commands', 'plan.md'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'scripts', 'hooks', 'session-end.js'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'tdd-workflow', 'SKILL.md'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'coding-standards', 'SKILL.md'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'plugin.json'))); const statePath = path.join(homeDir, '.claude', 'ecc', 'install-state.json'); const state = readJson(statePath); assert.strictEqual(state.target.id, 'claude-home'); assert.deepStrictEqual(state.request.legacyLanguages, ['typescript']); assert.strictEqual(state.request.legacyMode, true); assert.deepStrictEqual(state.request.modules, []); assert.ok(state.resolution.selectedModules.includes('rules-core')); assert.ok(state.resolution.selectedModules.includes('framework-language')); assert.ok( state.operations.some(operation => ( operation.destinationPath === path.join(claudeRoot, 'rules', 'common', 'coding-style.md') )), 'Should record common rule file operation' ); } finally { cleanup(homeDir); cleanup(projectDir); } })) passed++; else failed++; if (test('installs Cursor configs and writes install-state', () => { const homeDir = createTempDir('install-apply-home-'); const projectDir = createTempDir('install-apply-project-'); try { const result = run(['--target', 'cursor', 'typescript'], { cwd: projectDir, homeDir }); assert.strictEqual(result.code, 0, result.stderr); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common', 'coding-style.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'typescript', 'testing.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'agents', 'architect.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'commands', 'plan.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks', 'session-start.js'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'skills', 'tdd-workflow', 'SKILL.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'skills', 'coding-standards', 'SKILL.md'))); const statePath = path.join(projectDir, '.cursor', 'ecc-install-state.json'); const state = readJson(statePath); const normalizedProjectDir = fs.realpathSync(projectDir); assert.strictEqual(state.target.id, 'cursor-project'); assert.strictEqual(state.target.root, path.join(normalizedProjectDir, '.cursor')); assert.deepStrictEqual(state.request.legacyLanguages, ['typescript']); assert.strictEqual(state.request.legacyMode, true); assert.ok(state.resolution.selectedModules.includes('framework-language')); assert.ok( state.operations.some(operation => ( operation.destinationPath === path.join(normalizedProjectDir, '.cursor', 'commands', 'plan.md') )), 'Should record manifest command file copy operation' ); } finally { cleanup(homeDir); cleanup(projectDir); } })) passed++; else failed++; if (test('installs Antigravity configs and writes install-state', () => { const homeDir = createTempDir('install-apply-home-'); const projectDir = createTempDir('install-apply-project-'); try { const result = run(['--target', 'antigravity', 'typescript'], { cwd: projectDir, homeDir }); assert.strictEqual(result.code, 0, result.stderr); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'common', 'coding-style.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'typescript', 'testing.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'commands', 'plan.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'agents', 'architect.md'))); const statePath = path.join(projectDir, '.agent', 'ecc-install-state.json'); const state = readJson(statePath); assert.strictEqual(state.target.id, 'antigravity-project'); assert.deepStrictEqual(state.request.legacyLanguages, ['typescript']); assert.strictEqual(state.request.legacyMode, true); assert.deepStrictEqual(state.resolution.selectedModules, ['rules-core', 'agents-core', 'commands-core']); assert.ok( state.operations.some(operation => ( operation.destinationPath.endsWith(path.join('.agent', 'commands', 'plan.md')) )), 'Should record manifest command file copy operation' ); } finally { cleanup(homeDir); cleanup(projectDir); } })) passed++; else failed++; if (test('supports dry-run without mutating the target project', () => { const homeDir = createTempDir('install-apply-home-'); const projectDir = createTempDir('install-apply-project-'); try { const result = run(['--target', 'cursor', '--dry-run', 'typescript'], { cwd: projectDir, homeDir, }); assert.strictEqual(result.code, 0, result.stderr); assert.ok(result.stdout.includes('Dry-run install plan')); assert.ok(result.stdout.includes('Mode: legacy-compat')); assert.ok(result.stdout.includes('Legacy languages: typescript')); assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json'))); assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'ecc-install-state.json'))); } finally { cleanup(homeDir); cleanup(projectDir); } })) passed++; else failed++; if (test('supports manifest profile dry-runs through the installer', () => { const homeDir = createTempDir('install-apply-home-'); const projectDir = createTempDir('install-apply-project-'); try { const result = run(['--profile', 'core', '--dry-run'], { cwd: projectDir, homeDir }); assert.strictEqual(result.code, 0, result.stderr); assert.ok(result.stdout.includes('Mode: manifest')); assert.ok(result.stdout.includes('Profile: core')); assert.ok(result.stdout.includes('Included components: (none)')); assert.ok(result.stdout.includes('Selected modules: rules-core, agents-core, commands-core, hooks-runtime, platform-configs, workflow-quality')); assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'ecc', 'install-state.json'))); } finally { cleanup(homeDir); cleanup(projectDir); } })) passed++; else failed++; if (test('installs manifest profiles and writes non-legacy install-state', () => { const homeDir = createTempDir('install-apply-home-'); const projectDir = createTempDir('install-apply-project-'); try { const result = run(['--profile', 'core'], { cwd: projectDir, homeDir }); assert.strictEqual(result.code, 0, result.stderr); const claudeRoot = path.join(homeDir, '.claude'); assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'agents', 'architect.md'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'commands', 'plan.md'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'hooks', 'hooks.json'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'scripts', 'hooks', 'session-end.js'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'plugin.json'))); const state = readJson(path.join(claudeRoot, 'ecc', 'install-state.json')); assert.strictEqual(state.request.profile, 'core'); assert.strictEqual(state.request.legacyMode, false); assert.deepStrictEqual(state.request.legacyLanguages, []); assert.ok(state.resolution.selectedModules.includes('platform-configs')); assert.ok( state.operations.some(operation => ( operation.destinationPath === path.join(claudeRoot, 'commands', 'plan.md') )), 'Should record manifest-driven command file copy' ); } finally { cleanup(homeDir); cleanup(projectDir); } })) passed++; else failed++; if (test('installs antigravity manifest profiles while skipping incompatible modules', () => { const homeDir = createTempDir('install-apply-home-'); const projectDir = createTempDir('install-apply-project-'); try { const result = run(['--target', 'antigravity', '--profile', 'core'], { cwd: projectDir, homeDir }); assert.strictEqual(result.code, 0, result.stderr); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'common', 'coding-style.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'agents', 'architect.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'commands', 'plan.md'))); assert.ok(!fs.existsSync(path.join(projectDir, '.agent', 'skills', 'tdd-workflow', 'SKILL.md'))); const state = readJson(path.join(projectDir, '.agent', 'ecc-install-state.json')); assert.strictEqual(state.request.profile, 'core'); assert.strictEqual(state.request.legacyMode, false); assert.deepStrictEqual(state.resolution.selectedModules, ['rules-core', 'agents-core', 'commands-core']); assert.ok(state.resolution.skippedModules.includes('workflow-quality')); assert.ok(state.resolution.skippedModules.includes('platform-configs')); } finally { cleanup(homeDir); cleanup(projectDir); } })) passed++; else failed++; if (test('installs explicit modules for cursor using manifest operations', () => { const homeDir = createTempDir('install-apply-home-'); const projectDir = createTempDir('install-apply-project-'); try { const result = run(['--target', 'cursor', '--modules', 'platform-configs'], { cwd: projectDir, homeDir, }); assert.strictEqual(result.code, 0, result.stderr); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.md'))); const state = readJson(path.join(projectDir, '.cursor', 'ecc-install-state.json')); assert.strictEqual(state.request.profile, null); assert.deepStrictEqual(state.request.modules, ['platform-configs']); assert.deepStrictEqual(state.request.includeComponents, []); assert.deepStrictEqual(state.request.excludeComponents, []); assert.strictEqual(state.request.legacyMode, false); assert.ok(state.resolution.selectedModules.includes('platform-configs')); assert.ok( !state.operations.some(operation => operation.destinationPath.endsWith('ecc-install-state.json')), 'Manifest copy operations should not include generated install-state files' ); } finally { cleanup(homeDir); cleanup(projectDir); } })) passed++; else failed++; if (test('rejects unknown explicit manifest modules before resolution', () => { const result = run(['--modules', 'ghost-module']); assert.strictEqual(result.code, 1); assert.ok(result.stderr.includes('Unknown install module: ghost-module')); })) passed++; else failed++; if (test('installs from ecc-install.json and persists component selections', () => { const homeDir = createTempDir('install-apply-home-'); const projectDir = createTempDir('install-apply-project-'); const configPath = path.join(projectDir, 'ecc-install.json'); try { fs.writeFileSync(configPath, JSON.stringify({ version: 1, target: 'claude', profile: 'developer', include: ['capability:security'], exclude: ['capability:orchestration'], }, null, 2)); const result = run(['--config', configPath], { cwd: projectDir, homeDir }); assert.strictEqual(result.code, 0, result.stderr); assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'skills', 'security-review', 'SKILL.md'))); assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'skills', 'dmux-workflows', 'SKILL.md'))); const state = readJson(path.join(homeDir, '.claude', 'ecc', 'install-state.json')); assert.strictEqual(state.request.profile, 'developer'); assert.deepStrictEqual(state.request.includeComponents, ['capability:security']); assert.deepStrictEqual(state.request.excludeComponents, ['capability:orchestration']); assert.ok(state.resolution.selectedModules.includes('security')); assert.ok(!state.resolution.selectedModules.includes('orchestration')); } finally { cleanup(homeDir); cleanup(projectDir); } })) passed++; else failed++; console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); } runTests();