mirror of
https://github.com/affaan-m/ECC.git
synced 2026-11-05 23:55:50 +00:00
${CLAUDE_PLUGIN_ROOT:-$PWD} fell back to the user's project directory when
the plugin variable was absent, so standalone installs and standalone ECC
checkouts hit 'Cannot find module' — the same failure #3530 reported.
The three skill commands now probe CLAUDE_PLUGIN_ROOT, then a standalone
install ($CLAUDE_CONFIG_DIR or ~/.claude), then an ECC checkout run from its
root, verifying the script exists at each step and failing with an explicit
message when none resolves. Paths stay quoted so plugin roots containing
spaces work.
218 lines
8.3 KiB
JavaScript
218 lines
8.3 KiB
JavaScript
/**
|
|
* Skill script invocations must resolve the ECC root instead of assuming the
|
|
* user's project directory contains ECC's scripts (#3530).
|
|
*
|
|
* The documented commands have to work in every install mode:
|
|
* - plugin install (CLAUDE_PLUGIN_ROOT set; the path may contain spaces)
|
|
* - standalone install (~/.claude or $CLAUDE_CONFIG_DIR)
|
|
* - ECC checkout run from its root
|
|
* and when none of those resolves, the command must stop with an explicit
|
|
* error instead of a misleading `Cannot find module` from a guessed path.
|
|
*
|
|
* The executed cases run the exact bash block published in tdd-workflow from an
|
|
* unrelated project directory against a real copy of the ECC scripts.
|
|
*
|
|
* Run with: node tests/skills/plugin-root-invocations.test.js
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const { spawnSync } = require('child_process');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const repoRoot = path.resolve(__dirname, '..', '..');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function skip(reason) {
|
|
const error = new Error(reason);
|
|
error.code = 'SKIP';
|
|
throw error;
|
|
}
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
process.stdout.write(` PASS ${name}\n`);
|
|
passed += 1;
|
|
} catch (error) {
|
|
if (error && error.code === 'SKIP') {
|
|
process.stdout.write(` SKIP ${name}: ${error.message}\n`);
|
|
return;
|
|
}
|
|
process.stdout.write(` FAIL ${name}: ${error.message}\n`);
|
|
failed += 1;
|
|
}
|
|
}
|
|
|
|
function readSkill(name) {
|
|
return fs.readFileSync(path.join(repoRoot, 'skills', name, 'SKILL.md'), 'utf8');
|
|
}
|
|
|
|
function bashBlocks(markdown) {
|
|
return [...markdown.matchAll(/```bash\r?\n([\s\S]*?)```/g)].map((match) => match[1]);
|
|
}
|
|
|
|
function blockContaining(markdown, needle) {
|
|
const block = bashBlocks(markdown).find((candidate) => candidate.includes(needle));
|
|
assert.ok(block, `expected a bash block containing "${needle}"`);
|
|
return block;
|
|
}
|
|
|
|
function escapeRegExp(value) {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
const RESOLVER_SNIPPET =
|
|
'ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}}"';
|
|
|
|
function assertResolvesScript(skillName, script) {
|
|
const skill = readSkill(skillName);
|
|
assert.ok(skill.includes(RESOLVER_SNIPPET),
|
|
`${skillName} must probe the plugin root, the standalone install and the checkout`);
|
|
assert.match(skill,
|
|
new RegExp(`\\[ -f "\\$ECC_ROOT/scripts/${escapeRegExp(script)}" \\] \\|\\| ECC_ROOT="\\$PWD"`),
|
|
`${skillName} must verify the resolved script exists before falling back to the checkout`);
|
|
assert.match(skill, new RegExp(`node "\\$ECC_ROOT/scripts/${escapeRegExp(script)}"`),
|
|
`${skillName} must run the script from the resolved ECC root`);
|
|
assert.doesNotMatch(skill, /\$\{CLAUDE_PLUGIN_ROOT:-\$PWD\}/,
|
|
`${skillName} must not fall back to the user's project directory`);
|
|
assert.doesNotMatch(skill, new RegExp(`^\\s*node scripts/${escapeRegExp(script)}`, 'm'),
|
|
`${skillName} must not assume ECC scripts live in the current directory`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Static checks: the three skills document the resolving form.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
process.stdout.write('\nSkill plugin-root script invocation tests:\n\n');
|
|
|
|
test('tdd-workflow resolves the package-manager detector through the ECC root', () => {
|
|
assertResolvesScript('tdd-workflow', 'setup-package-manager.js');
|
|
});
|
|
|
|
test('autonomous-loops resolves claw.js through the ECC root', () => {
|
|
assertResolvesScript('autonomous-loops', 'claw.js');
|
|
const skill = readSkill('autonomous-loops');
|
|
assert.match(skill,
|
|
/CLAW_SESSION=my-project CLAW_SKILLS=tdd-workflow,security-review node "\$ECC_ROOT\/scripts\/claw\.js"/);
|
|
});
|
|
|
|
test('dmux-workflows resolves orchestrate-worktrees.js through the ECC root', () => {
|
|
assertResolvesScript('dmux-workflows', 'orchestrate-worktrees.js');
|
|
});
|
|
|
|
test('seedPaths examples stay relative to the user repository', () => {
|
|
const skill = readSkill('dmux-workflows');
|
|
assert.match(skill, /"scripts\/orchestrate-worktrees\.js"/);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Executed checks: run the documented command against a real plugin root.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function createFixture() {
|
|
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-plugin-root-invocations-'));
|
|
const pluginRoot = path.join(base, 'plugin root with spaces');
|
|
const standaloneHome = path.join(base, 'home dir with spaces');
|
|
const emptyHome = path.join(base, 'empty home');
|
|
const userProject = path.join(base, 'user project');
|
|
|
|
for (const scriptsParent of [pluginRoot, path.join(standaloneHome, '.claude')]) {
|
|
fs.mkdirSync(scriptsParent, { recursive: true });
|
|
fs.cpSync(path.join(repoRoot, 'scripts'), path.join(scriptsParent, 'scripts'), {
|
|
recursive: true,
|
|
});
|
|
}
|
|
fs.mkdirSync(emptyHome, { recursive: true });
|
|
fs.mkdirSync(userProject, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(userProject, 'package.json'),
|
|
'{"name":"unrelated-project","version":"1.0.0"}\n');
|
|
fs.writeFileSync(path.join(userProject, 'pnpm-lock.yaml'), 'lockfileVersion: "9.0"\n');
|
|
|
|
assert.ok(!fs.existsSync(path.join(userProject, 'scripts')),
|
|
'fixture precondition: the user project must not contain ECC scripts');
|
|
return { base, pluginRoot, standaloneHome, emptyHome, userProject };
|
|
}
|
|
|
|
const fixture = createFixture();
|
|
|
|
function runDetector({ pluginRoot, home, cwd }) {
|
|
const block = blockContaining(
|
|
readSkill('tdd-workflow'),
|
|
'node "$ECC_ROOT/scripts/setup-package-manager.js" --detect');
|
|
const env = { ...process.env, HOME: home, CLAUDE_CONFIG_DIR: '' };
|
|
if (pluginRoot) {
|
|
env.CLAUDE_PLUGIN_ROOT = pluginRoot;
|
|
} else {
|
|
delete env.CLAUDE_PLUGIN_ROOT;
|
|
}
|
|
delete env.CLAUDE_PACKAGE_MANAGER;
|
|
return spawnSync('bash', ['-c', block], { cwd, env, encoding: 'utf8', timeout: 60000 });
|
|
}
|
|
|
|
function assertDetectorRanInProject(result) {
|
|
assert.strictEqual(result.error, undefined, `detector failed to start: ${result.error}`);
|
|
assert.strictEqual(result.status, 0,
|
|
`detector exited ${result.status}: ${result.stderr || result.stdout}`);
|
|
assert.match(result.stdout, /Package Manager Detection/);
|
|
// The detection output must come from the unrelated project directory (its
|
|
// pnpm lockfile), proving the helper ran there rather than in an ECC checkout.
|
|
assert.match(result.stdout, /From lock file: pnpm/);
|
|
assert.doesNotMatch(result.stderr, /Cannot find module/);
|
|
}
|
|
|
|
test('documented detector command runs from an unrelated project through a plugin root with spaces', () => {
|
|
if (process.platform === 'win32') skip('executed fixture requires a POSIX shell');
|
|
const result = runDetector({
|
|
pluginRoot: fixture.pluginRoot,
|
|
home: fixture.emptyHome,
|
|
cwd: fixture.userProject,
|
|
});
|
|
assertDetectorRanInProject(result);
|
|
});
|
|
|
|
test('documented detector command falls back to the standalone install when CLAUDE_PLUGIN_ROOT is unset', () => {
|
|
if (process.platform === 'win32') skip('executed fixture requires a POSIX shell');
|
|
const result = runDetector({
|
|
pluginRoot: null,
|
|
home: fixture.standaloneHome,
|
|
cwd: fixture.userProject,
|
|
});
|
|
assertDetectorRanInProject(result);
|
|
});
|
|
|
|
test('documented detector command falls back to an ECC checkout run from its root', () => {
|
|
if (process.platform === 'win32') skip('executed fixture requires a POSIX shell');
|
|
const result = runDetector({
|
|
pluginRoot: null,
|
|
home: fixture.emptyHome,
|
|
cwd: repoRoot,
|
|
});
|
|
assert.strictEqual(result.status, 0, `detector exited ${result.status}: ${result.stderr}`);
|
|
assert.match(result.stdout, /Package Manager Detection/);
|
|
});
|
|
|
|
test('documented detector command fails loudly when no ECC root resolves', () => {
|
|
if (process.platform === 'win32') skip('executed fixture requires a POSIX shell');
|
|
const result = runDetector({
|
|
pluginRoot: null,
|
|
home: fixture.emptyHome,
|
|
cwd: fixture.userProject,
|
|
});
|
|
assert.strictEqual(result.status, 1,
|
|
`expected an explicit failure, got exit ${result.status}: ${result.stdout}`);
|
|
assert.match(result.stderr, /ECC scripts not found/);
|
|
assert.doesNotMatch(result.stderr, /Cannot find module/);
|
|
});
|
|
|
|
fs.rmSync(fixture.base, { recursive: true, force: true });
|
|
|
|
process.stdout.write(`\nResults: Passed: ${passed}, Failed: ${failed}\n`);
|
|
process.exitCode = failed ? 1 : 0;
|