mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-20 06:51:44 +02:00
fix: protect commands-only CommonJS runtime in Claude installs
Reuse the existing scripts boundary asset and guard for commands-core without hooks. Preflight selected helper scopes and preserve user module metadata, unowned/edited JS interpretation, and boundary ownership through uninstall. Actual installed ESM help/JSON, receipt, reinstall/uninstall and 34 focused cases pass. OpenCode boundary 8/8, adapters 51/51, manifests 43/43; focused coverage exceeds 80%. Two root-owned legacy ESM core-profile fixtures require correction; no unsupported states weakened.
This commit is contained in:
@@ -15,9 +15,10 @@ let failed = 0;
|
||||
function snapshot(root) {
|
||||
if (!fs.existsSync(root)) return {};
|
||||
return Object.fromEntries(fs.readdirSync(root, { recursive: true, withFileTypes: true })
|
||||
.filter(entry => entry.isFile())
|
||||
.map(entry => { const file = path.join(entry.parentPath || entry.path, entry.name);
|
||||
return [path.relative(root, file), require('crypto').createHash('sha256').update(fs.readFileSync(file)).digest('hex')]; }).sort());
|
||||
const value = entry.isDirectory() ? '[directory]' : entry.isSymbolicLink()
|
||||
? `[symlink:${fs.readlinkSync(file)}]` : require('crypto').createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
return [path.relative(root, file), value]; }).sort());
|
||||
}
|
||||
function test(name, fn, target = 'claude-project') {
|
||||
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-command-runtime-')));
|
||||
@@ -50,9 +51,11 @@ function test(name, fn, target = 'claude-project') {
|
||||
ok(result); return result.stdout.trim();
|
||||
};
|
||||
const noPayload = () => {
|
||||
for (const file of ['settings.json', '.mcp.json', 'hooks/hooks.json', 'scripts/hooks']) {
|
||||
for (const file of ['.mcp.json', 'hooks/hooks.json', 'scripts/hooks']) {
|
||||
assert.ok(!fs.existsSync(path.join(root, file)), `Unexpected ${file}`);
|
||||
}
|
||||
const settings = path.join(root, 'settings.json');
|
||||
if (fs.existsSync(settings)) assert.ok(!JSON.parse(fs.readFileSync(settings, 'utf8')).hooks);
|
||||
assert.ok(!state().operations.some(op => ['hooks-runtime', 'mcp-configs'].includes(op.moduleId)));
|
||||
};
|
||||
try { fn({ dir, home, project, root, scripts, marker, write, install, uninstall, plan,
|
||||
@@ -154,4 +157,40 @@ test('publication rechecks nested helper package conflict', f => {
|
||||
} }), /module boundary/);
|
||||
assert.ok(!fs.existsSync(f.marker));
|
||||
});
|
||||
test('uninstall retains owned boundary for newly added user JS until it is removed', f => {
|
||||
f.write(path.join(f.project, 'package.json'), '{"type":"module"}\n');
|
||||
f.ok(f.install()); const file = path.join(f.scripts, 'user.js'); f.write(file, 'module.exports = 47;\n');
|
||||
const partial = JSON.parse(f.uninstall().stdout);
|
||||
assert.strictEqual(partial.results[0].status, 'partial');
|
||||
assert.strictEqual(partial.summary.errorCount, 0);
|
||||
assert.ok(partial.results[0].retainedPaths.includes(f.marker));
|
||||
assert.ok(fs.existsSync(f.marker)); assert.strictEqual(f.probe(file), '47');
|
||||
fs.unlinkSync(file); f.ok(f.uninstall()); assert.ok(!fs.existsSync(f.marker)); assert.ok(!fs.existsSync(f.statePath));
|
||||
});
|
||||
for (const scope of ['lib/package.json', 'lib/skill-evolution/package.json', 'ci/package.json']) {
|
||||
test(`existing CommonJS parent still rejects incompatible selected ${scope}`, f => {
|
||||
f.write(f.marker, '{"type":"commonjs","user":true}\n');
|
||||
f.write(path.join(f.scripts, scope), '{"type":"module"}\n'); const before = snapshot(f.dir);
|
||||
assert.notStrictEqual(f.install().status, 0); assert.deepStrictEqual(snapshot(f.dir), before);
|
||||
});
|
||||
}
|
||||
test('edited owned CommonJS package bytes are preserved by rejection', f => {
|
||||
f.ok(f.install()); f.write(f.marker, '{"type":"commonjs","userAdded":true}\n');
|
||||
const before = snapshot(f.dir); const result = f.install(); assert.notStrictEqual(result.status, 0);
|
||||
assert.match(result.stderr + result.stdout, /edited.*module boundary/); assert.deepStrictEqual(snapshot(f.dir), before);
|
||||
});
|
||||
for (const scope of ['package.json', 'lib/package.json']) {
|
||||
test(`symlinked ${scope} refuses before mutation`, f => {
|
||||
const file = path.join(f.scripts, scope); const outside = path.join(f.dir, 'user-package.json');
|
||||
f.write(outside, '{"type":"commonjs"}\n'); fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.symlinkSync(outside, file); const before = snapshot(f.dir);
|
||||
assert.notStrictEqual(f.install().status, 0); assert.deepStrictEqual(snapshot(f.dir), before);
|
||||
});
|
||||
}
|
||||
for (const scope of ['package.json', 'lib/package.json']) {
|
||||
test(`non-file ${scope} refuses before mutation`, f => {
|
||||
fs.mkdirSync(path.join(f.scripts, scope), { recursive: true }); const before = snapshot(f.dir);
|
||||
assert.notStrictEqual(f.install().status, 0); assert.deepStrictEqual(snapshot(f.dir), before);
|
||||
});
|
||||
}
|
||||
console.log(`Passed: ${passed}`); console.log(`Failed: ${failed}`); process.exitCode = failed ? 1 : 0;
|
||||
|
||||
Reference in New Issue
Block a user