fix(resolve-ecc-root): require ECC skills, not just scripts, before accepting a root (#2544) (#2577)

* fix(resolve-ecc-root): require ECC skills, not just scripts, before accepting a root (#2544)

resolveEccRoot() accepted a candidate root on script-only evidence
(scripts/lib/utils.js). A partial install that lands ECC's scripts into
~/.claude but not ECC's skills short-circuited at the standard-install
branch, so skill-resolving callers built skills/... paths against a root
where they do not exist and every command failed three layers away.

For the default probe (skill consumers, reached via INLINE_RESOLVE) a
candidate now qualifies only if it contains both the script tree and a
sentinel ECC skill; the same stricter check guards the plugin-root and
plugin-cache branches. An explicit caller probe is still honored exactly,
so script consumers (e.g. session-start-bootstrap, which probes for the
hook runner) are unaffected. Merely checking that skills/ exists is
insufficient — a user's own ~/.claude/skills/ can be present with none of
ECC's skills.

Adds a regression test for the exact partial-install scenario and updates
the resolver test fixtures to build complete roots.

* test(resolve-ecc-root): cover partial exact-plugin and cache roots; DRY skill sentinel (#2544)

Address CodeRabbit review on PR #2577:
- Extend #2544 regression coverage to the exact-plugin and versioned
  plugin-cache branches, asserting the stricter both-sentinels predicate
  rejects a scripts-only root there too (not only for ~/.claude).
- Extract the ECC_SKILL_SENTINEL constant in command-plugin-root.test.js
  and reuse it at both fixture setup sites instead of duplicating the literal.
This commit is contained in:
Gaurav Dubey 2026-07-26 10:51:27 +05:30 committed by GitHub
parent ac30ff3ea2
commit c714dc5654
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 125 additions and 6 deletions

View file

@ -6,6 +6,10 @@ const os = require('os');
const assert = require('assert');
const { INLINE_RESOLVE } = require('../../scripts/lib/resolve-ecc-root');
// Sentinel ECC skill that resolveEccRoot() requires alongside the script tree
// before accepting a root; kept in sync with the module's DEFAULT_SKILL_PROBE.
const ECC_SKILL_SENTINEL = path.join('skills', 'continuous-learning-v2');
let passed = 0;
let failed = 0;
@ -55,6 +59,7 @@ test('resolveEccRoot module covers current and legacy marketplace plugin roots',
const legacyRoot = path.join(legacyHomeDir, '.claude', 'plugins', 'marketplaces', 'ecc');
fs.mkdirSync(path.join(legacyRoot, 'scripts', 'lib'), { recursive: true });
fs.writeFileSync(path.join(legacyRoot, 'scripts', 'lib', 'utils.js'), '// stub');
fs.mkdirSync(path.join(legacyRoot, ECC_SKILL_SENTINEL), { recursive: true });
assert.strictEqual(resolveEccRoot({ envRoot: '', homeDir: legacyHomeDir }), legacyRoot);
} finally {
fs.rmSync(legacyHomeDir, { recursive: true, force: true });
@ -65,6 +70,7 @@ test('resolveEccRoot module covers current and legacy marketplace plugin roots',
const cacheRoot = path.join(cacheHomeDir, '.claude', 'plugins', 'cache', 'ecc', 'affaan-m', '1.0.0');
fs.mkdirSync(path.join(cacheRoot, 'scripts', 'lib'), { recursive: true });
fs.writeFileSync(path.join(cacheRoot, 'scripts', 'lib', 'utils.js'), '// stub');
fs.mkdirSync(path.join(cacheRoot, ECC_SKILL_SENTINEL), { recursive: true });
assert.strictEqual(resolveEccRoot({ envRoot: '', homeDir: cacheHomeDir }), cacheRoot);
} finally {
fs.rmSync(cacheHomeDir, { recursive: true, force: true });