mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-17 23:28:04 +02:00
Addresses Greptile's review on #2994: - missing-dependency.js no longer hardcodes a second copy of the four runtime dependency versions; it reads them from package.json's dependencies field instead, so the two can't silently drift apart. describeMissingDependencyError() still recognizes a tracked dependency even if package.json can't be read for some reason, just without a version-pinned install command in that case. - The regression test now asserts no ancestor directory of its temp fixture has a node_modules, so a stray one wouldn't let Node resolve ajv/sql.js from there and mask what the test is actually meant to exercise. Also copies package.json into the fixture, matching a real plugin-marketplace git clone and what the version-lookup above now needs.
131 lines
5.0 KiB
JavaScript
131 lines
5.0 KiB
JavaScript
/**
|
|
* Regression test for https://github.com/affaan-m/ECC/issues/2822
|
|
*
|
|
* When ECC is installed through the Claude Code plugin marketplace, the
|
|
* marketplace directory is a plain git clone: `npm install` never runs, so
|
|
* node_modules never exists. This copies just the runtime files (scripts/,
|
|
* schemas/, manifests/) into a temp directory with no node_modules anywhere
|
|
* in its ancestor chain, which reproduces that install exactly, and asserts
|
|
* that the user-facing entry points named in the issue still work.
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { execFileSync } = require('child_process');
|
|
|
|
const REPO_ROOT = path.join(__dirname, '..', '..');
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${name}`);
|
|
return true;
|
|
} catch (error) {
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` Error: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function copyRuntimeFiles(destDir) {
|
|
for (const entry of ['scripts', 'schemas', 'manifests', 'package.json']) {
|
|
fs.cpSync(path.join(REPO_ROOT, entry), path.join(destDir, entry), { recursive: true });
|
|
}
|
|
}
|
|
|
|
// Node's module resolution walks up the directory tree looking for
|
|
// node_modules, so if any ancestor of pluginDir happened to have one, a
|
|
// require('ajv') from inside pluginDir could resolve there instead of
|
|
// hitting the MODULE_NOT_FOUND path this test exists to exercise. Confirm
|
|
// the fixture is actually isolated before trusting any of the results below.
|
|
function assertNoNodeModulesInAncestry(dir) {
|
|
let current = dir;
|
|
while (true) {
|
|
if (fs.existsSync(path.join(current, 'node_modules'))) {
|
|
throw new Error(
|
|
`Fixture is not isolated: ${path.join(current, 'node_modules')} exists, so this test ` +
|
|
'would resolve dependencies from there instead of exercising the missing-dependency path.'
|
|
);
|
|
}
|
|
const parent = path.dirname(current);
|
|
if (parent === current) break;
|
|
current = parent;
|
|
}
|
|
}
|
|
|
|
function run(scriptRelativePath, args, cwd) {
|
|
try {
|
|
const stdout = execFileSync('node', [path.join(cwd, scriptRelativePath), ...args], {
|
|
encoding: 'utf8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
timeout: 10000,
|
|
});
|
|
return { code: 0, stdout, stderr: '' };
|
|
} catch (error) {
|
|
return {
|
|
code: error.status ?? 1,
|
|
stdout: error.stdout || '',
|
|
stderr: error.stderr || '',
|
|
};
|
|
}
|
|
}
|
|
|
|
function runTests() {
|
|
console.log('\n=== Testing plugin install without node_modules (issue #2822) ===\n');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
// No node_modules exists anywhere above os.tmpdir(), so this faithfully
|
|
// reproduces a plugin-marketplace git clone with no dependencies installed.
|
|
const pluginDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-plugin-install-'));
|
|
|
|
try {
|
|
if (test('fixture has no node_modules anywhere in its ancestor chain', () => {
|
|
assertNoNodeModulesInAncestry(pluginDir);
|
|
})) passed++; else failed++;
|
|
|
|
copyRuntimeFiles(pluginDir);
|
|
|
|
if (test('install-plan.js --list-profiles runs without ajv installed', () => {
|
|
const result = run('scripts/install-plan.js', ['--list-profiles'], pluginDir);
|
|
assert.strictEqual(result.code, 0, `stderr: ${result.stderr}`);
|
|
assert.ok(!result.stderr.includes('Cannot find module'), `stderr: ${result.stderr}`);
|
|
assert.ok(result.stdout.includes('Install profiles'));
|
|
})) passed++; else failed++;
|
|
|
|
if (test('install-plan.js --list-modules runs without ajv installed', () => {
|
|
const result = run('scripts/install-plan.js', ['--list-modules'], pluginDir);
|
|
assert.strictEqual(result.code, 0, `stderr: ${result.stderr}`);
|
|
assert.ok(result.stdout.includes('Install modules'));
|
|
})) passed++; else failed++;
|
|
|
|
if (test('control-pane.js --help runs without sql.js installed', () => {
|
|
const result = run('scripts/control-pane.js', ['--help'], pluginDir);
|
|
assert.strictEqual(result.code, 0, `stderr: ${result.stderr}`);
|
|
assert.ok(!result.stderr.includes('Cannot find module'), `stderr: ${result.stderr}`);
|
|
assert.ok(result.stdout.includes('Usage:'));
|
|
})) passed++; else failed++;
|
|
|
|
if (test('install-plan.js --config gives an actionable error when ajv is genuinely missing', () => {
|
|
const configPath = path.join(pluginDir, 'ecc-install.json');
|
|
fs.writeFileSync(configPath, JSON.stringify({ version: 1, profile: 'minimal' }));
|
|
|
|
const result = run('scripts/install-plan.js', ['--config', configPath], pluginDir);
|
|
assert.strictEqual(result.code, 1);
|
|
assert.ok(result.stderr.includes("Missing dependency 'ajv'"), `stderr: ${result.stderr}`);
|
|
assert.ok(result.stderr.includes('npm install'), `stderr: ${result.stderr}`);
|
|
assert.ok(!result.stderr.includes('Require stack'), `stderr should not leak a raw stack trace: ${result.stderr}`);
|
|
})) passed++; else failed++;
|
|
} finally {
|
|
fs.rmSync(pluginDir, { recursive: true, force: true });
|
|
}
|
|
|
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
runTests();
|