2026-03-14 12:55:25 -07:00
/**
* Tests for scripts/install-apply.js
*/
const assert = require ( 'assert' );
const fs = require ( 'fs' );
const os = require ( 'os' );
const path = require ( 'path' );
2026-08-06 15:39:49 -04:00
const { execFileSync , spawnSync } = require ( 'child_process' );
2026-08-13 16:42:51 -04:00
const yaml = require ( 'js-yaml' );
2026-04-05 14:37:28 -07:00
const { applyInstallPlan } = require ( '../../scripts/lib/install/apply' );
2026-03-14 12:55:25 -07:00
const SCRIPT = path . join ( __dirname , '..' , '..' , 'scripts' , 'install-apply.js' );
2026-04-29 17:42:19 -04:00
const DEFAULT_INSTALL_APPLY_TIMEOUT_MS = process . platform === 'win32' ? 30000 : 10000 ;
2026-03-14 12:55:25 -07:00
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' ));
}
2026-08-13 16:42:51 -04:00
function readMarkdownFrontmatter ( filePath ) {
const source = fs . readFileSync ( filePath , 'utf8' );
const match = source . match ( /^---\n([\s\S]*?)\n---\n/ );
assert . ok ( match , `Expected YAML frontmatter in ${ filePath } ` );
return yaml . load ( match [ 1 ]);
}
2026-03-14 12:55:25 -07:00
function run ( args = [], options = {}) {
2026-04-02 17:09:21 -07:00
const homeDir = options . homeDir || process . env . HOME ;
2026-03-14 12:55:25 -07:00
const env = {
... process . env ,
2026-04-02 17:09:21 -07:00
HOME : homeDir ,
USERPROFILE : homeDir ,
2026-03-14 12:55:25 -07:00
...( options . env || {}),
};
try {
const stdout = execFileSync ( 'node' , [ SCRIPT , ... args ], {
cwd : options . cwd ,
env ,
encoding : 'utf8' ,
stdio : [ 'pipe' , 'pipe' , 'pipe' ],
2026-07-23 19:28:55 -07:00
maxBuffer : 4 * 1024 * 1024 ,
2026-04-29 17:42:19 -04:00
timeout : options . timeout || DEFAULT_INSTALL_APPLY_TIMEOUT_MS ,
2026-03-14 12:55:25 -07:00
});
return { code : 0 , stdout , stderr : '' };
} catch ( error ) {
return {
code : error . status || 1 ,
stdout : error . stdout || '' ,
2026-04-29 17:42:19 -04:00
stderr : error . stderr || error . message || '' ,
2026-03-14 12:55:25 -07:00
};
}
}
2026-08-06 15:39:49 -04:00
function runWithGuidedDispatcherFailure ( failureMode ) {
const root = createTempDir ( 'install-apply-guided-failure-' );
const preloadPath = path . join ( root , 'preload.js' );
const failureMessage = 'guided dispatcher failed\u001b[31m' ;
const replacement = failureMode === 'load'
? `throw new Error( ${ JSON . stringify ( failureMessage ) } );`
: `return { main: () => Promise.reject(new Error( ${ JSON . stringify ( failureMessage ) } )) };` ;
fs . writeFileSync ( preloadPath , `
const Module = require('module');
const originalLoad = Module._load;
Module._load = function(request, parent, isMain) {
if (request === './install-guided' && /install-apply\\.js$/.test(parent?.filename || '')) {
${ replacement }
}
return originalLoad.call(this, request, parent, isMain);
};
` );
try {
return spawnSync ( process . execPath , [ '--require' , preloadPath , SCRIPT , '--guided' ], {
cwd : path . dirname ( SCRIPT ),
encoding : 'utf8' ,
});
} finally {
cleanup ( root );
}
}
2026-03-14 12:55:25 -07:00
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 <name>' ));
assert . ok ( result . stdout . includes ( '--modules <id,id,...>' ));
})) passed ++ ; else failed ++ ;
2026-08-06 15:39:49 -04:00
if ( test ( 'guided dispatcher reports sanitized load and rejection failures' , () => {
for ( const failureMode of [ 'load' , 'reject' ]) {
const result = runWithGuidedDispatcherFailure ( failureMode );
assert . strictEqual ( result . status , 1 );
assert . strictEqual ( result . stdout , '' );
assert . strictEqual ( result . stderr , 'Error: guided dispatcher failed\n' );
}
})) passed ++ ; else failed ++ ;
2026-03-14 12:55:25 -07:00
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 {
2026-08-07 15:18:14 -04:00
const result = run ([ 'typescript' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-03-14 12:55:25 -07:00
assert . strictEqual ( result . code , 0 , result . stderr );
2026-03-15 21:47:22 -07:00
const claudeRoot = path . join ( homeDir , '.claude' );
2026-04-30 07:43:20 -04:00
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'rules' , 'ecc' , 'common' , 'coding-style.md' )));
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'rules' , 'ecc' , 'typescript' , 'testing.md' )));
2026-03-15 21:47:22 -07:00
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'commands' , 'plan.md' )));
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'scripts' , 'hooks' , 'session-end.js' )));
2026-03-24 23:08:27 -04:00
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'scripts' , 'lib' , 'utils.js' )));
2026-07-26 03:20:06 -07:00
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'skills' , 'tdd-workflow' , 'SKILL.md' )));
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'skills' , 'coding-standards' , 'SKILL.md' )));
2026-03-15 21:47:22 -07:00
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'plugin.json' )));
2026-03-14 12:55:25 -07:00
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 );
2026-03-15 21:47:22 -07:00
assert . deepStrictEqual ( state . request . modules , []);
assert . ok ( state . resolution . selectedModules . includes ( 'rules-core' ));
assert . ok ( state . resolution . selectedModules . includes ( 'framework-language' ));
2026-03-14 12:55:25 -07:00
assert . ok (
state . operations . some ( operation => (
2026-04-30 07:43:20 -04:00
operation . destinationPath === path . join ( claudeRoot , 'rules' , 'ecc' , 'common' , 'coding-style.md' )
2026-03-14 12:55:25 -07:00
)),
'Should record common rule file operation'
);
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-07-03 21:10:45 -07:00
if ( test ( 'rewrites namespaced skill links to the ecc/ rules path (#2340)' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
2026-08-07 15:18:14 -04:00
const result = run ([ 'typescript' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-07-03 21:10:45 -07:00
assert . strictEqual ( result . code , 0 , result . stderr );
const claudeRoot = path . join ( homeDir , '.claude' );
2026-07-26 03:20:06 -07:00
const skillPath = path . join ( claudeRoot , 'skills' , 'react-patterns' , 'SKILL.md' );
2026-07-03 21:10:45 -07:00
assert . ok ( fs . existsSync ( skillPath ), 'react-patterns SKILL.md should be installed' );
const content = fs . readFileSync ( skillPath , 'utf8' );
assert . ok (
2026-07-26 03:20:06 -07:00
content . includes ( '../../rules/ecc/react/' ),
2026-07-03 21:10:45 -07:00
'source-relative rules link should be rewritten for the ecc/ namespace'
);
assert . ok (
2026-07-26 03:20:06 -07:00
! content . includes ( '](../../rules/react/' ),
'no un-namespaced ](../../rules/react/ links should remain'
2026-07-03 21:10:45 -07:00
);
// The rewritten link must resolve to a file that actually exists on disk.
const linkTarget = path . join (
path . dirname ( skillPath ),
2026-07-26 03:20:06 -07:00
'../../rules/ecc/react/hooks.md'
2026-07-03 21:10:45 -07:00
);
assert . ok ( fs . existsSync ( linkTarget ), 'rewritten link target should exist' );
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-03-14 12:55:25 -07:00
if ( test ( 'installs Cursor configs and writes install-state' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
2026-08-07 15:18:14 -04:00
const result = run ([ '--target' , 'cursor' , 'typescript' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-03-14 12:55:25 -07:00
assert . strictEqual ( result . code , 0 , result . stderr );
2026-04-12 23:32:39 -07:00
assert . ok ( fs . existsSync ( path . join ( projectDir , '.cursor' , 'rules' , 'common-coding-style.mdc' )));
assert . ok ( fs . existsSync ( path . join ( projectDir , '.cursor' , 'rules' , 'typescript-testing.mdc' )));
2026-04-12 23:51:58 -07:00
assert . ok ( fs . existsSync ( path . join ( projectDir , '.cursor' , 'rules' , 'common-agents.mdc' )));
assert . ok ( ! fs . existsSync ( path . join ( projectDir , '.cursor' , 'rules' , 'common-agents.md' )));
assert . ok ( ! fs . existsSync ( path . join ( projectDir , '.cursor' , 'rules' , 'README.mdc' )));
2026-04-30 02:12:24 -04:00
assert . ok ( fs . existsSync ( path . join ( projectDir , '.cursor' , 'agents' , 'ecc-architect.md' )));
assert . ok ( ! fs . existsSync ( path . join ( projectDir , '.cursor' , 'agents' , 'architect.md' )));
2026-03-15 21:47:22 -07:00
assert . ok ( fs . existsSync ( path . join ( projectDir , '.cursor' , 'commands' , 'plan.md' )));
2026-03-14 12:55:25 -07:00
assert . ok ( fs . existsSync ( path . join ( projectDir , '.cursor' , 'hooks.json' )));
2026-04-21 18:35:21 -04:00
assert . ok ( fs . existsSync ( path . join ( projectDir , '.cursor' , 'mcp.json' )));
2026-03-14 12:55:25 -07:00
assert . ok ( fs . existsSync ( path . join ( projectDir , '.cursor' , 'hooks' , 'session-start.js' )));
2026-03-24 23:08:27 -04:00
assert . ok ( fs . existsSync ( path . join ( projectDir , '.cursor' , 'scripts' , 'lib' , 'utils.js' )));
2026-03-15 21:47:22 -07:00
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' )));
2026-03-14 12:55:25 -07:00
2026-04-21 18:35:21 -04:00
const hooksConfig = readJson ( path . join ( projectDir , '.cursor' , 'hooks.json' ));
const mcpConfig = readJson ( path . join ( projectDir , '.cursor' , 'mcp.json' ));
assert . strictEqual ( hooksConfig . version , 1 );
assert . ok ( hooksConfig . hooks . sessionStart , 'Should keep Cursor sessionStart hooks' );
2026-06-09 23:28:35 -04:00
assert . ok ( mcpConfig . mcpServers [ 'chrome-devtools' ], 'Should install shared MCP servers into Cursor' );
2026-04-21 18:35:21 -04:00
2026-03-14 12:55:25 -07:00
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' ));
2026-03-15 21:47:22 -07:00
assert . deepStrictEqual ( state . request . legacyLanguages , [ 'typescript' ]);
assert . strictEqual ( state . request . legacyMode , true );
assert . ok ( state . resolution . selectedModules . includes ( 'framework-language' ));
2026-03-14 12:55:25 -07:00
assert . ok (
state . operations . some ( operation => (
2026-03-15 21:47:22 -07:00
operation . destinationPath === path . join ( normalizedProjectDir , '.cursor' , 'commands' , 'plan.md' )
2026-03-14 12:55:25 -07:00
)),
2026-03-15 21:47:22 -07:00
'Should record manifest command file copy operation'
2026-03-14 12:55:25 -07:00
);
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-04-21 18:35:21 -04:00
if ( test ( 'installs Cursor MCP config by merging bundled servers into an existing mcp.json' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
const cursorRoot = path . join ( projectDir , '.cursor' );
fs . mkdirSync ( cursorRoot , { recursive : true });
fs . writeFileSync ( path . join ( cursorRoot , 'mcp.json' ), JSON . stringify ({
mcpServers : {
custom : {
command : 'node' ,
args : [ 'custom-mcp.js' ],
},
},
}, null , 2 ));
2026-08-07 15:18:14 -04:00
const result = run ([ '--target' , 'cursor' , 'typescript' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-04-21 18:35:21 -04:00
assert . strictEqual ( result . code , 0 , result . stderr );
const mcpConfig = readJson ( path . join ( projectDir , '.cursor' , 'mcp.json' ));
assert . ok ( mcpConfig . mcpServers . custom , 'Should preserve existing custom Cursor MCP servers' );
2026-06-09 23:28:35 -04:00
assert . ok ( mcpConfig . mcpServers [ 'chrome-devtools' ], 'Should merge the bundled chrome-devtools MCP server' );
2026-04-21 18:35:21 -04:00
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-03-14 12:55:25 -07:00
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 );
2026-08-13 16:42:51 -04:00
assert . ok ( fs . existsSync ( path . join ( projectDir , '.agents' , 'rules' , 'common-coding-style.md' )));
assert . ok ( fs . existsSync ( path . join ( projectDir , '.agents' , 'rules' , 'typescript-testing.md' )));
assert . ok ( ! fs . existsSync ( path . join ( projectDir , '.agents' , 'rules' , 'python-testing.md' )));
assert . ok ( fs . existsSync ( path . join ( projectDir , '.agents' , 'workflows' , 'plan.md' )));
assert . ok ( fs . existsSync ( path . join ( projectDir , '.agents' , 'skills' , 'tdd-workflow' , 'SKILL.md' )));
assert . ok ( fs . existsSync ( path . join ( projectDir , '.agents' , 'agents' , 'architect.md' )));
const tddGuide = readMarkdownFrontmatter (
path . join ( projectDir , '.agents' , 'agents' , 'tdd-guide.md' )
);
assert . deepStrictEqual (
tddGuide . tools ,
[ 'view_file' , 'write_to_file' , 'replace_file_content' , 'run_command' , 'grep_search' ]
);
assert . strictEqual ( tddGuide . model , 'pro' );
const docsLookup = readMarkdownFrontmatter (
path . join ( projectDir , '.agents' , 'agents' , 'docs-lookup.md' )
);
assert . deepStrictEqual ( docsLookup . tools , [ 'view_file' , 'grep_search' ]);
const harnessOptimizer = readMarkdownFrontmatter (
path . join ( projectDir , '.agents' , 'agents' , 'harness-optimizer.md' )
);
assert . ok ( ! Object . hasOwn ( harnessOptimizer , 'color' ), 'Should omit Claude-only color metadata' );
2026-03-14 12:55:25 -07:00
2026-08-13 16:42:51 -04:00
const statePath = path . join ( projectDir , '.agents' , 'ecc-install-state.json' );
2026-03-14 12:55:25 -07:00
const state = readJson ( statePath );
assert . strictEqual ( state . target . id , 'antigravity-project' );
2026-03-15 21:47:22 -07:00
assert . deepStrictEqual ( state . request . legacyLanguages , [ 'typescript' ]);
assert . strictEqual ( state . request . legacyMode , true );
2026-08-13 16:42:51 -04:00
assert . deepStrictEqual (
state . resolution . selectedModules ,
[
'rules-core' ,
'agents-core' ,
'commands-core' ,
'platform-configs' ,
'skill-unified-memory' ,
'workflow-quality' ,
]
);
2026-03-14 12:55:25 -07:00
assert . ok (
state . operations . some ( operation => (
2026-08-13 16:42:51 -04:00
operation . destinationPath . endsWith ( path . join ( '.agents' , 'workflows' , 'plan.md' ))
2026-03-14 12:55:25 -07:00
)),
2026-03-15 21:47:22 -07:00
'Should record manifest command file copy operation'
2026-03-14 12:55:25 -07:00
);
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-08-13 16:42:51 -04:00
if ( test ( 'maps legacy language aliases to Antigravity rule namespaces' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
const result = run (
[ '--target' , 'antigravity' , 'c' , 'go' , 'kotlin' , 'javascript' , 'rails' , 'harmonyos' ],
{ cwd : projectDir , homeDir }
);
assert . strictEqual ( result . code , 0 , result . stderr );
const rulesDir = path . join ( projectDir , '.agents' , 'rules' );
for ( const fileName of [
'golang-testing.md' ,
'kotlin-testing.md' ,
'typescript-testing.md' ,
'ruby-testing.md' ,
'arkts-testing.md' ,
'cpp-testing.md' ,
]) {
assert . ok ( fs . existsSync ( path . join ( rulesDir , fileName )), `Expected ${ fileName } ` );
}
assert . ok ( ! fs . existsSync ( path . join ( rulesDir , 'python-testing.md' )));
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-05-11 10:58:24 -04:00
if ( test ( 'installs JoyCode profile through managed install-state' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
const result = run ([ '--target' , 'joycode' , '--profile' , 'minimal' ], { cwd : projectDir , homeDir });
assert . strictEqual ( result . code , 0 , result . stderr );
assert . ok ( fs . existsSync ( path . join ( projectDir , '.joycode' , 'rules' , 'common-coding-style.md' )));
assert . ok ( ! fs . existsSync ( path . join ( projectDir , '.joycode' , 'rules' , 'common' , 'coding-style.md' )));
assert . ok ( fs . existsSync ( path . join ( projectDir , '.joycode' , 'agents' , 'architect.md' )));
assert . ok ( fs . existsSync ( path . join ( projectDir , '.joycode' , 'commands' , 'plan.md' )));
assert . ok ( fs . existsSync ( path . join ( projectDir , '.joycode' , 'skills' , 'tdd-workflow' , 'SKILL.md' )));
assert . ok ( fs . existsSync ( path . join ( projectDir , '.joycode' , 'mcp-configs' , 'mcp-servers.json' )));
assert . ok ( ! fs . existsSync ( path . join ( projectDir , '.joycode' , 'hooks' )));
const statePath = path . join ( projectDir , '.joycode' , 'ecc-install-state.json' );
const state = readJson ( statePath );
assert . strictEqual ( state . target . id , 'joycode-project' );
assert . deepStrictEqual ( state . request . modules , []);
assert . strictEqual ( state . request . profile , 'minimal' );
assert . ok ( state . resolution . selectedModules . includes ( 'workflow-quality' ));
assert . ok (
state . operations . some ( operation => (
operation . destinationPath . endsWith ( path . join ( '.joycode' , 'skills' , 'tdd-workflow' , 'SKILL.md' ))
)),
'Should record JoyCode skill file operation'
);
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-05-11 11:15:45 -04:00
if ( test ( 'installs Qwen profile through managed home install-state' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
const result = run ([ '--target' , 'qwen' , '--profile' , 'minimal' ], { cwd : projectDir , homeDir });
assert . strictEqual ( result . code , 0 , result . stderr );
assert . ok ( fs . existsSync ( path . join ( homeDir , '.qwen' , 'QWEN.md' )));
assert . ok ( fs . existsSync ( path . join ( homeDir , '.qwen' , 'rules' , 'common' , 'coding-style.md' )));
assert . ok ( fs . existsSync ( path . join ( homeDir , '.qwen' , 'agents' , 'architect.md' )));
assert . ok ( fs . existsSync ( path . join ( homeDir , '.qwen' , 'commands' , 'plan.md' )));
assert . ok ( fs . existsSync ( path . join ( homeDir , '.qwen' , 'skills' , 'tdd-workflow' , 'SKILL.md' )));
assert . ok ( fs . existsSync ( path . join ( homeDir , '.qwen' , 'mcp-configs' , 'mcp-servers.json' )));
assert . ok ( ! fs . existsSync ( path . join ( homeDir , '.qwen' , 'hooks' )));
const statePath = path . join ( homeDir , '.qwen' , 'ecc-install-state.json' );
const state = readJson ( statePath );
assert . strictEqual ( state . target . id , 'qwen-home' );
assert . deepStrictEqual ( state . request . modules , []);
assert . strictEqual ( state . request . profile , 'minimal' );
assert . ok ( state . resolution . selectedModules . includes ( 'workflow-quality' ));
assert . ok (
state . operations . some ( operation => (
operation . destinationPath . endsWith ( path . join ( '.qwen' , 'skills' , 'tdd-workflow' , 'SKILL.md' ))
)),
'Should record Qwen skill file operation'
);
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-03-14 12:55:25 -07:00
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' ));
2026-03-15 21:47:22 -07:00
assert . ok ( result . stdout . includes ( 'Mode: legacy-compat' ));
assert . ok ( result . stdout . includes ( 'Legacy languages: typescript' ));
2026-03-14 12:55:25 -07:00
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)' ));
2026-07-26 02:46:59 -07:00
assert . ok ( result . stdout . includes (
'Selected modules: rules-core, agents-core, commands-core, hooks-runtime, '
+ 'platform-configs, skill-unified-memory, workflow-quality'
));
2026-03-14 12:55:25 -07:00
assert . ok ( ! fs . existsSync ( path . join ( homeDir , '.claude' , 'ecc' , 'install-state.json' )));
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-07-08 17:14:52 -04:00
if ( test ( 'full profile dry-runs include delivery-gate in the install plan' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
const result = run ([ '--profile' , 'full' , '--dry-run' , '--json' ], { cwd : projectDir , homeDir });
assert . strictEqual ( result . code , 0 , result . stderr );
const parsed = JSON . parse ( result . stdout );
assert . strictEqual ( parsed . dryRun , true );
assert . ok ( parsed . plan . selectedModuleIds . includes ( 'workflow-quality' ));
assert . ok (
parsed . plan . operations . some ( operation => (
String ( operation . sourceRelativePath || '' ). replace ( /\\/g , '/' ). startsWith ( 'skills/delivery-gate/' )
)),
'Full profile dry-run should include the delivery-gate skill'
);
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-03-14 12:55:25 -07:00
2026-04-30 01:48:31 -04:00
if ( test ( 'supports minimal profile dry-runs without hooks through the installer' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
const result = run ([ '--profile' , 'minimal' , '--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: minimal' ));
2026-07-26 02:46:59 -07:00
assert . ok ( result . stdout . includes (
'Selected modules: rules-core, agents-core, commands-core, platform-configs, '
+ 'skill-unified-memory, workflow-quality'
));
2026-04-30 01:48:31 -04:00
assert . ok ( ! result . stdout . includes ( 'hooks-runtime' ));
assert . ok ( ! fs . existsSync ( path . join ( homeDir , '.claude' , 'ecc' , 'install-state.json' )));
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-03-14 12:55:25 -07:00
if ( test ( 'installs manifest profiles and writes non-legacy install-state' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
2026-08-07 15:18:14 -04:00
const result = run ([ '--profile' , 'core' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-03-14 12:55:25 -07:00
assert . strictEqual ( result . code , 0 , result . stderr );
const claudeRoot = path . join ( homeDir , '.claude' );
2026-04-30 07:43:20 -04:00
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'rules' , 'ecc' , 'common' , 'coding-style.md' )));
2026-03-14 12:55:25 -07:00
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' )));
2026-03-24 23:08:27 -04:00
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'scripts' , 'lib' , 'session-manager.js' )));
2026-03-14 12:55:25 -07:00
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 ++ ;
2026-04-30 07:43:20 -04:00
if ( test ( 'preserves existing top-level Claude rules and skills during managed install' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
const claudeRoot = path . join ( homeDir , '.claude' );
const userRulePath = path . join ( claudeRoot , 'rules' , 'common' , 'coding-style.md' );
const userSkillPath = path . join ( claudeRoot , 'skills' , 'tdd-workflow' , 'SKILL.md' );
fs . mkdirSync ( path . dirname ( userRulePath ), { recursive : true });
fs . mkdirSync ( path . dirname ( userSkillPath ), { recursive : true });
fs . writeFileSync ( userRulePath , '# User custom rule\n' );
fs . writeFileSync ( userSkillPath , '# User custom skill\n' );
2026-08-07 15:18:14 -04:00
const result = run ([ '--profile' , 'core' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-04-30 07:43:20 -04:00
assert . strictEqual ( result . code , 0 , result . stderr );
2026-07-26 03:20:06 -07:00
assert . ok ( result . stdout . includes ( 'user-owned' ), result . stdout );
assert . ok ( result . stdout . includes ( 'Skipped operations:' ), result . stdout );
2026-04-30 07:43:20 -04:00
assert . strictEqual ( fs . readFileSync ( userRulePath , 'utf8' ), '# User custom rule\n' );
assert . strictEqual ( fs . readFileSync ( userSkillPath , 'utf8' ), '# User custom skill\n' );
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'rules' , 'ecc' , 'common' , 'coding-style.md' )));
2026-07-26 03:20:06 -07:00
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'skills' , 'verification-loop' , 'SKILL.md' )));
const state = readJson ( path . join ( claudeRoot , 'ecc' , 'install-state.json' ));
assert . ok ( ! state . operations . some ( operation => (
operation . destinationPath . startsWith ( path . join ( claudeRoot , 'skills' , 'tdd-workflow' ))
)));
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'reports applied and skipped user-owned Claude skill operations in JSON' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
const userSkillPath = path . join (
homeDir ,
'.claude' ,
'skills' ,
'tdd-workflow' ,
'SKILL.md'
);
fs . mkdirSync ( path . dirname ( userSkillPath ), { recursive : true });
fs . writeFileSync ( userSkillPath , '# User custom skill\n' );
const result = run ([ '--skills' , 'tdd-workflow' , '--json' ], {
cwd : projectDir ,
homeDir ,
});
assert . strictEqual ( result . code , 0 , result . stderr );
const payload = JSON . parse ( result . stdout );
assert . strictEqual ( payload . dryRun , false );
assert . ok ( payload . result . plannedOperations . length > 0 );
assert . ok ( payload . result . operations . length > 0 );
assert . ok ( payload . result . skippedOperations . length > 0 );
assert . strictEqual (
payload . result . operations . length + payload . result . skippedOperations . length ,
payload . result . plannedOperations . length
);
assert . ok ( payload . result . skippedOperations . every ( operation => (
operation . destinationPath . startsWith ( path . dirname ( userSkillPath ))
)));
assert . ok ( ! payload . result . operations . some ( operation => (
operation . destinationPath . startsWith ( path . dirname ( userSkillPath ))
)));
assert . ok ( payload . result . warnings . some ( warning => warning . includes ( 'user-owned' )));
assert . strictEqual ( fs . readFileSync ( userSkillPath , 'utf8' ), '# User custom skill\n' );
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'dry-run reports the same user-owned Claude skill conflicts as apply' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
const userSkillRoot = path . join (
homeDir ,
'.claude' ,
'skills' ,
'tdd-workflow'
);
const userSkillPath = path . join ( userSkillRoot , 'SKILL.md' );
fs . mkdirSync ( userSkillRoot , { recursive : true });
fs . writeFileSync ( userSkillPath , '# User custom skill\n' );
const result = run (
[ '--skills' , 'tdd-workflow' , '--dry-run' , '--json' ],
{ cwd : projectDir , homeDir }
);
assert . strictEqual ( result . code , 0 , result . stderr );
const payload = JSON . parse ( result . stdout );
assert . strictEqual ( payload . dryRun , true );
assert . ok ( payload . plan . plannedOperations . length > 0 );
assert . ok ( payload . plan . skippedOperations . length > 0 );
assert . ok ( payload . plan . warnings . some ( warning => warning . includes ( 'user-owned' )));
assert . ok ( payload . plan . skippedOperations . every ( operation => (
operation . destinationPath . startsWith ( userSkillRoot )
)));
assert . ok ( ! payload . plan . operations . some ( operation => (
operation . destinationPath . startsWith ( userSkillRoot )
)));
assert . strictEqual ( fs . readFileSync ( userSkillPath , 'utf8' ), '# User custom skill\n' );
assert . ok ( ! fs . existsSync ( path . join ( homeDir , '.claude' , 'ecc' , 'install-state.json' )));
2026-04-30 07:43:20 -04:00
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-03-25 04:00:50 -04:00
if ( test ( 'installs antigravity manifest profiles while skipping only unsupported modules' , () => {
2026-03-15 21:47:22 -07:00
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 );
2026-08-13 16:42:51 -04:00
assert . ok ( fs . existsSync ( path . join ( projectDir , '.agents' , 'rules' , 'common-coding-style.md' )));
assert . ok (
fs . existsSync ( path . join ( projectDir , '.agents' , 'rules' , 'python-testing.md' )),
'Manifest profiles should retain broad rule coverage'
);
assert . ok ( fs . existsSync ( path . join ( projectDir , '.agents' , 'agents' , 'architect.md' )));
assert . ok ( fs . existsSync ( path . join ( projectDir , '.agents' , 'workflows' , 'plan.md' )));
assert . ok ( fs . existsSync ( path . join ( projectDir , '.agents' , 'skills' , 'tdd-workflow' , 'SKILL.md' )));
2026-03-15 21:47:22 -07:00
2026-08-13 16:42:51 -04:00
const state = readJson ( path . join ( projectDir , '.agents' , 'ecc-install-state.json' ));
2026-03-15 21:47:22 -07:00
assert . strictEqual ( state . request . profile , 'core' );
assert . strictEqual ( state . request . legacyMode , false );
2026-03-25 04:00:50 -04:00
assert . deepStrictEqual (
state . resolution . selectedModules ,
2026-07-26 02:46:59 -07:00
[
'rules-core' ,
'agents-core' ,
'commands-core' ,
'platform-configs' ,
'skill-unified-memory' ,
'workflow-quality'
]
2026-03-25 04:00:50 -04:00
);
assert . ok ( state . resolution . skippedModules . includes ( 'hooks-runtime' ));
assert . ok ( ! state . resolution . skippedModules . includes ( 'workflow-quality' ));
assert . ok ( ! state . resolution . skippedModules . includes ( 'platform-configs' ));
2026-03-15 21:47:22 -07:00
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-03-14 12:55:25 -07:00
if ( test ( 'installs explicit modules for cursor using manifest operations' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
2026-08-07 15:18:14 -04:00
const result = run ([ '--target' , 'cursor' , '--modules' , 'platform-configs' , '--enable-hooks' ], {
2026-03-14 12:55:25 -07:00
cwd : projectDir ,
homeDir ,
});
assert . strictEqual ( result . code , 0 , result . stderr );
assert . ok ( fs . existsSync ( path . join ( projectDir , '.cursor' , 'hooks.json' )));
2026-04-12 23:51:58 -07:00
assert . ok ( fs . existsSync ( path . join ( projectDir , '.cursor' , 'rules' , 'common-agents.mdc' )));
assert . ok ( ! fs . existsSync ( path . join ( projectDir , '.cursor' , 'rules' , 'common-agents.md' )));
2026-03-14 12:55:25 -07:00
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 ++ ;
2026-03-15 21:47:22 -07:00
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 ++ ;
2026-08-12 00:43:58 -04:00
if ( test ( 'installs claude hooks and defaults commit attribution off' , () => {
2026-03-27 07:52:03 -04:00
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
2026-08-07 15:18:14 -04:00
const result = run ([ '--profile' , 'core' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-03-27 07:52:03 -04:00
assert . strictEqual ( result . code , 0 , result . stderr );
const claudeRoot = path . join ( homeDir , '.claude' );
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'hooks' , 'hooks.json' )), 'hooks.json should be copied' );
2026-08-12 00:43:58 -04:00
assert . deepStrictEqual (
readJson ( path . join ( claudeRoot , 'settings.json' )),
{ includeCoAuthoredBy : false }
);
2026-03-27 07:52:03 -04:00
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-04-14 20:24:21 -07:00
if ( test ( 'installs claude hooks with the safe plugin bootstrap contract' , () => {
2026-04-01 16:04:56 -07:00
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
2026-08-07 15:18:14 -04:00
const result = run ([ '--profile' , 'core' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-04-01 16:04:56 -07:00
assert . strictEqual ( result . code , 0 , result . stderr );
const claudeRoot = path . join ( homeDir , '.claude' );
const installedHooks = readJson ( path . join ( claudeRoot , 'hooks' , 'hooks.json' ));
2026-04-14 21:23:57 -07:00
const installedBashDispatcherEntry = installedHooks . hooks . PreToolUse . find ( entry => entry . id === 'pre:bash:dispatcher' );
assert . ok ( installedBashDispatcherEntry , 'hooks/hooks.json should include the consolidated Bash dispatcher hook' );
2026-04-15 19:33:34 -07:00
assert . strictEqual ( typeof installedBashDispatcherEntry . hooks [ 0 ]. command , 'string' , 'hooks/hooks.json should install string-form commands for Claude Code schema compatibility' );
2026-04-01 16:04:56 -07:00
assert . ok (
2026-04-15 19:33:34 -07:00
installedBashDispatcherEntry . hooks [ 0 ]. command . startsWith ( 'node -e ' ),
2026-04-14 20:24:21 -07:00
'hooks/hooks.json should use the inline node bootstrap contract'
2026-04-01 16:04:56 -07:00
);
assert . ok (
2026-04-15 19:33:34 -07:00
installedBashDispatcherEntry . hooks [ 0 ]. command . includes ( 'plugin-hook-bootstrap.js' ),
2026-04-14 20:24:21 -07:00
'hooks/hooks.json should route plugin-managed hooks through the shared bootstrap'
);
assert . ok (
2026-04-15 19:33:34 -07:00
installedBashDispatcherEntry . hooks [ 0 ]. command . includes ( 'CLAUDE_PLUGIN_ROOT' ),
2026-04-14 20:24:21 -07:00
'hooks/hooks.json should still consult CLAUDE_PLUGIN_ROOT for runtime resolution'
);
assert . ok (
2026-04-15 19:33:34 -07:00
installedBashDispatcherEntry . hooks [ 0 ]. command . includes ( 'pre-bash-dispatcher.js' ),
2026-04-14 21:23:57 -07:00
'hooks/hooks.json should point the Bash preflight contract at the consolidated dispatcher'
);
2026-05-19 05:05:26 -04:00
assert . ok (
! installedBashDispatcherEntry . hooks [ 0 ]. command . includes ( '\\"' ),
'hooks/hooks.json should avoid escaped double quotes that break Windows Git Bash parsing'
);
2026-04-14 21:23:57 -07:00
assert . ok (
2026-04-15 19:33:34 -07:00
! installedBashDispatcherEntry . hooks [ 0 ]. command . includes ( '${CLAUDE_PLUGIN_ROOT}' ),
2026-04-14 20:24:21 -07:00
'hooks/hooks.json should not retain raw CLAUDE_PLUGIN_ROOT shell placeholders after install'
2026-04-01 16:04:56 -07:00
);
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-08-12 00:43:58 -04:00
if ( test ( 'preserves existing settings.json while disabling Claude co-author attribution' , () => {
2026-03-27 07:52:03 -04:00
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
const claudeRoot = path . join ( homeDir , '.claude' );
fs . mkdirSync ( claudeRoot , { recursive : true });
fs . writeFileSync (
path . join ( claudeRoot , 'settings.json' ),
JSON . stringify ({
effortLevel : 'high' ,
env : { MY_VAR : '1' },
hooks : {
PreToolUse : [{ matcher : 'Write' , hooks : [{ type : 'command' , command : 'echo custom-pretool' }] }],
UserPromptSubmit : [{ matcher : '*' , hooks : [{ type : 'command' , command : 'echo custom-submit' }] }],
},
}, null , 2 )
);
2026-08-07 15:18:14 -04:00
const result = run ([ '--profile' , 'core' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-03-27 07:52:03 -04:00
assert . strictEqual ( result . code , 0 , result . stderr );
const settings = readJson ( path . join ( claudeRoot , 'settings.json' ));
assert . strictEqual ( settings . effortLevel , 'high' , 'existing effortLevel should be preserved' );
2026-08-12 00:43:58 -04:00
assert . strictEqual ( settings . includeCoAuthoredBy , false , 'Claude co-author attribution should be disabled by default' );
2026-03-27 07:52:03 -04:00
assert . deepStrictEqual ( settings . env , { MY_VAR : '1' }, 'existing env should be preserved' );
assert . deepStrictEqual (
settings . hooks . UserPromptSubmit ,
[{ matcher : '*' , hooks : [{ type : 'command' , command : 'echo custom-submit' }] }],
2026-04-12 22:39:48 -07:00
'existing hooks should be left untouched'
);
assert . deepStrictEqual (
settings . hooks . PreToolUse ,
[{ matcher : 'Write' , hooks : [{ type : 'command' , command : 'echo custom-pretool' }] }],
'managed Claude hooks should not be injected into settings.json'
2026-03-27 07:52:03 -04:00
);
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-04-05 14:37:28 -07:00
if ( test ( 'filters copied mcp config files when ECC_DISABLED_MCPS is set' , () => {
const tempDir = createTempDir ( 'install-apply-mcp-' );
const sourcePath = path . join ( tempDir , '.mcp.json' );
const destinationPath = path . join ( tempDir , 'installed' , '.mcp.json' );
const installStatePath = path . join ( tempDir , 'installed' , 'ecc-install-state.json' );
const previousValue = process . env . ECC_DISABLED_MCPS ;
try {
fs . mkdirSync ( path . dirname ( sourcePath ), { recursive : true });
fs . writeFileSync ( sourcePath , JSON . stringify ({
mcpServers : {
github : { command : 'npx' },
exa : { url : 'https://mcp.exa.ai/mcp' },
memory : { command : 'npx' },
},
}, null , 2 ));
process . env . ECC_DISABLED_MCPS = 'github,memory' ;
applyInstallPlan ({
targetRoot : path . join ( tempDir , 'installed' ),
installStatePath ,
statePreview : {
schemaVersion : 'ecc.install.v1' ,
installedAt : new Date (). toISOString (),
target : {
id : 'test-install' ,
kind : 'project' ,
root : path . join ( tempDir , 'installed' ),
installStatePath ,
},
request : {
profile : null ,
modules : [ 'test-mcp' ],
includeComponents : [],
excludeComponents : [],
legacyLanguages : [],
legacyMode : false ,
},
resolution : {
selectedModules : [ 'test-mcp' ],
skippedModules : [],
},
source : {
repoVersion : null ,
repoCommit : null ,
manifestVersion : 1 ,
},
operations : [],
},
operations : [{
kind : 'copy-file' ,
moduleId : 'test-mcp' ,
sourcePath ,
sourceRelativePath : '.mcp.json' ,
destinationPath ,
strategy : 'preserve-relative-path' ,
ownership : 'managed' ,
scaffoldOnly : false ,
}],
});
const installed = readJson ( destinationPath );
assert . deepStrictEqual ( Object . keys ( installed . mcpServers ), [ 'exa' ]);
} finally {
if ( previousValue === undefined ) {
delete process . env . ECC_DISABLED_MCPS ;
} else {
process . env . ECC_DISABLED_MCPS = previousValue ;
}
cleanup ( tempDir );
}
})) passed ++ ; else failed ++ ;
2026-08-12 00:43:58 -04:00
if ( test ( 'reinstall keeps commit attribution disabled when only managed hooks are installed' , () => {
2026-03-27 07:52:03 -04:00
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
2026-08-07 15:18:14 -04:00
const firstInstall = run ([ '--profile' , 'core' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-03-27 07:52:03 -04:00
assert . strictEqual ( firstInstall . code , 0 , firstInstall . stderr );
2026-08-07 15:18:14 -04:00
const secondInstall = run ([ '--profile' , 'core' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-03-27 07:52:03 -04:00
assert . strictEqual ( secondInstall . code , 0 , secondInstall . stderr );
2026-08-12 00:43:58 -04:00
assert . deepStrictEqual (
readJson ( path . join ( homeDir , '.claude' , 'settings.json' )),
{ includeCoAuthoredBy : false }
);
2026-03-27 07:52:03 -04:00
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-08-12 00:43:58 -04:00
if ( test ( 'reinstall leaves pre-existing hook-based settings.json untouched apart from co-author preference' , () => {
2026-04-01 02:16:32 -07:00
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
2026-04-12 22:39:48 -07:00
const claudeRoot = path . join ( homeDir , '.claude' );
fs . mkdirSync ( claudeRoot , { recursive : true });
const settingsPath = path . join ( claudeRoot , 'settings.json' );
const legacySettings = {
hooks : {
PreToolUse : [{ matcher : 'Write' , hooks : [{ type : 'command' , command : 'echo legacy-pretool' }] }],
},
};
2026-04-01 02:16:32 -07:00
fs . writeFileSync ( settingsPath , JSON . stringify ( legacySettings , null , 2 ));
2026-08-07 15:18:14 -04:00
const secondInstall = run ([ '--profile' , 'core' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-04-01 02:16:32 -07:00
assert . strictEqual ( secondInstall . code , 0 , secondInstall . stderr );
const afterSecondInstall = readJson ( settingsPath );
2026-08-12 00:43:58 -04:00
assert . deepStrictEqual ( afterSecondInstall , {
... legacySettings ,
includeCoAuthoredBy : false ,
});
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'reinstall preserves an explicit includeCoAuthoredBy opt-in' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
const claudeRoot = path . join ( homeDir , '.claude' );
fs . mkdirSync ( claudeRoot , { recursive : true });
const settingsPath = path . join ( claudeRoot , 'settings.json' );
const customSettings = {
includeCoAuthoredBy : true ,
theme : 'dark' ,
};
fs . writeFileSync ( settingsPath , JSON . stringify ( customSettings , null , 2 ));
2026-08-07 15:18:14 -04:00
const install = run ([ '--profile' , 'core' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-08-12 00:43:58 -04:00
assert . strictEqual ( install . code , 0 , install . stderr );
const afterInstall = readJson ( settingsPath );
assert . deepStrictEqual ( afterInstall , customSettings );
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'reinstall preserves an explicit attribution opt-in' , () => {
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
const claudeRoot = path . join ( homeDir , '.claude' );
fs . mkdirSync ( claudeRoot , { recursive : true });
const settingsPath = path . join ( claudeRoot , 'settings.json' );
// `attribution` supersedes `includeCoAuthoredBy` in Claude Code, so writing
// the deprecated key here would be dead config that loses to the user's choice.
const customSettings = {
attribution : { commit : 'Signed-off-by: Someone <someone@example.com>' },
theme : 'dark' ,
};
fs . writeFileSync ( settingsPath , JSON . stringify ( customSettings , null , 2 ));
2026-08-07 15:18:14 -04:00
const install = run ([ '--profile' , 'core' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-08-12 00:43:58 -04:00
assert . strictEqual ( install . code , 0 , install . stderr );
const afterInstall = readJson ( settingsPath );
assert . deepStrictEqual ( afterInstall , customSettings );
2026-04-01 02:16:32 -07:00
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-04-12 22:39:48 -07:00
if ( test ( 'ignores malformed existing settings.json during claude install' , () => {
2026-03-27 07:52:03 -04:00
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
const claudeRoot = path . join ( homeDir , '.claude' );
fs . mkdirSync ( claudeRoot , { recursive : true });
const settingsPath = path . join ( claudeRoot , 'settings.json' );
fs . writeFileSync ( settingsPath , '{ invalid json\n' );
2026-08-07 15:18:14 -04:00
const result = run ([ '--profile' , 'core' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-04-12 22:39:48 -07:00
assert . strictEqual ( result . code , 0 , result . stderr );
2026-03-27 07:52:03 -04:00
assert . strictEqual ( fs . readFileSync ( settingsPath , 'utf8' ), '{ invalid json\n' );
2026-04-12 22:39:48 -07:00
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'hooks' , 'hooks.json' )), 'hooks.json should still be copied' );
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'ecc' , 'install-state.json' )), 'install state should still be written' );
2026-03-27 07:52:03 -04:00
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-04-12 22:39:48 -07:00
if ( test ( 'ignores non-object existing settings.json during claude install' , () => {
2026-03-27 07:59:19 -04:00
const homeDir = createTempDir ( 'install-apply-home-' );
const projectDir = createTempDir ( 'install-apply-project-' );
try {
const claudeRoot = path . join ( homeDir , '.claude' );
fs . mkdirSync ( claudeRoot , { recursive : true });
const settingsPath = path . join ( claudeRoot , 'settings.json' );
fs . writeFileSync ( settingsPath , '[]\n' );
2026-08-07 15:18:14 -04:00
const result = run ([ '--profile' , 'core' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-04-12 22:39:48 -07:00
assert . strictEqual ( result . code , 0 , result . stderr );
2026-03-27 07:59:19 -04:00
assert . strictEqual ( fs . readFileSync ( settingsPath , 'utf8' ), '[]\n' );
2026-04-12 22:39:48 -07:00
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'hooks' , 'hooks.json' )), 'hooks.json should still be copied' );
assert . ok ( fs . existsSync ( path . join ( claudeRoot , 'ecc' , 'install-state.json' )), 'install state should still be written' );
2026-03-27 07:59:19 -04:00
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-03-28 19:55:28 -04:00
if ( test ( 'fails when source hooks.json root is not an object before copying files' , () => {
2026-04-12 22:47:25 -07:00
const tempDir = createTempDir ( 'install-apply-invalid-hooks-' );
const targetRoot = path . join ( tempDir , '.claude' );
const installStatePath = path . join ( targetRoot , 'ecc' , 'install-state.json' );
const sourceHooksPath = path . join ( tempDir , 'hooks.json' );
2026-03-28 19:55:28 -04:00
try {
fs . writeFileSync ( sourceHooksPath , '[]\n' );
2026-04-12 22:47:25 -07:00
assert . throws (() => {
applyInstallPlan ({
targetRoot ,
installStatePath ,
2026-08-07 15:18:14 -04:00
hookConsent : 'enabled' ,
2026-04-12 22:47:25 -07:00
statePreview : {
schemaVersion : 'ecc.install.v1' ,
installedAt : new Date (). toISOString (),
target : {
id : 'claude-home' ,
kind : 'home' ,
root : targetRoot ,
installStatePath ,
},
request : {
profile : 'core' ,
modules : [],
includeComponents : [],
excludeComponents : [],
legacyLanguages : [],
legacyMode : false ,
},
resolution : {
selectedModules : [ 'hooks-runtime' ],
skippedModules : [],
},
source : {
repoVersion : null ,
repoCommit : null ,
manifestVersion : 1 ,
},
operations : [],
},
adapter : { target : 'claude' },
operations : [{
kind : 'copy-file' ,
moduleId : 'hooks-runtime' ,
sourcePath : sourceHooksPath ,
sourceRelativePath : 'hooks/hooks.json' ,
destinationPath : path . join ( targetRoot , 'hooks' , 'hooks.json' ),
strategy : 'preserve-relative-path' ,
ownership : 'managed' ,
scaffoldOnly : false ,
}],
});
}, /Invalid hooks config at .*expected a JSON object/ );
2026-03-28 19:55:28 -04:00
2026-04-12 22:47:25 -07:00
assert . ok ( ! fs . existsSync ( path . join ( targetRoot , 'hooks' , 'hooks.json' )), 'hooks.json should not be copied when source hooks are invalid' );
assert . ok ( ! fs . existsSync ( installStatePath ), 'install state should not be written when source hooks are invalid' );
2026-03-28 19:55:28 -04:00
} finally {
2026-04-12 22:47:25 -07:00
cleanup ( tempDir );
2026-03-28 19:55:28 -04:00
}
})) passed ++ ; else failed ++ ;
2026-03-14 12:55:25 -07:00
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 ));
2026-08-07 15:18:14 -04:00
const result = run ([ '--config' , configPath , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-03-14 12:55:25 -07:00
assert . strictEqual ( result . code , 0 , result . stderr );
2026-07-26 03:20:06 -07:00
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' )));
2026-03-14 12:55:25 -07:00
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 ++ ;
2026-03-24 03:53:45 -07:00
if ( test ( 'auto-detects ecc-install.json from the project root' , () => {
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 ));
2026-08-07 15:18:14 -04:00
const result = run ([ '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-03-24 03:53:45 -07:00
assert . strictEqual ( result . code , 0 , result . stderr );
2026-07-26 03:20:06 -07:00
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' )));
2026-03-24 03:53:45 -07:00
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 ++ ;
if ( test ( 'preserves legacy language installs when a project config is present' , () => {
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' ],
}, null , 2 ));
2026-08-07 15:18:14 -04:00
const result = run ([ 'typescript' , '--enable-hooks' ], { cwd : projectDir , homeDir });
2026-03-24 03:53:45 -07:00
assert . strictEqual ( result . code , 0 , result . stderr );
const state = readJson ( path . join ( homeDir , '.claude' , 'ecc' , 'install-state.json' ));
assert . strictEqual ( state . request . legacyMode , true );
assert . deepStrictEqual ( state . request . legacyLanguages , [ 'typescript' ]);
assert . strictEqual ( state . request . profile , null );
assert . deepStrictEqual ( state . request . includeComponents , []);
assert . ok ( state . resolution . selectedModules . includes ( 'framework-language' ));
assert . ok ( ! state . resolution . selectedModules . includes ( 'security' ));
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-08-07 15:18:14 -04:00
if ( test ( 'holds hook materialization without an explicit hook decision' , () => {
const projectDir = createTempDir ( 'install-apply-consent-held-' );
const homeDir = createTempDir ( 'install-apply-consent-held-home-' );
try {
const result = run ([ '--profile' , 'core' ], { cwd : projectDir , homeDir });
assert . notStrictEqual ( result . code , 0 );
assert . ok ( result . stderr . includes ( 'automatic hook runtime' ));
assert . ok ( result . stderr . includes ( '--enable-hooks' ));
assert . ok ( ! fs . existsSync ( path . join ( homeDir , '.claude' , 'hooks' , 'hooks.json' )));
assert . ok ( ! fs . existsSync ( path . join ( homeDir , '.claude' , 'ecc' , 'install-state.json' )));
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
if ( test ( '--no-hooks installs the profile without the hook runtime' , () => {
const projectDir = createTempDir ( 'install-apply-no-hooks-' );
const homeDir = createTempDir ( 'install-apply-no-hooks-home-' );
try {
const result = run ([ '--profile' , 'core' , '--no-hooks' ], { cwd : projectDir , homeDir });
assert . strictEqual ( result . code , 0 , result . stderr );
assert . ok ( ! fs . existsSync ( path . join ( homeDir , '.claude' , 'hooks' , 'hooks.json' )));
const state = readJson ( path . join ( homeDir , '.claude' , 'ecc' , 'install-state.json' ));
assert . strictEqual ( state . request . hookConsent , 'declined' );
assert . ok ( ! state . resolution . selectedModules . includes ( 'hooks-runtime' ));
assert . ok ( state . resolution . selectedModules . includes ( 'rules-core' ));
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
if ( test ( 'rejects --enable-hooks combined with --no-hooks' , () => {
const result = run ([ '--profile' , 'core' , '--enable-hooks' , '--no-hooks' ]);
assert . notStrictEqual ( result . code , 0 );
assert . ok ( result . stderr . includes ( 'mutually exclusive' ));
})) passed ++ ; else failed ++ ;
if ( test ( 'dry-run surfaces the pending hook decision as a warning' , () => {
const projectDir = createTempDir ( 'install-apply-consent-dry-' );
const homeDir = createTempDir ( 'install-apply-consent-dry-home-' );
try {
const result = run ([ '--profile' , 'core' , '--dry-run' ], { cwd : projectDir , homeDir });
assert . strictEqual ( result . code , 0 , result . stderr );
assert . ok ( result . stdout . includes ( 'explicit hook decision' ));
} finally {
cleanup ( homeDir );
cleanup ( projectDir );
}
})) passed ++ ; else failed ++ ;
2026-03-14 12:55:25 -07:00
console . log ( `\nResults: Passed: ${ passed } , Failed: ${ failed } ` );
process . exit ( failed > 0 ? 1 : 0 );
}
runTests ();