/** * Direct tests for scripts/lib/install-executor.js. */ 'use strict'; const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { applyInstallPlan, createLegacyCompatInstallPlan, createLegacyInstallPlan, createManifestInstallPlan, listAvailableLanguages, } = require('../../scripts/lib/install-executor'); const REPO_ROOT = path.resolve(__dirname, '..', '..'); function createTempDir(prefix) { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); } function cleanup(dirPath) { fs.rmSync(dirPath, { recursive: true, force: true }); } function writeFile(root, relativePath, content = '') { const filePath = path.join(root, relativePath); fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, content, 'utf8'); return filePath; } function writeJson(root, relativePath, value) { writeFile(root, relativePath, `${JSON.stringify(value, null, 2)}\n`); } function operationFor(plan, suffix) { return plan.operations.find(operation => ( operation.destinationPath.endsWith(suffix) || operation.sourceRelativePath.split(path.sep).join('/').endsWith(suffix.split(path.sep).join('/')) )); } function writeLegacySourceFixture(root) { writeJson(root, 'package.json', { version: '9.8.7' }); writeFile(root, path.join('rules', 'common', 'coding-style.md'), '# Common\n'); writeFile(root, path.join('rules', 'common', 'nested', 'shared.md'), '# Shared\n'); writeFile(root, path.join('rules', 'common', 'node_modules', 'ignored.md'), '# Ignored\n'); writeFile(root, path.join('rules', 'common', '.git', 'ignored.md'), '# Ignored\n'); writeFile(root, path.join('rules', 'typescript', 'testing.md'), '# TS\n'); writeFile(root, path.join('rules', 'python', 'testing.md'), '# Python\n'); writeFile(root, path.join('.cursor', 'rules', 'common-style.md'), '# Cursor common\n'); writeFile(root, path.join('.cursor', 'rules', 'typescript-style.md'), '# Cursor TS\n'); writeFile(root, path.join('.cursor', 'rules', 'python-style.txt'), '# Not markdown\n'); writeFile(root, path.join('.cursor', 'agents', 'planner.md'), '# Planner\n'); writeFile(root, path.join('.cursor', 'skills', 'demo', 'SKILL.md'), '# Demo\n'); writeFile(root, path.join('.cursor', 'commands', 'plan.md'), '# Plan\n'); writeFile(root, path.join('.cursor', 'hooks', 'hook.js'), 'process.exit(0);\n'); writeJson(root, path.join('.cursor', 'hooks.json'), { version: 1, hooks: {} }); writeJson(root, '.mcp.json', { mcpServers: { github: { command: 'github-mcp' } } }); writeFile(root, path.join('commands', 'plan.md'), '# Plan\n'); writeFile(root, path.join('agents', 'architect.md'), '# Architect\n'); writeFile(root, path.join('skills', 'demo', 'SKILL.md'), '# Demo\n'); } function writeManifestSourceFixture(root) { writeJson(root, 'package.json', { version: '1.2.3' }); writeJson(root, path.join('manifests', 'install-modules.json'), { version: 7, modules: [ { id: 'fixture-core', kind: 'fixture', description: 'Fixture module', paths: [ 'src', 'standalone.txt', 'missing.txt', path.join('runtime', 'ecc', 'install-state.json'), '.claude-plugin', ], targets: ['claude'], dependencies: [], defaultInstall: true, cost: 'light', stability: 'stable', }, ], }); writeJson(root, path.join('manifests', 'install-profiles.json'), { version: 1, profiles: { minimal: { description: 'Minimal fixture profile', modules: ['fixture-core'], }, }, }); writeFile(root, path.join('src', 'app.js'), 'console.log("app");\n'); writeFile(root, path.join('src', 'nested', 'feature.js'), 'console.log("feature");\n'); writeFile(root, path.join('src', 'node_modules', 'ignored.js'), 'console.log("ignored");\n'); writeFile(root, path.join('src', '.git', 'ignored.js'), 'console.log("ignored");\n'); writeFile(root, path.join('src', 'nested', 'ecc-install-state.json'), '{}\n'); writeFile(root, 'standalone.txt', 'standalone\n'); writeFile(root, path.join('runtime', 'ecc', 'install-state.json'), '{}\n'); writeJson(root, path.join('.claude-plugin', 'plugin.json'), { name: 'fixture' }); } function test(name, fn) { try { fn(); console.log(` PASS ${name}`); return true; } catch (error) { console.log(` FAIL ${name}`); console.log(` Error: ${error.message}`); return false; } } function runTests() { console.log('\n=== Testing install-executor.js ===\n'); let passed = 0; let failed = 0; if (test('lists legacy and local rule languages while ignoring common', () => { const sourceRoot = createTempDir('install-executor-source-'); try { fs.mkdirSync(path.join(sourceRoot, 'rules', 'common'), { recursive: true }); fs.mkdirSync(path.join(sourceRoot, 'rules', 'zig'), { recursive: true }); const languages = listAvailableLanguages(sourceRoot); assert.ok(languages.includes('typescript')); assert.ok(languages.includes('zig')); assert.ok(!languages.includes('common')); assert.deepStrictEqual([...languages].sort(), languages); } finally { cleanup(sourceRoot); } })) passed++; else failed++; if (test('rejects unknown legacy install targets before planning', () => { assert.throws( () => createLegacyInstallPlan({ target: 'not-a-target' }), /Unknown install target: not-a-target/ ); })) passed++; else failed++; if (test('plans Claude legacy rules with warnings and state preview', () => { const sourceRoot = createTempDir('install-executor-source-'); const homeDir = createTempDir('install-executor-home-'); const projectRoot = createTempDir('install-executor-project-'); const claudeRulesDir = path.join(homeDir, 'custom-rules'); try { writeLegacySourceFixture(sourceRoot); writeFile(homeDir, path.join('custom-rules', 'existing.md'), '# Existing\n'); const plan = createLegacyInstallPlan({ sourceRoot, homeDir, projectRoot, claudeRulesDir, target: 'claude', languages: ['typescript', 'missing-lang', '../bad'], }); assert.strictEqual(plan.mode, 'legacy'); assert.strictEqual(plan.target, 'claude'); assert.strictEqual(plan.installRoot, claudeRulesDir); assert.ok(plan.warnings.some(warning => warning.includes('files may be overwritten'))); assert.ok(plan.warnings.some(warning => warning.includes("rules/missing-lang/ does not exist"))); assert.ok(plan.warnings.some(warning => warning.includes("Invalid language name '../bad'"))); assert.ok(operationFor(plan, path.join('custom-rules', 'common', 'coding-style.md'))); assert.ok(operationFor(plan, path.join('custom-rules', 'common', 'nested', 'shared.md'))); assert.ok(operationFor(plan, path.join('custom-rules', 'typescript', 'testing.md'))); assert.ok(!plan.operations.some(operation => operation.sourceRelativePath.includes('node_modules'))); assert.ok(!plan.operations.some(operation => operation.sourceRelativePath.includes('.git'))); assert.deepStrictEqual(plan.statePreview.request.legacyLanguages, ['typescript', 'missing-lang', '../bad']); assert.strictEqual(plan.statePreview.request.legacyMode, true); assert.strictEqual(plan.statePreview.source.repoVersion, '9.8.7'); assert.strictEqual(plan.statePreview.source.manifestVersion, 1); } finally { cleanup(sourceRoot); cleanup(homeDir); cleanup(projectRoot); } })) passed++; else failed++; if (test('plans Cursor legacy assets and JSON merge payloads', () => { const sourceRoot = createTempDir('install-executor-source-'); const projectRoot = createTempDir('install-executor-project-'); const homeDir = createTempDir('install-executor-home-'); try { writeLegacySourceFixture(sourceRoot); const plan = createLegacyInstallPlan({ sourceRoot, projectRoot, homeDir, target: 'cursor', languages: ['typescript', 'ruby', 'bad/name'], }); const targetRoot = path.join(projectRoot, '.cursor'); assert.strictEqual(plan.installRoot, targetRoot); assert.ok(operationFor(plan, path.join('.cursor', 'rules', 'common-style.md'))); assert.ok(operationFor(plan, path.join('.cursor', 'rules', 'typescript-style.md'))); assert.ok(operationFor(plan, path.join('.cursor', 'agents', 'planner.md'))); assert.ok(operationFor(plan, path.join('.cursor', 'skills', 'demo', 'SKILL.md'))); assert.ok(operationFor(plan, path.join('.cursor', 'commands', 'plan.md'))); assert.ok(operationFor(plan, path.join('.cursor', 'hooks', 'hook.js'))); assert.ok(operationFor(plan, path.join('.cursor', 'hooks.json'))); const mergeOperation = plan.operations.find(operation => operation.kind === 'merge-json'); assert.ok(mergeOperation, 'Should merge shared MCP config into Cursor'); assert.deepStrictEqual(mergeOperation.mergePayload.mcpServers.github.command, 'github-mcp'); assert.ok(plan.warnings.some(warning => warning.includes("No Cursor rules for 'ruby'"))); assert.ok(plan.warnings.some(warning => warning.includes("Invalid language name 'bad/name'"))); assert.strictEqual(plan.statePreview.target.id, 'cursor-project'); } finally { cleanup(sourceRoot); cleanup(projectRoot); cleanup(homeDir); } })) passed++; else failed++; if (test('surfaces invalid Cursor MCP JSON while planning legacy install', () => { const sourceRoot = createTempDir('install-executor-source-'); const projectRoot = createTempDir('install-executor-project-'); const homeDir = createTempDir('install-executor-home-'); try { writeLegacySourceFixture(sourceRoot); fs.writeFileSync(path.join(sourceRoot, '.mcp.json'), '[]\n', 'utf8'); assert.throws( () => createLegacyInstallPlan({ sourceRoot, projectRoot, homeDir, target: 'cursor' }), /Invalid \.mcp\.json/ ); } finally { cleanup(sourceRoot); cleanup(projectRoot); cleanup(homeDir); } })) passed++; else failed++; if (test('plans Antigravity legacy files with flattened rule names', () => { const sourceRoot = createTempDir('install-executor-source-'); const projectRoot = createTempDir('install-executor-project-'); const homeDir = createTempDir('install-executor-home-'); try { writeLegacySourceFixture(sourceRoot); writeFile(projectRoot, path.join('.agent', 'rules', 'existing.md'), '# Existing\n'); const plan = createLegacyInstallPlan({ sourceRoot, projectRoot, homeDir, target: 'antigravity', languages: ['typescript', 'missing-lang', 'bad/name'], }); assert.strictEqual(plan.installRoot, path.join(projectRoot, '.agent')); assert.ok(plan.warnings.some(warning => warning.includes('files may be overwritten'))); assert.ok(plan.warnings.some(warning => warning.includes("rules/missing-lang/ does not exist"))); assert.ok(plan.warnings.some(warning => warning.includes("Invalid language name 'bad/name'"))); assert.ok(operationFor(plan, path.join('.agent', 'rules', 'common-coding-style.md'))); assert.ok(operationFor(plan, path.join('.agent', 'rules', 'typescript-testing.md'))); assert.ok(operationFor(plan, path.join('.agent', 'workflows', 'plan.md'))); assert.ok(operationFor(plan, path.join('.agent', 'skills', 'architect.md'))); assert.ok(operationFor(plan, path.join('.agent', 'skills', 'demo', 'SKILL.md'))); assert.strictEqual(plan.statePreview.target.id, 'antigravity-project'); } finally { cleanup(sourceRoot); cleanup(projectRoot); cleanup(homeDir); } })) passed++; else failed++; if (test('materializes manifest scaffold operations and filters generated runtime state', () => { const sourceRoot = createTempDir('install-executor-source-'); const homeDir = createTempDir('install-executor-home-'); try { writeManifestSourceFixture(sourceRoot); const plan = createManifestInstallPlan({ sourceRoot, homeDir, target: 'claude', profileId: 'minimal', requestIncludeComponentIds: ['capability:fixture'], requestExcludeComponentIds: ['capability:skip'], warnings: ['fixture warning'], }); const normalizedSources = plan.operations.map(operation => ( operation.sourceRelativePath.split(path.sep).join('/') )); assert.ok(normalizedSources.includes('src/app.js')); assert.ok(normalizedSources.includes('src/nested/feature.js')); assert.ok(normalizedSources.includes('standalone.txt')); assert.ok(normalizedSources.includes('.claude-plugin/plugin.json')); assert.ok(!normalizedSources.includes('missing.txt')); assert.ok(!normalizedSources.includes('runtime/ecc/install-state.json')); assert.ok(!normalizedSources.includes('src/nested/ecc-install-state.json')); assert.ok(!normalizedSources.some(source => source.includes('node_modules'))); assert.ok(!normalizedSources.some(source => source.includes('.git'))); assert.ok(plan.operations.some(operation => ( operation.sourceRelativePath === path.join('.claude-plugin', 'plugin.json') && operation.destinationPath === path.join(homeDir, '.claude', 'plugin.json') ))); assert.deepStrictEqual(plan.warnings, ['fixture warning']); assert.strictEqual(plan.statePreview.request.profile, 'minimal'); assert.deepStrictEqual(plan.statePreview.request.includeComponents, ['capability:fixture']); assert.deepStrictEqual(plan.statePreview.request.excludeComponents, ['capability:skip']); assert.strictEqual(plan.statePreview.source.repoVersion, '1.2.3'); assert.strictEqual(plan.statePreview.source.manifestVersion, 7); } finally { cleanup(sourceRoot); cleanup(homeDir); } })) passed++; else failed++; if (test('creates legacy compatibility manifest plans from language selections', () => { const projectRoot = createTempDir('install-executor-project-'); const homeDir = createTempDir('install-executor-home-'); try { const plan = createLegacyCompatInstallPlan({ sourceRoot: REPO_ROOT, projectRoot, homeDir, target: 'cursor', legacyLanguages: ['rust'], }); assert.strictEqual(plan.mode, 'legacy-compat'); assert.deepStrictEqual(plan.legacyLanguages, ['rust']); assert.ok(plan.selectedModuleIds.includes('framework-language')); assert.strictEqual(plan.statePreview.request.legacyMode, true); assert.deepStrictEqual(plan.statePreview.request.legacyLanguages, ['rust']); assert.deepStrictEqual(plan.statePreview.request.modules, []); } finally { cleanup(projectRoot); cleanup(homeDir); } })) passed++; else failed++; if (test('applyInstallPlan re-export applies a manifest plan and writes install state', () => { const sourceRoot = createTempDir('install-executor-source-'); const homeDir = createTempDir('install-executor-home-'); try { writeManifestSourceFixture(sourceRoot); const plan = createManifestInstallPlan({ sourceRoot, homeDir, target: 'claude', profileId: 'minimal', }); const applied = applyInstallPlan(plan); assert.strictEqual(applied.applied, true); assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'src', 'app.js'))); assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'standalone.txt'))); assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'plugin.json'))); const state = JSON.parse(fs.readFileSync(path.join(homeDir, '.claude', 'ecc', 'install-state.json'), 'utf8')); assert.strictEqual(state.request.profile, 'minimal'); assert.deepStrictEqual(state.resolution.selectedModules, ['fixture-core']); } finally { cleanup(sourceRoot); cleanup(homeDir); } })) passed++; else failed++; console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); } runTests();