everything-claude-code/tests/lib/install-targets.test.js
Gaurav Dubey ee9e5a19c4
fix(install-targets): validate compiled OpenCode plugin before install (#2041)
Fail fast when the OpenCode home install is attempted from a source checkout without the compiled .opencode/dist payload. PR had the full CI matrix green.
2026-05-25 14:07:52 -04:00

1102 lines
43 KiB
JavaScript

/**
* Tests for scripts/lib/install-targets/registry.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
getInstallTargetAdapter,
listInstallTargetAdapters,
planInstallTargetScaffold,
} = require('../../scripts/lib/install-targets/registry');
function normalizedRelativePath(value) {
return String(value || '').replace(/\\/g, '/');
}
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-target adapters ===\n');
let passed = 0;
let failed = 0;
if (test('lists supported target adapters', () => {
const adapters = listInstallTargetAdapters();
const targets = adapters.map(adapter => adapter.target);
assert.ok(targets.includes('claude'), 'Should include claude target');
assert.ok(targets.includes('claude-project'), 'Should include claude-project target');
assert.ok(targets.includes('cursor'), 'Should include cursor target');
assert.ok(targets.includes('antigravity'), 'Should include antigravity target');
assert.ok(targets.includes('codex'), 'Should include codex target');
assert.ok(targets.includes('gemini'), 'Should include gemini target');
assert.ok(targets.includes('opencode'), 'Should include opencode target');
assert.ok(targets.includes('codebuddy'), 'Should include codebuddy target');
assert.ok(targets.includes('joycode'), 'Should include joycode target');
assert.ok(targets.includes('qwen'), 'Should include qwen target');
assert.ok(targets.includes('zed'), 'Should include zed target');
})) passed++; else failed++;
if (test('resolves cursor adapter root and install-state path from project root', () => {
const adapter = getInstallTargetAdapter('cursor');
const projectRoot = '/workspace/app';
const root = adapter.resolveRoot({ projectRoot });
const statePath = adapter.getInstallStatePath({ projectRoot });
assert.strictEqual(root, path.join(projectRoot, '.cursor'));
assert.strictEqual(statePath, path.join(projectRoot, '.cursor', 'ecc-install-state.json'));
})) passed++; else failed++;
if (test('resolves claude adapter root and install-state path from home dir', () => {
const adapter = getInstallTargetAdapter('claude');
const homeDir = '/Users/example';
const root = adapter.resolveRoot({ homeDir, repoRoot: '/repo/ecc' });
const statePath = adapter.getInstallStatePath({ homeDir, repoRoot: '/repo/ecc' });
assert.strictEqual(root, path.join(homeDir, '.claude'));
assert.strictEqual(statePath, path.join(homeDir, '.claude', 'ecc', 'install-state.json'));
})) passed++; else failed++;
if (test('plans claude rules and skills under ECC-managed subdirectories', () => {
const repoRoot = path.join(__dirname, '..', '..');
const homeDir = '/Users/example';
const plan = planInstallTargetScaffold({
target: 'claude',
repoRoot,
homeDir,
modules: [
{
id: 'rules-core',
paths: ['rules'],
},
{
id: 'workflow-quality',
paths: ['skills/tdd-workflow'],
},
],
});
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules'
&& operation.destinationPath === path.join(homeDir, '.claude', 'rules', 'ecc')
)),
'Should install bundled Claude rules under rules/ecc'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'skills/tdd-workflow'
&& operation.destinationPath === path.join(homeDir, '.claude', 'skills', 'ecc', 'tdd-workflow')
)),
'Should install bundled Claude skills under skills/ecc'
);
})) passed++; else failed++;
if (test('plans scaffold operations and flattens native target roots', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const modules = [
{
id: 'platform-configs',
paths: ['.cursor', 'mcp-configs'],
},
{
id: 'rules-core',
paths: ['rules'],
},
];
const plan = planInstallTargetScaffold({
target: 'cursor',
repoRoot,
projectRoot,
modules,
});
assert.strictEqual(plan.adapter.id, 'cursor-project');
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.cursor'));
assert.strictEqual(plan.installStatePath, path.join(projectRoot, '.cursor', 'ecc-install-state.json'));
const hooksJson = plan.operations.find(operation => (
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/hooks.json'
));
const mcpJson = plan.operations.find(operation => (
normalizedRelativePath(operation.sourceRelativePath) === '.mcp.json'
));
const preserved = plan.operations.find(operation => (
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/rules/common-coding-style.md'
));
assert.ok(hooksJson, 'Should preserve non-rule Cursor platform config files');
assert.strictEqual(hooksJson.strategy, 'preserve-relative-path');
assert.strictEqual(hooksJson.destinationPath, path.join(projectRoot, '.cursor', 'hooks.json'));
assert.ok(mcpJson, 'Should materialize a Cursor MCP config from the shared root MCP config');
assert.strictEqual(mcpJson.kind, 'merge-json');
assert.strictEqual(mcpJson.strategy, 'merge-json');
assert.strictEqual(mcpJson.destinationPath, path.join(projectRoot, '.cursor', 'mcp.json'));
assert.ok(preserved, 'Should include flattened Cursor rule scaffold operations');
assert.strictEqual(preserved.strategy, 'flatten-copy');
assert.strictEqual(
preserved.destinationPath,
path.join(projectRoot, '.cursor', 'rules', 'common-coding-style.mdc')
);
})) passed++; else failed++;
if (test('plans cursor rules with flat namespaced filenames to avoid rule collisions', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'cursor',
repoRoot,
projectRoot,
modules: [
{
id: 'rules-core',
paths: ['rules'],
},
],
});
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md'
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-coding-style.mdc')
)),
'Should flatten common rules into namespaced .mdc files'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules/typescript/testing.md'
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'typescript-testing.mdc')
)),
'Should flatten language rules into namespaced .mdc files'
);
assert.ok(
!plan.operations.some(operation => (
operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common', 'coding-style.md')
)),
'Should not preserve nested rule directories for cursor installs'
);
assert.ok(
!plan.operations.some(operation => (
operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-coding-style.md')
)),
'Should not emit .md Cursor rule files'
);
assert.ok(
!plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules/README.md'
)),
'Should not install Cursor README docs as runtime rule files'
);
assert.ok(
!plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules/zh/README.md'
)),
'Should not flatten localized README docs into Cursor rule files'
);
})) passed++; else failed++;
if (test('does not install root AGENTS.md into Cursor nested context', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'cursor',
repoRoot,
projectRoot,
modules: [
{
id: 'agents-core',
paths: ['.agents', 'agents', 'AGENTS.md'],
},
],
});
assert.ok(
!plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'AGENTS.md'
)),
'Cursor installs should not copy ECC root AGENTS.md into host project context'
);
assert.ok(
!plan.operations.some(operation => (
operation.destinationPath === path.join(projectRoot, '.cursor', 'AGENTS.md')
)),
'Cursor installs should not create .cursor/AGENTS.md'
);
})) passed++; else failed++;
if (test('plans cursor agents with ecc-prefixed filenames to avoid agent collisions', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'cursor',
repoRoot,
projectRoot,
modules: [
{
id: 'agents-core',
paths: ['agents'],
},
],
});
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'agents/architect.md'
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'agents', 'ecc-architect.md')
)),
'Should prefix Cursor agent files with ecc-'
);
assert.ok(
!plan.operations.some(operation => (
operation.destinationPath === path.join(projectRoot, '.cursor', 'agents', 'architect.md')
)),
'Should not write bare Cursor agent filenames'
);
assert.ok(
!plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'agents'
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'agents')
)),
'Should not plan a whole-directory Cursor agent copy'
);
})) passed++; else failed++;
if (test('plans cursor platform rule files as .mdc and excludes rule README docs', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'cursor',
repoRoot,
projectRoot,
modules: [
{
id: 'platform-configs',
paths: ['.cursor'],
},
],
});
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/rules/common-agents.md'
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-agents.mdc')
)),
'Should rename Cursor platform rule files to .mdc'
);
assert.ok(
!plan.operations.some(operation => (
operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-agents.md')
)),
'Should not preserve .md Cursor platform rule files'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/hooks.json'
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'hooks.json')
)),
'Should preserve non-rule Cursor platform config files'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === '.mcp.json'
&& operation.kind === 'merge-json'
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'mcp.json')
)),
'Should materialize a project-level Cursor MCP config'
);
assert.ok(
!plan.operations.some(operation => (
operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'README.mdc')
)),
'Should not emit Cursor rule README docs as .mdc files'
);
})) passed++; else failed++;
if (test('deduplicates cursor rule destinations when rules-core and platform-configs overlap', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'cursor',
repoRoot,
projectRoot,
modules: [
{
id: 'rules-core',
paths: ['rules'],
},
{
id: 'platform-configs',
paths: ['.cursor'],
},
],
});
const commonAgentsDestinations = plan.operations.filter(operation => (
operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-agents.mdc')
));
assert.strictEqual(commonAgentsDestinations.length, 1, 'Should keep only one common-agents.mdc operation');
assert.strictEqual(
normalizedRelativePath(commonAgentsDestinations[0].sourceRelativePath),
'.cursor/rules/common-agents.md',
'Should prefer native .cursor/rules content when cursor platform rules would collide'
);
})) passed++; else failed++;
if (test('prefers native cursor hooks when hooks-runtime and platform-configs overlap', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'cursor',
repoRoot,
projectRoot,
modules: [
{
id: 'hooks-runtime',
paths: ['hooks', 'scripts/hooks', 'scripts/lib'],
},
{
id: 'platform-configs',
paths: ['.cursor'],
},
],
});
const hooksDestinations = plan.operations.filter(operation => (
operation.destinationPath === path.join(projectRoot, '.cursor', 'hooks')
));
assert.strictEqual(hooksDestinations.length, 1, 'Should keep only one .cursor/hooks scaffold operation');
assert.strictEqual(
normalizedRelativePath(hooksDestinations[0].sourceRelativePath),
'.cursor/hooks',
'Should prefer native Cursor hooks over generic hooks-runtime hooks'
);
})) passed++; else failed++;
if (test('plans antigravity remaps for workflows, skills, and flat rules', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'antigravity',
repoRoot,
projectRoot,
modules: [
{
id: 'commands-core',
paths: ['commands'],
},
{
id: 'agents-core',
paths: ['agents'],
},
{
id: 'rules-core',
paths: ['rules'],
},
],
});
assert.ok(
plan.operations.some(operation => (
operation.sourceRelativePath === 'commands'
&& operation.destinationPath === path.join(projectRoot, '.agent', 'workflows')
)),
'Should remap commands into workflows'
);
assert.ok(
plan.operations.some(operation => (
operation.sourceRelativePath === 'agents'
&& operation.destinationPath === path.join(projectRoot, '.agent', 'skills')
)),
'Should remap agents into skills'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md'
&& operation.destinationPath === path.join(projectRoot, '.agent', 'rules', 'common-coding-style.md')
)),
'Should flatten common rules for antigravity'
);
})) passed++; else failed++;
if (test('exposes validate and planOperations on adapters', () => {
const claudeAdapter = getInstallTargetAdapter('claude');
const cursorAdapter = getInstallTargetAdapter('cursor');
assert.strictEqual(typeof claudeAdapter.planOperations, 'function');
assert.strictEqual(typeof claudeAdapter.validate, 'function');
assert.deepStrictEqual(
claudeAdapter.validate({ homeDir: '/Users/example', repoRoot: '/repo/ecc' }),
[]
);
assert.strictEqual(typeof cursorAdapter.planOperations, 'function');
assert.strictEqual(typeof cursorAdapter.validate, 'function');
assert.deepStrictEqual(
cursorAdapter.validate({ projectRoot: '/workspace/app', repoRoot: '/repo/ecc' }),
[]
);
})) passed++; else failed++;
if (test('throws on unknown target adapter', () => {
assert.throws(
() => getInstallTargetAdapter('ghost-target'),
/Unknown install target adapter/
);
})) passed++; else failed++;
if (test('resolves codebuddy adapter root and install-state path from project root', () => {
const adapter = getInstallTargetAdapter('codebuddy');
const projectRoot = '/workspace/app';
const root = adapter.resolveRoot({ projectRoot });
const statePath = adapter.getInstallStatePath({ projectRoot });
assert.strictEqual(adapter.id, 'codebuddy-project');
assert.strictEqual(adapter.target, 'codebuddy');
assert.strictEqual(adapter.kind, 'project');
assert.strictEqual(root, path.join(projectRoot, '.codebuddy'));
assert.strictEqual(statePath, path.join(projectRoot, '.codebuddy', 'ecc-install-state.json'));
})) passed++; else failed++;
if (test('resolves gemini adapter root and install-state path from project root', () => {
const adapter = getInstallTargetAdapter('gemini');
const projectRoot = '/workspace/app';
const root = adapter.resolveRoot({ projectRoot });
const statePath = adapter.getInstallStatePath({ projectRoot });
assert.strictEqual(adapter.id, 'gemini-project');
assert.strictEqual(adapter.target, 'gemini');
assert.strictEqual(adapter.kind, 'project');
assert.strictEqual(root, path.join(projectRoot, '.gemini'));
assert.strictEqual(statePath, path.join(projectRoot, '.gemini', 'ecc-install-state.json'));
})) passed++; else failed++;
if (test('codebuddy adapter supports lookup by target and adapter id', () => {
const byTarget = getInstallTargetAdapter('codebuddy');
const byId = getInstallTargetAdapter('codebuddy-project');
assert.strictEqual(byTarget.id, 'codebuddy-project');
assert.strictEqual(byId.id, 'codebuddy-project');
assert.ok(byTarget.supports('codebuddy'));
assert.ok(byTarget.supports('codebuddy-project'));
})) passed++; else failed++;
if (test('resolves joycode adapter root and install-state path from project root', () => {
const adapter = getInstallTargetAdapter('joycode');
const projectRoot = '/workspace/app';
const root = adapter.resolveRoot({ projectRoot });
const statePath = adapter.getInstallStatePath({ projectRoot });
assert.strictEqual(adapter.id, 'joycode-project');
assert.strictEqual(adapter.target, 'joycode');
assert.strictEqual(adapter.kind, 'project');
assert.strictEqual(root, path.join(projectRoot, '.joycode'));
assert.strictEqual(statePath, path.join(projectRoot, '.joycode', 'ecc-install-state.json'));
})) passed++; else failed++;
if (test('joycode adapter supports lookup by target and adapter id', () => {
const byTarget = getInstallTargetAdapter('joycode');
const byId = getInstallTargetAdapter('joycode-project');
assert.strictEqual(byTarget.id, 'joycode-project');
assert.strictEqual(byId.id, 'joycode-project');
assert.ok(byTarget.supports('joycode'));
assert.ok(byTarget.supports('joycode-project'));
})) passed++; else failed++;
if (test('resolves qwen adapter root and install-state path from home dir', () => {
const adapter = getInstallTargetAdapter('qwen');
const homeDir = '/Users/example';
const root = adapter.resolveRoot({ homeDir });
const statePath = adapter.getInstallStatePath({ homeDir });
assert.strictEqual(adapter.id, 'qwen-home');
assert.strictEqual(adapter.target, 'qwen');
assert.strictEqual(adapter.kind, 'home');
assert.strictEqual(root, path.join(homeDir, '.qwen'));
assert.strictEqual(statePath, path.join(homeDir, '.qwen', 'ecc-install-state.json'));
})) passed++; else failed++;
if (test('qwen adapter supports lookup by target and adapter id', () => {
const byTarget = getInstallTargetAdapter('qwen');
const byId = getInstallTargetAdapter('qwen-home');
assert.strictEqual(byTarget.id, 'qwen-home');
assert.strictEqual(byId.id, 'qwen-home');
assert.ok(byTarget.supports('qwen'));
assert.ok(byTarget.supports('qwen-home'));
})) passed++; else failed++;
if (test('resolves zed adapter root and install-state path from project root', () => {
const adapter = getInstallTargetAdapter('zed');
const projectRoot = '/workspace/app';
const root = adapter.resolveRoot({ projectRoot });
const statePath = adapter.getInstallStatePath({ projectRoot });
assert.strictEqual(adapter.id, 'zed-project');
assert.strictEqual(adapter.target, 'zed');
assert.strictEqual(adapter.kind, 'project');
assert.strictEqual(root, path.join(projectRoot, '.zed'));
assert.strictEqual(statePath, path.join(projectRoot, '.zed', 'ecc-install-state.json'));
})) passed++; else failed++;
if (test('zed adapter supports lookup by target and adapter id', () => {
const byTarget = getInstallTargetAdapter('zed');
const byId = getInstallTargetAdapter('zed-project');
assert.strictEqual(byTarget.id, 'zed-project');
assert.strictEqual(byId.id, 'zed-project');
assert.ok(byTarget.supports('zed'));
assert.ok(byTarget.supports('zed-project'));
})) passed++; else failed++;
if (test('plans codebuddy rules with flat namespaced filenames', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'codebuddy',
repoRoot,
projectRoot,
modules: [
{
id: 'rules-core',
paths: ['rules'],
},
],
});
assert.strictEqual(plan.adapter.id, 'codebuddy-project');
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.codebuddy'));
assert.strictEqual(plan.installStatePath, path.join(projectRoot, '.codebuddy', 'ecc-install-state.json'));
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md'
&& operation.destinationPath === path.join(projectRoot, '.codebuddy', 'rules', 'common-coding-style.md')
)),
'Should flatten common rules into namespaced files for codebuddy'
);
assert.ok(
!plan.operations.some(operation => (
operation.destinationPath === path.join(projectRoot, '.codebuddy', 'rules', 'common', 'coding-style.md')
)),
'Should not preserve nested rule directories for codebuddy installs'
);
})) passed++; else failed++;
if (test('plans joycode commands, agents, skills, and flattened rules', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'joycode',
repoRoot,
projectRoot,
modules: [
{
id: 'rules-core',
paths: ['rules'],
},
{
id: 'agents-core',
paths: ['agents'],
},
{
id: 'commands-core',
paths: ['commands'],
},
{
id: 'workflow-quality',
paths: ['skills/tdd-workflow'],
},
],
});
assert.strictEqual(plan.adapter.id, 'joycode-project');
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.joycode'));
assert.strictEqual(plan.installStatePath, path.join(projectRoot, '.joycode', 'ecc-install-state.json'));
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md'
&& operation.destinationPath === path.join(projectRoot, '.joycode', 'rules', 'common-coding-style.md')
)),
'Should flatten common rules into namespaced files for joycode'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'agents'
&& operation.destinationPath === path.join(projectRoot, '.joycode', 'agents')
)),
'Should install agents under .joycode/agents'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'commands'
&& operation.destinationPath === path.join(projectRoot, '.joycode', 'commands')
)),
'Should install commands under .joycode/commands'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'skills/tdd-workflow'
&& operation.destinationPath === path.join(projectRoot, '.joycode', 'skills', 'tdd-workflow')
)),
'Should install skills under .joycode/skills'
);
})) passed++; else failed++;
if (test('plans qwen commands, agents, skills, and native config under home root', () => {
const repoRoot = path.join(__dirname, '..', '..');
const homeDir = '/Users/example';
const plan = planInstallTargetScaffold({
target: 'qwen',
repoRoot,
homeDir,
modules: [
{
id: 'rules-core',
paths: ['rules'],
},
{
id: 'agents-core',
paths: ['agents'],
},
{
id: 'commands-core',
paths: ['commands'],
},
{
id: 'platform-configs',
paths: ['.qwen', '.gemini', 'mcp-configs'],
},
{
id: 'workflow-quality',
paths: ['skills/tdd-workflow'],
},
],
});
assert.strictEqual(plan.adapter.id, 'qwen-home');
assert.strictEqual(plan.targetRoot, path.join(homeDir, '.qwen'));
assert.strictEqual(plan.installStatePath, path.join(homeDir, '.qwen', 'ecc-install-state.json'));
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules'
&& operation.destinationPath === path.join(homeDir, '.qwen', 'rules')
)),
'Should preserve rules under ~/.qwen/rules'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === '.qwen'
&& operation.destinationPath === path.join(homeDir, '.qwen')
&& operation.strategy === 'sync-root-children'
)),
'Should sync Qwen native config into ~/.qwen'
);
assert.ok(
!plan.operations.some(operation => normalizedRelativePath(operation.sourceRelativePath) === '.gemini'),
'Should skip foreign platform config paths'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'skills/tdd-workflow'
&& operation.destinationPath === path.join(homeDir, '.qwen', 'skills', 'tdd-workflow')
)),
'Should install skills under ~/.qwen/skills'
);
})) passed++; else failed++;
if (test('plans zed project settings, commands, agents, skills, and flattened rules', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'zed',
repoRoot,
projectRoot,
modules: [
{
id: 'rules-core',
paths: ['rules'],
},
{
id: 'agents-core',
paths: ['agents'],
},
{
id: 'commands-core',
paths: ['commands'],
},
{
id: 'platform-configs',
paths: ['.zed', '.cursor', 'mcp-configs'],
},
{
id: 'workflow-quality',
paths: ['skills/tdd-workflow'],
},
],
});
assert.strictEqual(plan.adapter.id, 'zed-project');
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.zed'));
assert.strictEqual(plan.installStatePath, path.join(projectRoot, '.zed', 'ecc-install-state.json'));
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === '.zed'
&& operation.destinationPath === path.join(projectRoot, '.zed')
&& operation.strategy === 'sync-root-children'
)),
'Should sync Zed native project settings into .zed'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md'
&& operation.destinationPath === path.join(projectRoot, '.zed', 'rules', 'common-coding-style.md')
)),
'Should flatten common rules into namespaced files for zed'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'agents'
&& operation.destinationPath === path.join(projectRoot, '.zed', 'agents')
)),
'Should install agents under .zed/agents'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'commands'
&& operation.destinationPath === path.join(projectRoot, '.zed', 'commands')
)),
'Should install commands under .zed/commands'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'skills/tdd-workflow'
&& operation.destinationPath === path.join(projectRoot, '.zed', 'skills', 'tdd-workflow')
)),
'Should install skills under .zed/skills'
);
assert.ok(
!plan.operations.some(operation => normalizedRelativePath(operation.sourceRelativePath) === '.cursor'),
'Should skip foreign Cursor platform config paths'
);
})) passed++; else failed++;
if (test('exposes validate and planOperations on codebuddy adapter', () => {
const codebuddyAdapter = getInstallTargetAdapter('codebuddy');
assert.strictEqual(typeof codebuddyAdapter.planOperations, 'function');
assert.strictEqual(typeof codebuddyAdapter.validate, 'function');
assert.deepStrictEqual(
codebuddyAdapter.validate({ projectRoot: '/workspace/app', repoRoot: '/repo/ecc' }),
[]
);
})) passed++; else failed++;
if (test('every schema target enum value has a matching adapter (regression guard)', () => {
const schemaPath = path.join(__dirname, '..', '..', 'schemas', 'ecc-install-config.schema.json');
const schema = JSON.parse(require('fs').readFileSync(schemaPath, 'utf8'));
const schemaTargets = schema.properties.target.enum;
const adapters = listInstallTargetAdapters();
const adapterTargets = adapters.map(a => a.target);
for (const target of schemaTargets) {
assert.ok(
adapterTargets.includes(target),
`Schema target "${target}" has no matching adapter. ` +
`Available adapter targets: ${adapterTargets.join(', ')}`
);
}
})) passed++; else failed++;
if (test('every adapter target is listed in the schema enum (regression guard)', () => {
const schemaPath = path.join(__dirname, '..', '..', 'schemas', 'ecc-install-config.schema.json');
const schema = JSON.parse(require('fs').readFileSync(schemaPath, 'utf8'));
const schemaTargets = schema.properties.target.enum;
const adapters = listInstallTargetAdapters();
for (const adapter of adapters) {
assert.ok(
schemaTargets.includes(adapter.target),
`Adapter target "${adapter.target}" is not in schema enum. ` +
`Schema targets: ${schemaTargets.join(', ')}`
);
}
})) passed++; else failed++;
if (test('every adapter target is in SUPPORTED_INSTALL_TARGETS (regression guard)', () => {
const { SUPPORTED_INSTALL_TARGETS } = require('../../scripts/lib/install-manifests');
const adapters = listInstallTargetAdapters();
for (const adapter of adapters) {
assert.ok(
SUPPORTED_INSTALL_TARGETS.includes(adapter.target),
`Adapter target "${adapter.target}" is not in SUPPORTED_INSTALL_TARGETS. ` +
`Supported: ${SUPPORTED_INSTALL_TARGETS.join(', ')}`
);
}
})) passed++; else failed++;
if (test('resolves claude-project adapter root and install-state path from project root', () => {
const adapter = getInstallTargetAdapter('claude-project');
const projectRoot = '/workspace/app';
const root = adapter.resolveRoot({ projectRoot });
const statePath = adapter.getInstallStatePath({ projectRoot });
assert.strictEqual(adapter.id, 'claude-project');
assert.strictEqual(adapter.target, 'claude-project');
assert.strictEqual(adapter.kind, 'project');
assert.strictEqual(root, path.join(projectRoot, '.claude'));
assert.strictEqual(statePath, path.join(projectRoot, '.claude', 'ecc', 'install-state.json'));
})) passed++; else failed++;
if (test('claude-project adapter supports lookup by target and adapter id', () => {
const byTarget = getInstallTargetAdapter('claude-project');
const byId = getInstallTargetAdapter('claude-project');
assert.strictEqual(byTarget.id, 'claude-project');
assert.strictEqual(byId.id, 'claude-project');
assert.ok(byTarget.supports('claude-project'));
})) passed++; else failed++;
if (test('plans claude-project rules and skills under project-scope ECC-managed subdirectories', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'claude-project',
repoRoot,
projectRoot,
modules: [
{
id: 'rules-core',
paths: ['rules'],
},
{
id: 'workflow-quality',
paths: ['skills/tdd-workflow'],
},
],
});
assert.strictEqual(plan.adapter.id, 'claude-project');
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.claude'));
assert.strictEqual(plan.installStatePath, path.join(projectRoot, '.claude', 'ecc', 'install-state.json'));
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules'
&& operation.destinationPath === path.join(projectRoot, '.claude', 'rules', 'ecc')
)),
'Should install bundled rules under project-scope rules/ecc'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'skills/tdd-workflow'
&& operation.destinationPath === path.join(projectRoot, '.claude', 'skills', 'ecc', 'tdd-workflow')
)),
'Should install bundled skills under project-scope skills/ecc'
);
})) passed++; else failed++;
if (test('claude-project skips foreign platform source paths', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';
const plan = planInstallTargetScaffold({
target: 'claude-project',
repoRoot,
projectRoot,
modules: [
{
id: 'platform-configs',
paths: ['.cursor', '.zed', 'rules'],
},
],
});
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules'
&& operation.destinationPath === path.join(projectRoot, '.claude', 'rules', 'ecc')
)),
'Should still include non-foreign rules path (guards against empty-plan regression)'
);
assert.ok(
!plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === '.cursor'
|| normalizedRelativePath(operation.sourceRelativePath).startsWith('.cursor/')
)),
'Should skip foreign Cursor platform paths'
);
assert.ok(
!plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === '.zed'
|| normalizedRelativePath(operation.sourceRelativePath).startsWith('.zed/')
)),
'Should skip foreign Zed platform paths'
);
})) passed++; else failed++;
if (test('resolves opencode adapter root and install-state path from home dir', () => {
const adapter = getInstallTargetAdapter('opencode');
const homeDir = '/Users/example';
const root = adapter.resolveRoot({ homeDir });
const statePath = adapter.getInstallStatePath({ homeDir });
assert.strictEqual(adapter.id, 'opencode-home');
assert.strictEqual(adapter.target, 'opencode');
assert.strictEqual(adapter.kind, 'home');
assert.strictEqual(root, path.join(homeDir, '.opencode'));
assert.strictEqual(statePath, path.join(homeDir, '.opencode', 'ecc-install-state.json'));
})) passed++; else failed++;
if (test('opencode adapter validate reports an error when compiled plugin is missing', () => {
const adapter = getInstallTargetAdapter('opencode');
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'install-targets-opencode-missing-'));
try {
const issues = adapter.validate({ homeDir: '/Users/example', repoRoot });
assert.strictEqual(issues.length, 1, 'Should surface exactly one validation issue');
assert.strictEqual(issues[0].severity, 'error');
assert.strictEqual(issues[0].code, 'opencode-plugin-not-built');
assert.ok(
issues[0].message.includes('.opencode/dist') || issues[0].message.includes('.opencode\\dist'),
'Validation message should reference the .opencode/dist payload location'
);
assert.ok(
issues[0].message.includes('build-opencode.js') || issues[0].message.includes('build:opencode'),
'Validation message should hint at the build command'
);
assert.ok(Array.isArray(issues[0].missingRelativePaths) && issues[0].missingRelativePaths.length >= 1,
'Validation issue should expose the list of missing artefacts as metadata');
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('opencode adapter validate reports a partial build (entry present, runtime dirs absent)', () => {
const adapter = getInstallTargetAdapter('opencode');
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'install-targets-opencode-partial-'));
try {
const distDir = path.join(repoRoot, '.opencode', 'dist');
fs.mkdirSync(distDir, { recursive: true });
fs.writeFileSync(path.join(distDir, 'index.js'), '// stub\n');
// Intentionally omit dist/plugins and dist/tools.
const issues = adapter.validate({ homeDir: '/Users/example', repoRoot });
assert.strictEqual(issues.length, 1, 'Should surface a single validation issue for partial builds');
assert.strictEqual(issues[0].code, 'opencode-plugin-not-built');
const missing = issues[0].missingRelativePaths.map(p => p.replace(/\\/g, '/'));
assert.ok(missing.includes('.opencode/dist/plugins'), 'Missing list should include dist/plugins');
assert.ok(missing.includes('.opencode/dist/tools'), 'Missing list should include dist/tools');
assert.ok(!missing.includes('.opencode/dist/index.js'), 'Missing list should not include the present entry');
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('opencode adapter validate rejects wrong artefact type (file where directory expected)', () => {
const adapter = getInstallTargetAdapter('opencode');
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'install-targets-opencode-wrongtype-'));
try {
const distDir = path.join(repoRoot, '.opencode', 'dist');
fs.mkdirSync(distDir, { recursive: true });
fs.writeFileSync(path.join(distDir, 'index.js'), '// stub\n');
// Materialize plugins/tools as files instead of directories.
fs.writeFileSync(path.join(distDir, 'plugins'), 'not-a-dir');
fs.writeFileSync(path.join(distDir, 'tools'), 'not-a-dir');
const issues = adapter.validate({ homeDir: '/Users/example', repoRoot });
assert.strictEqual(issues.length, 1, 'Wrong-type artefacts should still surface a validation issue');
assert.strictEqual(issues[0].code, 'opencode-plugin-not-built');
const missing = issues[0].missingRelativePaths.map(p => p.replace(/\\/g, '/'));
assert.ok(missing.includes('.opencode/dist/plugins'), 'Should flag plugins file as wrong type');
assert.ok(missing.includes('.opencode/dist/tools'), 'Should flag tools file as wrong type');
assert.ok(!missing.includes('.opencode/dist/index.js'), 'Should not flag index.js when it is correctly a file');
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('opencode adapter validate handles ENOTDIR (intermediate path is a file) without throwing', () => {
const adapter = getInstallTargetAdapter('opencode');
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'install-targets-opencode-enotdir-'));
try {
// Create `.opencode/dist` as a regular file. Stat'ing
// `.opencode/dist/index.js` then throws ENOTDIR (intermediate component
// is a file, not a directory). The validate gate must treat this as a
// missing artefact and surface the structured opencode-plugin-not-built
// issue, not propagate the raw fs error.
const opencodeDir = path.join(repoRoot, '.opencode');
fs.mkdirSync(opencodeDir, { recursive: true });
fs.writeFileSync(path.join(opencodeDir, 'dist'), 'not-a-dir');
let issues;
assert.doesNotThrow(
() => { issues = adapter.validate({ homeDir: '/Users/example', repoRoot }); },
'validate should swallow ENOTDIR and surface a structured issue'
);
assert.strictEqual(issues.length, 1, 'ENOTDIR case should produce exactly one validation issue');
assert.strictEqual(issues[0].severity, 'error');
assert.strictEqual(issues[0].code, 'opencode-plugin-not-built');
const missing = issues[0].missingRelativePaths.map(p => p.replace(/\\/g, '/'));
assert.ok(missing.includes('.opencode/dist/index.js'), 'ENOTDIR target should be reported as missing');
assert.ok(missing.includes('.opencode/dist/plugins'), 'Sibling artefacts under the bad path should be reported');
assert.ok(missing.includes('.opencode/dist/tools'), 'Sibling artefacts under the bad path should be reported');
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('opencode adapter validate passes once compiled plugin payload exists', () => {
const adapter = getInstallTargetAdapter('opencode');
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'install-targets-opencode-built-'));
try {
const distDir = path.join(repoRoot, '.opencode', 'dist');
fs.mkdirSync(path.join(distDir, 'plugins'), { recursive: true });
fs.mkdirSync(path.join(distDir, 'tools'), { recursive: true });
fs.writeFileSync(path.join(distDir, 'index.js'), '// stub\n');
const issues = adapter.validate({ homeDir: '/Users/example', repoRoot });
assert.deepStrictEqual(issues, [], 'Should not surface validation issues when plugin is built');
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();