'use strict'; const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { applyInstallPlan } = require('../../scripts/lib/install/apply'); const { readInstallState, writeInstallState } = require('../../scripts/lib/install-state'); const { uninstallInstalledStates } = require('../../scripts/lib/install-lifecycle'); function createTempDir(prefix) { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); } function cleanup(dirPath) { fs.rmSync(dirPath, { recursive: true, force: true }); } function createOperation(moduleId, sourceRoot, sourceRelativePath, destinationPath) { return { kind: 'copy-file', moduleId, sourcePath: path.join(sourceRoot, sourceRelativePath), sourceRelativePath, destinationPath, strategy: 'preserve-relative-path', ownership: 'managed', scaffoldOnly: false, }; } function createFixture(options = {}) { const tempDir = createTempDir('claude-skill-migration-'); const homeDir = path.join(tempDir, 'home'); const projectRoot = path.join(tempDir, 'project'); const sourceRoot = path.join(tempDir, 'source'); const target = options.target || 'claude'; const targetRoot = target === 'claude' ? path.join(homeDir, '.claude') : path.join(projectRoot, '.claude'); const installStatePath = path.join(targetRoot, 'ecc', 'install-state.json'); const skillFiles = options.skillFiles || { 'SKILL.md': '# Current ECC skill\n', 'references/guide.md': '# Current ECC guide\n', }; for (const [relativePath, content] of Object.entries(skillFiles)) { const sourcePath = path.join(sourceRoot, 'skills', 'demo-skill', relativePath); fs.mkdirSync(path.dirname(sourcePath), { recursive: true }); fs.writeFileSync(sourcePath, content); } const operations = Object.keys(skillFiles).map(relativePath => createOperation( 'workflow-quality', sourceRoot, path.join('skills', 'demo-skill', relativePath), path.join(targetRoot, 'skills', 'demo-skill', relativePath) )); const statePreview = { schemaVersion: 'ecc.install.v1', installedAt: new Date().toISOString(), target: { id: target === 'claude' ? 'claude-home' : 'claude-project', target, kind: target === 'claude' ? 'home' : 'project', root: targetRoot, installStatePath, }, request: { profile: null, modules: ['workflow-quality'], includeComponents: [], excludeComponents: [], legacyLanguages: [], legacyMode: false, }, resolution: { selectedModules: ['workflow-quality'], skippedModules: [], }, source: { repoVersion: null, repoCommit: null, manifestVersion: 1, }, operations: operations.map(operation => ({ ...operation })), }; return { tempDir, homeDir, projectRoot, sourceRoot, target, targetRoot, installStatePath, operations, plan: { mode: 'manifest', target, adapter: { id: target === 'claude' ? 'claude-home' : 'claude-project', target, kind: target === 'claude' ? 'home' : 'project', }, targetRoot, installRoot: targetRoot, installStatePath, operations, statePreview, warnings: [], }, }; } function legacyDestinationPath(targetRoot, operation) { const sourceParts = operation.sourceRelativePath.split(path.sep); return path.join(targetRoot, 'skills', 'ecc', ...sourceParts.slice(1)); } function seedLegacyInstall(fixture, options = {}) { const legacyOperations = fixture.operations.map((operation, index) => { const destinationPath = legacyDestinationPath(fixture.targetRoot, operation); fs.mkdirSync(path.dirname(destinationPath), { recursive: true }); fs.writeFileSync(destinationPath, `# Legacy managed file ${index}\n`); return { ...operation, sourceRelativePath: options.windowsSourcePaths ? operation.sourceRelativePath.split(path.sep).join('\\') : operation.sourceRelativePath, destinationPath, }; }); writeInstallState(fixture.installStatePath, { ...fixture.plan.statePreview, operations: legacyOperations, }); return legacyOperations; } function runUninstall(fixture) { return uninstallInstalledStates({ homeDir: fixture.homeDir, projectRoot: fixture.projectRoot, targets: [fixture.target], }); } function test(name, fn) { try { fn(); console.log(` \u2713 ${name}`); return true; } catch (error) { console.log(` \u2717 ${name}`); console.log(` Error: ${error.stack || error.message}`); return false; } } function runTests() { console.log('\n=== Testing Claude flat-skill migration ===\n'); let passed = 0; let failed = 0; for (const target of ['claude', 'claude-project']) { if (test(`migrates state-managed nested skills for ${target} without deleting untracked files`, () => { const fixture = createFixture({ target }); try { const legacyOperations = seedLegacyInstall(fixture, { windowsSourcePaths: target === 'claude-project', }); const untrackedPath = path.join( fixture.targetRoot, 'skills', 'ecc', 'demo-skill', 'user-notes.md' ); fs.writeFileSync(untrackedPath, '# User notes\n'); applyInstallPlan(fixture.plan); for (const operation of fixture.operations) { assert.strictEqual( fs.readFileSync(operation.destinationPath, 'utf8'), fs.readFileSync(operation.sourcePath, 'utf8') ); } for (const operation of legacyOperations) { assert.ok(!fs.existsSync(operation.destinationPath), operation.destinationPath); } assert.strictEqual(fs.readFileSync(untrackedPath, 'utf8'), '# User notes\n'); const state = readInstallState(fixture.installStatePath); assert.ok(state.operations.some(operation => ( operation.destinationPath === fixture.operations[0].destinationPath ))); assert.ok(!state.operations.some(operation => ( operation.destinationPath.includes(path.join('skills', 'ecc', 'demo-skill')) ))); const rerun = applyInstallPlan(fixture.plan); assert.deepStrictEqual(rerun.skippedOperations, []); assert.strictEqual(fs.readFileSync(untrackedPath, 'utf8'), '# User notes\n'); const uninstall = runUninstall(fixture); assert.strictEqual(uninstall.summary.errorCount, 0); assert.ok(!fs.existsSync(fixture.operations[0].destinationPath)); assert.strictEqual(fs.readFileSync(untrackedPath, 'utf8'), '# User notes\n'); } finally { cleanup(fixture.tempDir); } })) passed++; else failed++; } if (test('selective migration preserves unrelated legacy skills and uninstall ownership', () => { const fixture = createFixture(); try { const legacyOperations = seedLegacyInstall(fixture); const otherSourceRelativePath = path.join('skills', 'other-skill', 'SKILL.md'); const otherSourcePath = path.join(fixture.sourceRoot, otherSourceRelativePath); const otherLegacyPath = path.join( fixture.targetRoot, 'skills', 'ecc', 'other-skill', 'SKILL.md' ); fs.mkdirSync(path.dirname(otherSourcePath), { recursive: true }); fs.mkdirSync(path.dirname(otherLegacyPath), { recursive: true }); fs.writeFileSync(otherSourcePath, '# Other source\n'); fs.writeFileSync(otherLegacyPath, '# Other legacy managed skill\n'); const otherLegacyOperation = createOperation( 'other-module', fixture.sourceRoot, otherSourceRelativePath, otherLegacyPath ); writeInstallState(fixture.installStatePath, { ...fixture.plan.statePreview, operations: [...legacyOperations, otherLegacyOperation], }); applyInstallPlan(fixture.plan); assert.ok(legacyOperations.every(operation => !fs.existsSync(operation.destinationPath))); assert.strictEqual( fs.readFileSync(otherLegacyPath, 'utf8'), '# Other legacy managed skill\n' ); const state = readInstallState(fixture.installStatePath); assert.ok(state.operations.some(operation => ( operation.destinationPath === otherLegacyPath ))); const uninstall = runUninstall(fixture); assert.strictEqual(uninstall.summary.errorCount, 0); assert.ok(!fs.existsSync(otherLegacyPath)); } finally { cleanup(fixture.tempDir); } })) passed++; else failed++; if (test('reruns a completed migration idempotently and remains uninstallable', () => { const fixture = createFixture(); try { const legacyOperations = seedLegacyInstall(fixture); applyInstallPlan(fixture.plan); const stateAfterMigration = readInstallState(fixture.installStatePath); const rerun = applyInstallPlan(fixture.plan); const stateAfterRerun = readInstallState(fixture.installStatePath); assert.deepStrictEqual(rerun.skippedOperations, []); assert.ok(!rerun.warnings.some(warning => ( warning.includes('user-owned') || warning.includes('nested copy') ))); assert.deepStrictEqual(stateAfterRerun, stateAfterMigration); assert.ok(fixture.operations.every(operation => ( fs.readFileSync(operation.destinationPath, 'utf8') === fs.readFileSync(operation.sourcePath, 'utf8') ))); assert.ok(legacyOperations.every(operation => !fs.existsSync(operation.destinationPath))); assert.ok(!fs.existsSync(path.join(fixture.targetRoot, 'skills', 'ecc'))); const uninstall = runUninstall(fixture); assert.strictEqual(uninstall.summary.errorCount, 0); assert.ok(fixture.operations.every(operation => !fs.existsSync(operation.destinationPath))); } finally { cleanup(fixture.tempDir); } })) passed++; else failed++; if (test('preserves a user-owned flat skill and keeps legacy ownership for uninstall', () => { const fixture = createFixture(); try { const legacyOperations = seedLegacyInstall(fixture); const userSkillPath = fixture.operations[0].destinationPath; fs.mkdirSync(path.dirname(userSkillPath), { recursive: true }); fs.writeFileSync(userSkillPath, '# User-owned flat skill\n'); const result = applyInstallPlan(fixture.plan); assert.strictEqual(fs.readFileSync(userSkillPath, 'utf8'), '# User-owned flat skill\n'); assert.ok(legacyOperations.every(operation => fs.existsSync(operation.destinationPath))); assert.ok(result.warnings.some(warning => ( warning.includes('demo-skill') && warning.includes('user-owned') )), JSON.stringify(result.warnings)); assert.strictEqual(result.operations.length, 0); assert.strictEqual(result.skippedOperations.length, fixture.operations.length); const state = readInstallState(fixture.installStatePath); assert.ok(legacyOperations.every(legacyOperation => ( state.operations.some(operation => operation.destinationPath === legacyOperation.destinationPath) ))); assert.ok(!state.operations.some(operation => ( operation.destinationPath === fixture.operations[0].destinationPath ))); const uninstall = runUninstall(fixture); assert.strictEqual(uninstall.summary.errorCount, 0); assert.strictEqual(fs.readFileSync(userSkillPath, 'utf8'), '# User-owned flat skill\n'); assert.ok(legacyOperations.every(operation => !fs.existsSync(operation.destinationPath))); } finally { cleanup(fixture.tempDir); } })) passed++; else failed++; if (test('does not claim or merge into a user-owned flat skill on first install', () => { const fixture = createFixture(); try { const userSkillPath = fixture.operations[0].destinationPath; fs.mkdirSync(path.dirname(userSkillPath), { recursive: true }); fs.writeFileSync(userSkillPath, '# User-owned flat skill\n'); const result = applyInstallPlan(fixture.plan); assert.strictEqual(fs.readFileSync(userSkillPath, 'utf8'), '# User-owned flat skill\n'); assert.ok(!fs.existsSync(fixture.operations[1].destinationPath)); assert.ok(result.warnings.some(warning => warning.includes('user-owned'))); assert.strictEqual(result.operations.length, 0); assert.strictEqual(result.skippedOperations.length, fixture.operations.length); assert.deepStrictEqual(readInstallState(fixture.installStatePath).operations, []); const uninstall = runUninstall(fixture); assert.strictEqual(uninstall.summary.errorCount, 0); assert.strictEqual(fs.readFileSync(userSkillPath, 'utf8'), '# User-owned flat skill\n'); } finally { cleanup(fixture.tempDir); } })) passed++; else failed++; if (test('updates recorded flat files but preserves conflicting unrecorded files', () => { const initial = createFixture({ skillFiles: { 'SKILL.md': '# Initial ECC skill\n', }, }); let expanded; try { applyInstallPlan(initial.plan); expanded = createFixture({ skillFiles: { 'SKILL.md': '# Updated ECC skill\n', 'references/guide.md': '# ECC guide\n', 'references/new.md': '# New managed file\n', }, }); const expandedOriginalTargetRoot = expanded.targetRoot; expanded.homeDir = initial.homeDir; expanded.projectRoot = initial.projectRoot; expanded.targetRoot = initial.targetRoot; expanded.installStatePath = initial.installStatePath; expanded.operations = expanded.operations.map(operation => ({ ...operation, destinationPath: path.join( initial.targetRoot, path.relative(expandedOriginalTargetRoot, operation.destinationPath) ), })); expanded.plan = { ...expanded.plan, targetRoot: initial.targetRoot, installRoot: initial.targetRoot, installStatePath: initial.installStatePath, operations: expanded.operations, statePreview: { ...expanded.plan.statePreview, target: { ...expanded.plan.statePreview.target, root: initial.targetRoot, installStatePath: initial.installStatePath, }, operations: expanded.operations, }, }; const userGuidePath = expanded.operations[1].destinationPath; fs.mkdirSync(path.dirname(userGuidePath), { recursive: true }); fs.writeFileSync(userGuidePath, '# User guide\n'); const result = applyInstallPlan(expanded.plan); assert.strictEqual( fs.readFileSync(expanded.operations[0].destinationPath, 'utf8'), '# Updated ECC skill\n' ); assert.strictEqual(fs.readFileSync(userGuidePath, 'utf8'), '# User guide\n'); assert.strictEqual( fs.readFileSync(expanded.operations[2].destinationPath, 'utf8'), '# New managed file\n' ); assert.ok(result.warnings.some(warning => warning.includes('guide.md'))); const state = readInstallState(initial.installStatePath); assert.ok(state.operations.some(operation => ( operation.destinationPath === expanded.operations[0].destinationPath ))); assert.ok(!state.operations.some(operation => ( operation.destinationPath === userGuidePath ))); assert.ok(state.operations.some(operation => ( operation.destinationPath === expanded.operations[2].destinationPath ))); } finally { cleanup(initial.tempDir); if (expanded) { cleanup(expanded.tempDir); } } })) passed++; else failed++; if (test('tracks a partial migration so retry and uninstall remain safe', () => { const fixture = createFixture(); try { const legacyOperations = seedLegacyInstall(fixture); const missingSourcePlan = { ...fixture.plan, operations: fixture.operations.map((operation, index) => ( index === 1 ? { ...operation, sourcePath: path.join(fixture.sourceRoot, 'missing.md') } : operation )), }; assert.throws(() => applyInstallPlan(missingSourcePlan), /ENOENT/); assert.ok(legacyOperations.every(operation => fs.existsSync(operation.destinationPath))); assert.ok(fs.existsSync(fixture.operations[0].destinationPath)); assert.ok(!fs.existsSync(fixture.operations[1].destinationPath)); const bridgeState = readInstallState(fixture.installStatePath); assert.ok(legacyOperations.every(legacyOperation => ( bridgeState.operations.some(operation => ( operation.destinationPath === legacyOperation.destinationPath )) ))); assert.ok(fixture.operations.every(flatOperation => ( bridgeState.operations.some(operation => ( operation.destinationPath === flatOperation.destinationPath )) ))); const retry = applyInstallPlan(fixture.plan); assert.deepStrictEqual(retry.skippedOperations, []); assert.ok(fixture.operations.every(operation => fs.existsSync(operation.destinationPath))); assert.ok(legacyOperations.every(operation => !fs.existsSync(operation.destinationPath))); const uninstall = runUninstall(fixture); assert.strictEqual(uninstall.summary.errorCount, 0); assert.ok(fixture.operations.every(operation => !fs.existsSync(operation.destinationPath))); } finally { cleanup(fixture.tempDir); } })) passed++; else failed++; if (test('tracks a partial first install so retry does not misclassify it as user-owned', () => { const fixture = createFixture(); try { const missingSourcePlan = { ...fixture.plan, operations: fixture.operations.map((operation, index) => ( index === 1 ? { ...operation, sourcePath: path.join(fixture.sourceRoot, 'missing.md') } : operation )), }; assert.throws(() => applyInstallPlan(missingSourcePlan), /ENOENT/); assert.ok(fs.existsSync(fixture.operations[0].destinationPath)); assert.ok(!fs.existsSync(fixture.operations[1].destinationPath)); const bridgeState = readInstallState(fixture.installStatePath); assert.ok(fixture.operations.every(flatOperation => ( bridgeState.operations.some(operation => ( operation.destinationPath === flatOperation.destinationPath )) ))); const retry = applyInstallPlan(fixture.plan); assert.deepStrictEqual(retry.skippedOperations, []); assert.ok(!retry.warnings.some(warning => warning.includes('user-owned'))); assert.ok(fixture.operations.every(operation => fs.existsSync(operation.destinationPath))); const uninstall = runUninstall(fixture); assert.strictEqual(uninstall.summary.errorCount, 0); assert.ok(fixture.operations.every(operation => !fs.existsSync(operation.destinationPath))); } finally { cleanup(fixture.tempDir); } })) passed++; else failed++; if (test('tracks non-skill files written before a partial flat-skill install fails', () => { const fixture = createFixture(); try { const ruleSourceRelativePath = path.join('rules', 'common', 'coding.md'); const ruleSourcePath = path.join(fixture.sourceRoot, ruleSourceRelativePath); const ruleDestinationPath = path.join( fixture.targetRoot, 'rules', 'ecc', 'common', 'coding.md' ); fs.mkdirSync(path.dirname(ruleSourcePath), { recursive: true }); fs.writeFileSync(ruleSourcePath, '# Managed rule\n'); const ruleOperation = createOperation( 'workflow-quality', fixture.sourceRoot, ruleSourceRelativePath, ruleDestinationPath ); const missingOperation = createOperation( 'workflow-quality', fixture.sourceRoot, path.join('commands', 'missing.md'), path.join(fixture.targetRoot, 'commands', 'missing.md') ); const operations = [ fixture.operations[0], ruleOperation, missingOperation, ]; const partialPlan = { ...fixture.plan, operations, statePreview: { ...fixture.plan.statePreview, operations: operations.map(operation => ({ ...operation })), }, }; assert.throws(() => applyInstallPlan(partialPlan), /ENOENT/); assert.ok(fs.existsSync(ruleDestinationPath)); const bridgeState = readInstallState(fixture.installStatePath); assert.ok(bridgeState.operations.some(operation => ( operation.destinationPath === ruleDestinationPath ))); const uninstall = runUninstall(fixture); assert.strictEqual(uninstall.summary.errorCount, 0); assert.ok(!fs.existsSync(ruleDestinationPath)); } finally { cleanup(fixture.tempDir); } })) passed++; else failed++; if (test('tracks partial non-skill writes when every flat skill is user-owned', () => { const fixture = createFixture(); try { const userSkillPath = fixture.operations[0].destinationPath; fs.mkdirSync(path.dirname(userSkillPath), { recursive: true }); fs.writeFileSync(userSkillPath, '# User skill\n'); const ruleSourceRelativePath = path.join('rules', 'common', 'coding.md'); const ruleSourcePath = path.join(fixture.sourceRoot, ruleSourceRelativePath); const ruleDestinationPath = path.join( fixture.targetRoot, 'rules', 'ecc', 'common', 'coding.md' ); fs.mkdirSync(path.dirname(ruleSourcePath), { recursive: true }); fs.writeFileSync(ruleSourcePath, '# Managed rule\n'); const ruleOperation = createOperation( 'workflow-quality', fixture.sourceRoot, ruleSourceRelativePath, ruleDestinationPath ); const missingOperation = createOperation( 'workflow-quality', fixture.sourceRoot, path.join('commands', 'missing.md'), path.join(fixture.targetRoot, 'commands', 'missing.md') ); const operations = [ ...fixture.operations, ruleOperation, missingOperation, ]; const partialPlan = { ...fixture.plan, operations, statePreview: { ...fixture.plan.statePreview, operations: operations.map(operation => ({ ...operation })), }, }; assert.throws(() => applyInstallPlan(partialPlan), /ENOENT/); assert.strictEqual(fs.readFileSync(userSkillPath, 'utf8'), '# User skill\n'); assert.ok(fs.existsSync(ruleDestinationPath)); const bridgeState = readInstallState(fixture.installStatePath); assert.ok(!bridgeState.operations.some(operation => ( operation.destinationPath === userSkillPath ))); assert.ok(bridgeState.operations.some(operation => ( operation.destinationPath === ruleDestinationPath ))); const uninstall = runUninstall(fixture); assert.strictEqual(uninstall.summary.errorCount, 0); assert.strictEqual(fs.readFileSync(userSkillPath, 'utf8'), '# User skill\n'); assert.ok(!fs.existsSync(ruleDestinationPath)); } finally { cleanup(fixture.tempDir); } })) passed++; else failed++; if (test('keeps legacy files tracked when the bridge state write fails', () => { const fixture = createFixture(); try { const legacyOperations = seedLegacyInstall(fixture); const failingStateWriter = filePath => { assert.strictEqual( path.resolve(filePath), path.resolve(fixture.installStatePath) ); throw new Error('injected install-state write failure'); }; assert.throws( () => applyInstallPlan(fixture.plan, { writeInstallState: failingStateWriter }), /injected install-state write failure/ ); assert.ok(legacyOperations.every(operation => fs.existsSync(operation.destinationPath))); assert.ok(fixture.operations.every(operation => !fs.existsSync(operation.destinationPath))); const state = readInstallState(fixture.installStatePath); assert.ok(state.operations.every(operation => ( operation.destinationPath.includes(path.join('skills', 'ecc', 'demo-skill')) ))); const retry = applyInstallPlan(fixture.plan); assert.deepStrictEqual(retry.skippedOperations, []); const uninstall = runUninstall(fixture); assert.strictEqual(uninstall.summary.errorCount, 0); assert.ok(fixture.operations.every(operation => !fs.existsSync(operation.destinationPath))); } finally { cleanup(fixture.tempDir); } })) passed++; else failed++; if (test('keeps both layouts represented if the final state write fails', () => { const fixture = createFixture(); let stateWriteCount = 0; try { const legacyOperations = seedLegacyInstall(fixture); const failFinalStateWrite = (filePath, state) => { assert.strictEqual( path.resolve(filePath), path.resolve(fixture.installStatePath) ); stateWriteCount += 1; if (stateWriteCount === 2) { throw new Error('injected final install-state write failure'); } return writeInstallState(fixture.installStatePath, state); }; assert.throws( () => applyInstallPlan(fixture.plan, { writeInstallState: failFinalStateWrite }), /injected final install-state write failure/ ); assert.ok(fixture.operations.every(operation => fs.existsSync(operation.destinationPath))); assert.ok(legacyOperations.every(operation => !fs.existsSync(operation.destinationPath))); const bridgeState = readInstallState(fixture.installStatePath); assert.ok(fixture.operations.every(flatOperation => ( bridgeState.operations.some(operation => ( operation.destinationPath === flatOperation.destinationPath )) ))); assert.ok(bridgeState.operations.some(operation => ( operation.destinationPath.includes(path.join('skills', 'ecc', 'demo-skill')) ))); const uninstall = runUninstall(fixture); assert.strictEqual(uninstall.summary.errorCount, 0); assert.ok(fixture.operations.every(operation => !fs.existsSync(operation.destinationPath))); } finally { cleanup(fixture.tempDir); } })) passed++; else failed++; if (test('rejects a flat skill symlink that escapes the Claude install root', () => { if (process.platform === 'win32') { console.log(' ↷ skipped on Windows: symlink privileges vary'); return; } const fixture = createFixture(); try { const outsideRoot = path.join(fixture.tempDir, 'outside'); fs.mkdirSync(outsideRoot, { recursive: true }); const flatSkillRoot = path.join(fixture.targetRoot, 'skills', 'demo-skill'); fs.mkdirSync(path.dirname(flatSkillRoot), { recursive: true }); fs.symlinkSync(outsideRoot, flatSkillRoot, 'dir'); assert.throws( () => applyInstallPlan(fixture.plan), /outside the install root|symlinked Claude skill path/ ); assert.deepStrictEqual(fs.readdirSync(outsideRoot), []); assert.ok(!fs.existsSync(fixture.installStatePath)); } finally { cleanup(fixture.tempDir); } })) passed++; else failed++; if (test('rechecks skill directories created between validation and copy', () => { if (process.platform === 'win32') { console.log(' ↷ skipped on Windows: symlink privileges vary'); return; } const fixture = createFixture({ skillFiles: { 'SKILL.md': '# Current ECC skill\n', }, }); const destinationDirectory = path.dirname(fixture.operations[0].destinationPath); const outsideRoot = path.join(fixture.tempDir, 'outside'); const originalMkdirSync = fs.mkdirSync; try { originalMkdirSync(outsideRoot, { recursive: true }); let injectedSymlink = false; fs.mkdirSync = function mkdirAndReplaceWithSymlink(directoryPath, options) { const result = originalMkdirSync(directoryPath, options); if (!injectedSymlink && path.resolve(directoryPath) === path.resolve(destinationDirectory)) { fs.rmSync(destinationDirectory, { recursive: true, force: true }); fs.symlinkSync(outsideRoot, destinationDirectory, 'dir'); injectedSymlink = true; } return result; }; assert.throws( () => applyInstallPlan(fixture.plan, { writeInstallState() {} }), /outside the install root|symlinked Claude skill path/ ); assert.strictEqual(injectedSymlink, true); assert.deepStrictEqual(fs.readdirSync(outsideRoot), []); } finally { fs.mkdirSync = originalMkdirSync; cleanup(fixture.tempDir); } })) passed++; else failed++; if (test('rejects a dangling destination symlink before copying a Claude skill file', () => { if (process.platform === 'win32') { console.log(' ↷ skipped on Windows: symlink privileges vary'); return; } const fixture = createFixture({ skillFiles: { 'SKILL.md': '# Current ECC skill\n', }, }); try { const outsideRoot = path.join(fixture.tempDir, 'outside'); const outsideTarget = path.join(outsideRoot, 'not-created.md'); fs.mkdirSync(outsideRoot, { recursive: true }); fs.mkdirSync(path.dirname(fixture.operations[0].destinationPath), { recursive: true }); fs.symlinkSync(outsideTarget, fixture.operations[0].destinationPath, 'file'); assert.strictEqual(fs.existsSync(fixture.operations[0].destinationPath), false); assert.throws( () => applyInstallPlan(fixture.plan), /symlinked Claude skill path/ ); assert.ok(!fs.existsSync(outsideTarget)); assert.ok(!fs.existsSync(fixture.installStatePath)); } finally { cleanup(fixture.tempDir); } })) passed++; else failed++; console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); } runTests();