diff --git a/commands/auto-update.md b/commands/auto-update.md index b1f39d17d..d685bd24a 100644 --- a/commands/auto-update.md +++ b/commands/auto-update.md @@ -11,7 +11,7 @@ Update ECC from its upstream repo and regenerate the current context's managed i ```bash # Preview the update without mutating anything -ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i { + // A partial install can carry full resolver evidence (script tree plus + // sentinel ECC skill, per #2544/#2577) yet still lack scripts/auto-update.js. + // The command's inline resolver must probe for the script it actually + // executes so such roots don't shadow the complete plugin root. + const autoUpdateDocs = [ + path.join(__dirname, '..', '..', 'commands', 'auto-update.md'), + path.join(__dirname, '..', '..', 'docs', 'ja-JP', 'commands', 'auto-update.md'), + path.join(__dirname, '..', '..', 'docs', 'zh-CN', 'commands', 'auto-update.md'), + ]; + for (const docPath of autoUpdateDocs) { + const doc = fs.readFileSync(docPath, 'utf8'); + assert.strictEqual( + (doc.match(/scripts','lib','resolve-ecc-root/g) || []).length, 1, + `${docPath} should embed the shared inline resolver` + ); + assert.ok( + doc.includes("resolveEccRoot({probe:p.join('scripts','auto-update.js')})"), + `${docPath} should probe for scripts/auto-update.js` + ); + } +}); + test('resolveEccRoot module covers current and legacy marketplace plugin roots', () => { const { resolveEccRoot } = require('../../scripts/lib/resolve-ecc-root'); assert.ok(typeof resolveEccRoot === 'function'); diff --git a/tests/lib/resolve-ecc-root.test.js b/tests/lib/resolve-ecc-root.test.js index 36a8c0c13..25a53773c 100644 --- a/tests/lib/resolve-ecc-root.test.js +++ b/tests/lib/resolve-ecc-root.test.js @@ -363,6 +363,37 @@ function runTests() { } })) passed++; else failed++; + if (test('custom probe skips a qualifying root that lacks the probed script (auto-update)', () => { + // The surviving failure shape after #2544/#2577: a partial install can + // carry full resolver evidence (script tree + sentinel ECC skill) yet + // still lack the top-level script that auto-update will execute. The + // default probe rightly accepts such a root; a caller probing for the + // script it runs must skip it and reach the complete plugin root. + const homeDir = createTempDir(); + try { + const claudeDir = setupStandardInstall(homeDir); + const marketplaceRoot = setupLegacyPluginInstall(homeDir, ['marketplaces', 'ecc']); + fs.writeFileSync(path.join(marketplaceRoot, 'scripts', 'auto-update.js'), '// stub'); + + assert.strictEqual( + resolveEccRoot({ envRoot: '', homeDir }), + claudeDir, + 'default probe accepts a root with full resolver evidence' + ); + assert.strictEqual( + resolveEccRoot({ + envRoot: '', + homeDir, + probe: path.join('scripts', 'auto-update.js'), + }), + marketplaceRoot, + 'auto-update probe must skip roots that lack the script it will execute' + ); + } finally { + fs.rmSync(homeDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + // ─── INLINE_RESOLVE ─── if (test('INLINE_RESOLVE is a non-empty string', () => {