mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-20 16:47:59 +02:00
fix: reconcile pre-exclusion .agents installs on claude and codex home upgrades
This commit is contained in:
@@ -7,6 +7,7 @@ const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync, spawnSync } = require('child_process');
|
||||
const crypto = require('crypto');
|
||||
const yaml = require('js-yaml');
|
||||
const { applyInstallPlan } = require('../../scripts/lib/install/apply');
|
||||
|
||||
@@ -641,6 +642,134 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('reconciles legacy .agents files and state operations on Claude and Codex home upgrades', () => {
|
||||
const homeDir = createTempDir('install-apply-home-');
|
||||
const projectDir = createTempDir('install-apply-project-');
|
||||
const digest = content => crypto.createHash('sha256').update(content).digest('hex');
|
||||
const legacyOperation = (destinationPath, sourceRelativePath, installedContent) => ({
|
||||
kind: 'copy-file',
|
||||
moduleId: 'agents-core',
|
||||
sourceRelativePath,
|
||||
destinationPath,
|
||||
strategy: 'preserve-relative-path',
|
||||
ownership: 'managed',
|
||||
scaffoldOnly: false,
|
||||
contentSha256: digest(installedContent),
|
||||
});
|
||||
const writeFile = (filePath, content) => {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, content);
|
||||
};
|
||||
const writeLegacyState = (statePath, target, operations) => {
|
||||
writeFile(statePath, `${JSON.stringify({
|
||||
schemaVersion: 'ecc.install.v1',
|
||||
installedAt: '2026-09-01T00:00:00.000Z',
|
||||
target,
|
||||
request: {
|
||||
profile: 'core',
|
||||
modules: [],
|
||||
includeComponents: [],
|
||||
excludeComponents: [],
|
||||
legacyLanguages: [],
|
||||
legacyMode: false,
|
||||
hookConsent: target.target === 'claude' ? 'enabled' : null,
|
||||
},
|
||||
resolution: { selectedModules: ['agents-core'], skippedModules: [] },
|
||||
source: { repoVersion: '2.2.1', repoCommit: null, manifestVersion: 1 },
|
||||
operations,
|
||||
}, null, 2)}\n`);
|
||||
};
|
||||
|
||||
try {
|
||||
// Claude home seeded as installed before the .agents exclusion.
|
||||
const claudeRoot = path.join(homeDir, '.claude');
|
||||
const claudeStatePath = path.join(claudeRoot, 'ecc', 'install-state.json');
|
||||
const claudeSkillCopy = path.join(claudeRoot, '.agents', 'skills', 'legacy-skill', 'SKILL.md');
|
||||
const claudeModifiedCopy = path.join(claudeRoot, '.agents', 'plugins', 'marketplace.json');
|
||||
const claudeUserFile = path.join(claudeRoot, '.agents', 'user-note.txt');
|
||||
writeFile(claudeSkillCopy, '# legacy skill\n');
|
||||
writeFile(claudeModifiedCopy, '{"edited": true}\n');
|
||||
writeFile(claudeUserFile, 'user notes\n');
|
||||
writeLegacyState(claudeStatePath, {
|
||||
id: 'claude-home', target: 'claude', kind: 'home',
|
||||
root: claudeRoot, installStatePath: claudeStatePath,
|
||||
}, [
|
||||
legacyOperation(claudeSkillCopy, '.agents/skills/legacy-skill/SKILL.md', '# legacy skill\n'),
|
||||
legacyOperation(claudeModifiedCopy, '.agents/plugins/marketplace.json', '{"original": true}\n'),
|
||||
]);
|
||||
|
||||
const claudeResult = run(['--profile', 'core', '--enable-hooks'], { cwd: projectDir, homeDir });
|
||||
assert.strictEqual(claudeResult.code, 0, claudeResult.stderr);
|
||||
|
||||
assert.ok(!fs.existsSync(claudeSkillCopy), 'Unchanged managed .agents file should be removed');
|
||||
assert.ok(
|
||||
claudeResult.stdout.includes(
|
||||
`- removed ${path.join(fs.realpathSync(claudeRoot), '.agents', 'skills', 'legacy-skill', 'SKILL.md')}`
|
||||
),
|
||||
'Install output should log one line per removed path'
|
||||
);
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(claudeModifiedCopy, 'utf8'),
|
||||
'{"edited": true}\n',
|
||||
'Modified managed file must be preserved'
|
||||
);
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(claudeUserFile, 'utf8'),
|
||||
'user notes\n',
|
||||
'Files the state does not own must not be touched'
|
||||
);
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(claudeRoot, '.agents', 'skills')),
|
||||
'Emptied .agents subdirectories should be pruned'
|
||||
);
|
||||
|
||||
const claudeState = readJson(claudeStatePath);
|
||||
assert.ok(
|
||||
!claudeState.operations.some(operation => (
|
||||
String(operation.sourceRelativePath || '').replace(/\\/g, '/').split('/')[0] === '.agents'
|
||||
)),
|
||||
'Claude install-state must drop the excluded .agents operations'
|
||||
);
|
||||
assert.ok(fs.existsSync(path.join(claudeRoot, 'agents', 'architect.md')));
|
||||
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'tdd-workflow', 'SKILL.md')));
|
||||
|
||||
// Codex home seeded the same way; both recorded files are unchanged.
|
||||
const codexRoot = path.join(homeDir, '.codex');
|
||||
const codexStatePath = path.join(codexRoot, 'ecc-install-state.json');
|
||||
const codexSkillCopy = path.join(codexRoot, '.agents', 'skills', 'legacy-skill', 'SKILL.md');
|
||||
const codexMarketplaceCopy = path.join(codexRoot, '.agents', 'plugins', 'marketplace.json');
|
||||
writeFile(codexSkillCopy, '# legacy skill\n');
|
||||
writeFile(codexMarketplaceCopy, '{"original": true}\n');
|
||||
writeLegacyState(codexStatePath, {
|
||||
id: 'codex-home', target: 'codex', kind: 'home',
|
||||
root: codexRoot, installStatePath: codexStatePath,
|
||||
}, [
|
||||
legacyOperation(codexSkillCopy, '.agents/skills/legacy-skill/SKILL.md', '# legacy skill\n'),
|
||||
legacyOperation(codexMarketplaceCopy, '.agents/plugins/marketplace.json', '{"original": true}\n'),
|
||||
]);
|
||||
|
||||
const codexResult = run(['--target', 'codex', '--profile', 'core'], { cwd: projectDir, homeDir });
|
||||
assert.strictEqual(codexResult.code, 0, codexResult.stderr);
|
||||
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(codexRoot, '.agents')),
|
||||
'Fully reconciled .agents directory should be pruned from the Codex home'
|
||||
);
|
||||
const codexState = readJson(codexStatePath);
|
||||
assert.ok(
|
||||
!codexState.operations.some(operation => (
|
||||
String(operation.sourceRelativePath || '').replace(/\\/g, '/').split('/')[0] === '.agents'
|
||||
)),
|
||||
'Codex install-state must drop the excluded .agents operations'
|
||||
);
|
||||
assert.ok(fs.existsSync(path.join(codexRoot, 'agents', 'architect.md')));
|
||||
assert.ok(fs.existsSync(path.join(codexRoot, 'skills', 'tdd-workflow', 'SKILL.md')));
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('preserves existing top-level Claude rules and skills during managed install', () => {
|
||||
const homeDir = createTempDir('install-apply-home-');
|
||||
const projectDir = createTempDir('install-apply-project-');
|
||||
|
||||
Reference in New Issue
Block a user