fix: reconcile pre-exclusion .agents installs on claude and codex home upgrades

This commit is contained in:
Affaan Mustafa
2026-09-18 20:13:11 -04:00
parent da214d73b7
commit 8cfbc26797
4 changed files with 389 additions and 2 deletions
+129
View File
@@ -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-');