Files
ECC/tests/lib/install-lifecycle.test.js
T
1db5c8ab4a fix(install): harden ECC installer lifecycle
Make Antigravity 2.0 installs native and safely migrate legacy state. Ensure doctor, repair, status projection, repeat installs, legacy Codex sync, and uninstall converge without losing user files. Exclude Python bytecode and harden repo-scan bootstrap guidance.

Gate publishing and pull-request merges on one exact packed artifact completing install, repeat, drift, repair, status, and uninstall across Linux, macOS, and Windows.

Co-authored-by: lorencifernando-coder <lorenci.fernando@gmail.com>

Co-authored-by: Suliman Abdulrazzaq <suliman9000a@gmail.com>

Co-authored-by: Wu Shuwen <mikewushuwen@outlook.com>
2026-08-13 16:42:51 -04:00

2929 lines
99 KiB
JavaScript

/**
* Tests for scripts/lib/install-lifecycle.js
*/
const assert = require('assert');
const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
buildDoctorReport,
discoverInstalledStates,
normalizeTargets,
repairInstalledStates,
uninstallInstalledStates,
} = require('../../scripts/lib/install-lifecycle');
const { applyInstallPlan } = require('../../scripts/lib/install/apply');
const { getInstallTargetAdapter } = require('../../scripts/lib/install-targets/registry');
const {
createInstallState,
readInstallState,
writeInstallState,
} = require('../../scripts/lib/install-state');
const REPO_ROOT = path.join(__dirname, '..', '..');
const CURRENT_PACKAGE_VERSION = JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8')
).version;
const CURRENT_MANIFEST_VERSION = JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'manifests', 'install-modules.json'), 'utf8')
).version;
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeState(filePath, options) {
const state = createInstallState(options);
writeInstallState(filePath, state);
return state;
}
function createCursorStateOptions(projectRoot, overrides = {}) {
const targetRoot = overrides.targetRoot || path.join(projectRoot, '.cursor');
const installStatePath = overrides.installStatePath || path.join(targetRoot, 'ecc-install-state.json');
return {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot,
installStatePath,
request: {
profile: null,
modules: [],
includeComponents: [],
excludeComponents: [],
legacyLanguages: ['typescript'],
legacyMode: true,
...(overrides.request || {}),
},
resolution: {
selectedModules: ['legacy-cursor-install'],
skippedModules: [],
...(overrides.resolution || {}),
},
operations: overrides.operations || [],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
...(overrides.source || {}),
},
};
}
function writeCursorState(projectRoot, overrides = {}) {
const options = createCursorStateOptions(projectRoot, overrides);
writeState(options.installStatePath, options);
return {
targetRoot: options.targetRoot,
installStatePath: options.installStatePath,
state: options,
};
}
function createOpencodeStateOptions(homeDir, overrides = {}) {
const targetRoot = overrides.targetRoot || path.join(homeDir, '.opencode');
const installStatePath = overrides.installStatePath || path.join(targetRoot, 'ecc-install-state.json');
return {
adapter: { id: 'opencode-home', target: 'opencode', kind: 'home' },
targetRoot,
installStatePath,
request: {
profile: null,
modules: ['commands-core'],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
...(overrides.request || {}),
},
resolution: {
selectedModules: ['commands-core'],
skippedModules: [],
...(overrides.resolution || {}),
},
operations: overrides.operations || [],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
...(overrides.source || {}),
},
};
}
function writeOpencodeState(homeDir, overrides = {}) {
const options = createOpencodeStateOptions(homeDir, overrides);
writeState(options.installStatePath, options);
return {
targetRoot: options.targetRoot,
installStatePath: options.installStatePath,
state: options,
};
}
function withTemporarilyMovedPath(filePath, callback) {
if (!fs.existsSync(filePath)) {
try {
return callback(null);
} finally {
if (fs.existsSync(filePath)) {
fs.rmSync(filePath, { recursive: true, force: true });
}
}
}
const backupPath = `${filePath}.backup-${process.pid}-${Date.now()}`;
fs.renameSync(filePath, backupPath);
try {
return callback(backupPath);
} finally {
if (fs.existsSync(filePath)) {
fs.rmSync(filePath, { recursive: true, force: true });
}
if (fs.existsSync(backupPath)) {
fs.renameSync(backupPath, filePath);
}
}
}
function managedOperation(kind, destinationPath, overrides = {}) {
return {
kind,
moduleId: 'test-module',
sourceRelativePath: 'rules/common/coding-style.md',
destinationPath,
strategy: kind,
ownership: 'managed',
scaffoldOnly: false,
...overrides,
};
}
function runTests() {
console.log('\n=== Testing install-lifecycle.js ===\n');
let passed = 0;
let failed = 0;
if (test('normalizes default targets and dedupes adapter aliases', () => {
const defaultTargets = normalizeTargets();
assert.ok(defaultTargets.includes('claude'));
assert.ok(defaultTargets.includes('cursor'));
assert.ok(defaultTargets.includes('codex'));
assert.deepStrictEqual(
normalizeTargets(['cursor-project', 'cursor', 'claude-home', 'claude']),
['cursor', 'claude']
);
})) passed++; else failed++;
if (test('discovers installed states for multiple targets in the current context', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const claudeStatePath = path.join(homeDir, '.claude', 'ecc', 'install-state.json');
const cursorStatePath = path.join(projectRoot, '.cursor', 'ecc-install-state.json');
writeState(claudeStatePath, {
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
targetRoot: path.join(homeDir, '.claude'),
installStatePath: claudeStatePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-claude-rules'],
skippedModules: [],
},
operations: [],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
writeState(cursorStatePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot: path.join(projectRoot, '.cursor'),
installStatePath: cursorStatePath,
request: {
profile: 'core',
modules: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['rules-core', 'platform-configs'],
skippedModules: [],
},
operations: [],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'def456',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const records = discoverInstalledStates({
homeDir,
projectRoot,
targets: ['claude', 'cursor'],
});
assert.strictEqual(records.length, 2);
assert.strictEqual(records[0].exists, true);
assert.strictEqual(records[1].exists, true);
assert.strictEqual(records[0].state.target.id, 'claude-home');
assert.strictEqual(records[1].state.target.id, 'cursor-project');
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('discovers missing and invalid install-state records', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
let records = discoverInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(records.length, 1);
assert.strictEqual(records[0].exists, false);
assert.strictEqual(records[0].state, null);
assert.strictEqual(records[0].error, null);
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(statePath, '{not-json', 'utf8');
records = discoverInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(records[0].exists, true);
assert.strictEqual(records[0].state, null);
assert.ok(records[0].error.includes('Failed to read install-state'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('doctor reports missing managed files as an error', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
fs.mkdirSync(targetRoot, { recursive: true });
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: ['platform-configs'],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['platform-configs'],
skippedModules: [],
},
operations: [
{
kind: 'copy-file',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/hooks.json',
destinationPath: path.join(targetRoot, 'hooks.json'),
strategy: 'sync-root-children',
ownership: 'managed',
scaffoldOnly: false,
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(report.results.length, 1);
assert.strictEqual(report.results[0].status, 'error');
assert.ok(report.results[0].issues.some(issue => issue.code === 'missing-managed-files'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('doctor reports target mismatches, missing sources, unverified operations, and version drift', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const actualTargetRoot = path.join(projectRoot, '.cursor');
const actualStatePath = path.join(actualTargetRoot, 'ecc-install-state.json');
const recordedTargetRoot = path.join(projectRoot, '.old-cursor');
const recordedStatePath = path.join(recordedTargetRoot, 'state.json');
const copyDestination = path.join(actualTargetRoot, 'rules', 'missing-source.md');
const customDestination = path.join(actualTargetRoot, 'custom.txt');
fs.mkdirSync(path.dirname(copyDestination), { recursive: true });
fs.writeFileSync(copyDestination, 'managed copy\n');
fs.writeFileSync(customDestination, 'custom\n');
writeState(actualStatePath, createCursorStateOptions(projectRoot, {
targetRoot: recordedTargetRoot,
installStatePath: recordedStatePath,
request: {
profile: 'missing-profile',
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: [],
skippedModules: [],
},
source: {
repoVersion: '0.0.1',
manifestVersion: CURRENT_MANIFEST_VERSION + 100,
},
operations: [
managedOperation('copy-file', copyDestination, {
sourceRelativePath: 'missing/source.md',
strategy: 'copy-file',
}),
managedOperation('custom-kind', customDestination),
],
}));
const report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
const codes = report.results[0].issues.map(issue => issue.code);
assert.strictEqual(report.results[0].status, 'error');
assert.ok(codes.includes('missing-target-root'));
assert.ok(codes.includes('target-root-mismatch'));
assert.ok(codes.includes('install-state-path-mismatch'));
assert.ok(codes.includes('missing-source-files'));
assert.ok(codes.includes('unverified-managed-operations'));
assert.ok(codes.includes('manifest-version-mismatch'));
assert.ok(codes.includes('repo-version-mismatch'));
assert.ok(codes.includes('resolution-unavailable'));
assert.strictEqual(report.summary.checkedCount, 1);
assert.ok(report.summary.errorCount >= 3);
assert.ok(report.summary.warningCount >= 4);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('doctor verifies render-template and merge-json operations by content', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const templatePath = path.join(targetRoot, 'generated.txt');
const jsonPath = path.join(targetRoot, 'settings.json');
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(templatePath, 'generated\n');
fs.writeFileSync(jsonPath, JSON.stringify({
keep: true,
nested: {
managed: true,
extra: true,
},
}, null, 2));
writeCursorState(projectRoot, {
operations: [
managedOperation('render-template', templatePath, {
renderedContent: 'generated\n',
}),
managedOperation('merge-json', jsonPath, {
mergePayload: {
nested: {
managed: true,
},
},
}),
],
});
const report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(report.results[0].status, 'ok');
assert.strictEqual(report.results[0].issues.length, 0);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('doctor classifies remove, unverified template/json, and invalid JSON operation health', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const templatePath = path.join(targetRoot, 'template.txt');
const missingPayloadJsonPath = path.join(targetRoot, 'missing-payload.json');
const invalidJsonPath = path.join(targetRoot, 'invalid.json');
const removedPath = path.join(targetRoot, 'already-removed.txt');
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(templatePath, 'generated\n');
fs.writeFileSync(missingPayloadJsonPath, '{"managed":true}\n');
fs.writeFileSync(invalidJsonPath, '{not-json', 'utf8');
writeCursorState(projectRoot, {
operations: [
managedOperation('remove', removedPath),
managedOperation('render-template', templatePath),
managedOperation('merge-json', missingPayloadJsonPath),
managedOperation('merge-json', invalidJsonPath, {
mergePayload: { managed: true },
}),
],
});
const report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
const codes = report.results[0].issues.map(issue => issue.code);
assert.strictEqual(report.results[0].status, 'warning');
assert.ok(codes.includes('unverified-managed-operations'));
assert.ok(codes.includes('drifted-managed-files'));
assert.ok(!report.results[0].issues.some(issue => issue.code === 'missing-managed-files'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('doctor reports invalid install-state files as errors', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const statePath = path.join(projectRoot, '.cursor', 'ecc-install-state.json');
fs.mkdirSync(path.dirname(statePath), { recursive: true });
fs.writeFileSync(statePath, '{"schemaVersion":"wrong"}\n');
const report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(report.results[0].status, 'error');
assert.ok(report.results[0].issues.some(issue => issue.code === 'invalid-install-state'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('doctor reports a healthy legacy install when managed files are present', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(homeDir, '.claude');
const statePath = path.join(targetRoot, 'ecc', 'install-state.json');
const managedFile = path.join(targetRoot, 'rules', 'common', 'coding-style.md');
const sourceContent = fs.readFileSync(path.join(REPO_ROOT, 'rules', 'common', 'coding-style.md'), 'utf8');
fs.mkdirSync(path.dirname(managedFile), { recursive: true });
fs.writeFileSync(managedFile, sourceContent);
writeState(statePath, {
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-claude-rules'],
skippedModules: [],
},
operations: [
{
kind: 'copy-file',
moduleId: 'legacy-claude-rules',
sourceRelativePath: 'rules/common/coding-style.md',
destinationPath: managedFile,
strategy: 'preserve-relative-path',
ownership: 'managed',
scaffoldOnly: false,
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['claude'],
});
assert.strictEqual(report.results.length, 1);
assert.strictEqual(report.results[0].status, 'ok');
assert.strictEqual(report.results[0].issues.length, 0);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair dry-run reports planned copy repairs without writing files', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const destinationPath = path.join(targetRoot, 'rules', 'coding-style.md');
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, {
sourceRelativePath: 'rules/common/coding-style.md',
strategy: 'copy-file',
}),
],
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
dryRun: true,
});
assert.strictEqual(result.dryRun, true);
assert.strictEqual(result.results[0].status, 'planned');
assert.deepStrictEqual(result.results[0].plannedRepairs, [destinationPath]);
assert.ok(!fs.existsSync(destinationPath));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('no-op repair preserves recorded source metadata until upgraded bytes are installed', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const destinationPath = path.join(targetRoot, 'rules', 'coding-style.md');
const sourcePath = path.join(REPO_ROOT, 'rules', 'common', 'coding-style.md');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.copyFileSync(sourcePath, destinationPath);
const contentSha256 = crypto.createHash('sha256')
.update(fs.readFileSync(destinationPath))
.digest('hex');
const fixture = writeCursorState(projectRoot, {
source: {
repoVersion: '1.0.0',
repoCommit: 'old-commit',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
operations: [
managedOperation('copy-file', destinationPath, {
sourceRelativePath: 'rules/common/coding-style.md',
strategy: 'copy-file',
contentSha256,
}),
],
});
const repair = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
const stateAfterRepair = readInstallState(fixture.installStatePath);
const doctor = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(repair.results[0].status, 'ok');
assert.strictEqual(repair.results[0].stateRefreshed, true);
assert.strictEqual(stateAfterRepair.source.repoVersion, '1.0.0');
assert.strictEqual(stateAfterRepair.source.manifestVersion, CURRENT_MANIFEST_VERSION);
assert.ok(doctor.results[0].issues.some(issue => issue.code === 'repo-version-mismatch'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('Claude repair and dry-run preserve user-owned flat skills during legacy migration', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(homeDir, '.claude');
const installStatePath = path.join(targetRoot, 'ecc', 'install-state.json');
const flatSkillPath = path.join(targetRoot, 'skills', 'tdd-workflow', 'SKILL.md');
const legacySkillPath = path.join(
targetRoot,
'skills',
'ecc',
'tdd-workflow',
'SKILL.md'
);
fs.mkdirSync(path.dirname(flatSkillPath), { recursive: true });
fs.mkdirSync(path.dirname(legacySkillPath), { recursive: true });
fs.writeFileSync(flatSkillPath, '# User-owned flat skill\n');
fs.writeFileSync(legacySkillPath, '# Previously managed nested skill\n');
writeState(installStatePath, {
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
targetRoot,
installStatePath,
request: {
profile: null,
modules: ['workflow-quality'],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['platform-configs', 'workflow-quality'],
skippedModules: [],
},
operations: [{
kind: 'copy-file',
moduleId: 'workflow-quality',
sourcePath: path.join(REPO_ROOT, 'skills', 'tdd-workflow', 'SKILL.md'),
sourceRelativePath: path.join('skills', 'tdd-workflow', 'SKILL.md'),
destinationPath: legacySkillPath,
strategy: 'preserve-relative-path',
ownership: 'managed',
scaffoldOnly: false,
}],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const dryRun = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['claude'],
dryRun: true,
});
assert.ok(!dryRun.results[0].plannedRepairs.includes(flatSkillPath));
assert.ok(dryRun.results[0].warnings.some(warning => warning.includes('user-owned')));
assert.strictEqual(fs.readFileSync(flatSkillPath, 'utf8'), '# User-owned flat skill\n');
assert.strictEqual(
fs.readFileSync(legacySkillPath, 'utf8'),
'# Previously managed nested skill\n'
);
const repaired = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['claude'],
});
assert.strictEqual(repaired.results[0].status, 'repaired');
assert.ok(repaired.results[0].warnings.some(warning => warning.includes('user-owned')));
assert.strictEqual(fs.readFileSync(flatSkillPath, 'utf8'), '# User-owned flat skill\n');
assert.strictEqual(
fs.readFileSync(legacySkillPath, 'utf8'),
fs.readFileSync(
path.join(REPO_ROOT, 'skills', 'tdd-workflow', 'SKILL.md'),
'utf8'
)
);
const repairedState = JSON.parse(fs.readFileSync(installStatePath, 'utf8'));
assert.ok(repairedState.operations.some(operation => (
operation.destinationPath === legacySkillPath
)));
assert.ok(!repairedState.operations.some(operation => (
operation.destinationPath === flatSkillPath
)));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('Claude repair migration derives roots from the adapter and removes only the managed legacy file', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const outsideRoot = createTempDir('install-lifecycle-outside-');
try {
const targetRoot = path.join(homeDir, '.claude');
const adapterStatePath = path.join(targetRoot, 'ecc', 'install-state.json');
const recordedStatePath = path.join(outsideRoot, 'recorded-state.json');
const flatSkillPath = path.join(targetRoot, 'skills', 'tdd-workflow', 'SKILL.md');
const legacySkillPath = path.join(
targetRoot,
'skills',
'ecc',
'tdd-workflow',
'SKILL.md'
);
fs.mkdirSync(path.dirname(legacySkillPath), { recursive: true });
fs.writeFileSync(legacySkillPath, '# Previously managed nested skill\n');
writeState(adapterStatePath, {
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
targetRoot: outsideRoot,
installStatePath: recordedStatePath,
request: {
profile: null,
modules: ['workflow-quality'],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['platform-configs', 'workflow-quality'],
skippedModules: [],
},
operations: [{
kind: 'copy-file',
moduleId: 'workflow-quality',
sourcePath: path.join(REPO_ROOT, 'skills', 'tdd-workflow', 'SKILL.md'),
sourceRelativePath: path.join('skills', 'tdd-workflow', 'SKILL.md'),
destinationPath: legacySkillPath,
strategy: 'preserve-relative-path',
ownership: 'managed',
scaffoldOnly: false,
}],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
fs.writeFileSync(recordedStatePath, 'outside sentinel\n');
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['claude'],
});
assert.strictEqual(result.results[0].status, 'repaired');
assert.strictEqual(
fs.readFileSync(flatSkillPath, 'utf8'),
fs.readFileSync(
path.join(REPO_ROOT, 'skills', 'tdd-workflow', 'SKILL.md'),
'utf8'
)
);
assert.ok(!fs.existsSync(legacySkillPath));
assert.strictEqual(fs.readFileSync(recordedStatePath, 'utf8'), 'outside sentinel\n');
const refreshedState = readInstallState(adapterStatePath);
assert.strictEqual(refreshedState.target.root, targetRoot);
assert.strictEqual(refreshedState.target.installStatePath, adapterStatePath);
assert.ok(refreshedState.operations.some(operation => (
operation.destinationPath === flatSkillPath
)));
assert.ok(!refreshedState.operations.some(operation => (
operation.destinationPath === legacySkillPath
)));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('repair copies missing managed files from recorded source paths', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const destinationPath = path.join(targetRoot, 'rules', 'coding-style.md');
const sourcePath = path.join(REPO_ROOT, 'rules', 'common', 'coding-style.md');
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, {
sourceRelativePath: 'rules/common/coding-style.md',
strategy: 'copy-file',
}),
],
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'repaired');
assert.ok(fs.readFileSync(destinationPath).equals(fs.readFileSync(sourcePath)));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair reads source content and mode from one no-follow descriptor', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const sourcePath = path.join(REPO_ROOT, 'rules', 'common', 'coding-style.md');
const originalStatSync = fs.statSync;
try {
const targetRoot = path.join(projectRoot, '.cursor');
const destinationPath = path.join(targetRoot, 'rules', 'coding-style.md');
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, {
sourceRelativePath: 'rules/common/coding-style.md',
strategy: 'copy-file',
}),
],
});
fs.statSync = function rejectSeparateSourceMetadataLookup(candidatePath, ...args) {
if (path.resolve(candidatePath) === path.resolve(sourcePath)) {
throw new Error('source metadata must come from the opened descriptor');
}
return originalStatSync.call(fs, candidatePath, ...args);
};
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'repaired');
assert.ok(fs.readFileSync(destinationPath).equals(fs.readFileSync(sourcePath)));
} finally {
fs.statSync = originalStatSync;
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair reports invalid states, missing sources, unsupported operations, and no-op refreshes', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const invalidProjectRoot = createTempDir('install-lifecycle-invalid-');
const missingSourceProjectRoot = createTempDir('install-lifecycle-missing-source-');
const unsupportedProjectRoot = createTempDir('install-lifecycle-unsupported-');
const okProjectRoot = createTempDir('install-lifecycle-ok-');
try {
const invalidStatePath = path.join(invalidProjectRoot, '.cursor', 'ecc-install-state.json');
fs.mkdirSync(path.dirname(invalidStatePath), { recursive: true });
fs.writeFileSync(invalidStatePath, '{"schemaVersion":"wrong"}\n');
let result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot: invalidProjectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('Invalid install-state'));
const missingDestination = path.join(missingSourceProjectRoot, '.cursor', 'rules', 'missing.md');
fs.mkdirSync(path.dirname(missingDestination), { recursive: true });
fs.writeFileSync(missingDestination, 'managed\n');
writeCursorState(missingSourceProjectRoot, {
operations: [
managedOperation('copy-file', missingDestination, {
sourceRelativePath: 'missing/source.md',
strategy: 'copy-file',
}),
],
});
result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot: missingSourceProjectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('Missing source file(s)'));
const unsupportedDestination = path.join(unsupportedProjectRoot, '.cursor', 'custom.txt');
writeCursorState(unsupportedProjectRoot, {
operations: [
managedOperation('custom-kind', unsupportedDestination),
],
});
result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot: unsupportedProjectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('Unsupported repair operation kind'));
writeCursorState(okProjectRoot, { operations: [] });
result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot: okProjectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'ok');
assert.strictEqual(result.results[0].stateRefreshed, true);
assert.strictEqual(result.summary.errorCount, 0);
} finally {
cleanup(homeDir);
cleanup(invalidProjectRoot);
cleanup(missingSourceProjectRoot);
cleanup(unsupportedProjectRoot);
cleanup(okProjectRoot);
}
})) passed++; else failed++;
if (test('repair dry-run reports ok when no managed operations need changes', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
writeCursorState(projectRoot, { operations: [] });
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
dryRun: true,
});
assert.strictEqual(result.results[0].status, 'ok');
assert.strictEqual(result.results[0].stateRefreshed, true);
assert.deepStrictEqual(result.results[0].plannedRepairs, []);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair builds the OpenCode payload and clears the missing-payload warning', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
withTemporarilyMovedPath(path.join(REPO_ROOT, '.opencode', 'dist'), () => {
writeOpencodeState(homeDir, {
request: {
profile: null,
modules: ['commands-core'],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['commands-core'],
skippedModules: [],
},
operations: [],
});
const beforeValidate = getInstallTargetAdapter('opencode').validate({
homeDir,
repoRoot: REPO_ROOT,
});
assert.ok(beforeValidate.some(issue => issue.code === 'opencode-plugin-not-built'));
const beforeDoctor = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['opencode'],
});
assert.strictEqual(beforeDoctor.results[0].status, 'error');
assert.ok(beforeDoctor.results[0].issues.some(issue => issue.code === 'resolution-unavailable'));
let buildCalls = 0;
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['opencode'],
buildOpencodePayload: repoRoot => {
buildCalls += 1;
const distDir = path.join(repoRoot, '.opencode', 'dist');
fs.mkdirSync(path.join(distDir, 'plugins'), { recursive: true });
fs.mkdirSync(path.join(distDir, 'tools'), { recursive: true });
fs.writeFileSync(path.join(distDir, 'index.js'), 'module.exports = {};\\n');
},
});
assert.strictEqual(buildCalls, 1);
assert.strictEqual(result.results[0].status, 'repaired');
assert.ok(fs.existsSync(path.join(REPO_ROOT, '.opencode', 'dist', 'index.js')));
const afterValidate = getInstallTargetAdapter('opencode').validate({
homeDir,
repoRoot: REPO_ROOT,
});
assert.deepStrictEqual(afterValidate, []);
const afterDoctor = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['opencode'],
});
assert.strictEqual(afterDoctor.results[0].status, 'ok');
assert.strictEqual(afterDoctor.results[0].issues.length, 0);
});
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair dry-run plans the OpenCode payload build without creating it', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
withTemporarilyMovedPath(path.join(REPO_ROOT, '.opencode', 'dist'), () => {
writeOpencodeState(homeDir, {
request: {
profile: null,
modules: ['commands-core'],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['commands-core'],
skippedModules: [],
},
operations: [],
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['opencode'],
dryRun: true,
buildOpencodePayload: () => {
throw new Error('build should not run during dry-run');
},
});
assert.strictEqual(result.results[0].status, 'planned');
assert.ok(result.results[0].plannedRepairs.includes(path.join(REPO_ROOT, '.opencode', 'dist')));
assert.ok(!fs.existsSync(path.join(REPO_ROOT, '.opencode', 'dist', 'index.js')));
});
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('withTemporarilyMovedPath cleans up newly created paths when nothing was pre-existing', () => {
const filePath = path.join(REPO_ROOT, '.opencode', 'dist');
const backupPath = `${filePath}.backup-${process.pid}-test`;
try {
fs.rmSync(filePath, { recursive: true, force: true });
fs.rmSync(backupPath, { recursive: true, force: true });
const result = withTemporarilyMovedPath(filePath, receivedBackupPath => {
assert.strictEqual(receivedBackupPath, null);
fs.mkdirSync(path.join(filePath, 'plugins'), { recursive: true });
fs.mkdirSync(path.join(filePath, 'tools'), { recursive: true });
fs.writeFileSync(path.join(filePath, 'index.js'), '// temp build\n');
return 'callback-result';
});
assert.strictEqual(result, 'callback-result');
assert.ok(!fs.existsSync(filePath), 'Temporary path should be removed after the callback');
} finally {
fs.rmSync(filePath, { recursive: true, force: true });
fs.rmSync(backupPath, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('repair surfaces OpenCode build failures without blocking other targets', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
withTemporarilyMovedPath(path.join(REPO_ROOT, '.opencode', 'dist'), () => {
const cursorTargetRoot = path.join(projectRoot, '.cursor');
const cursorStatePath = path.join(cursorTargetRoot, 'ecc-install-state.json');
const cursorDestinationPath = path.join(cursorTargetRoot, 'rules', 'coding-style.md');
fs.mkdirSync(path.dirname(cursorDestinationPath), { recursive: true });
writeOpencodeState(homeDir, {
request: {
profile: null,
modules: ['commands-core'],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['commands-core'],
skippedModules: [],
},
operations: [],
});
writeState(cursorStatePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot: cursorTargetRoot,
installStatePath: cursorStatePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-cursor-install'],
skippedModules: [],
},
operations: [
managedOperation('copy-file', cursorDestinationPath, {
sourceRelativePath: 'rules/common/coding-style.md',
strategy: 'copy-file',
}),
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['opencode', 'cursor'],
buildOpencodePayload: () => {
throw new Error('typescript dependency missing');
},
});
const opencodeResult = result.results.find(entry => entry.adapter.id === 'opencode-home');
const cursorResult = result.results.find(entry => entry.adapter.id === 'cursor-project');
assert.strictEqual(opencodeResult.status, 'error');
assert.ok(opencodeResult.error.includes('typescript dependency missing'));
assert.strictEqual(cursorResult.status, 'repaired');
assert.ok(fs.existsSync(cursorDestinationPath));
});
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair surfaces missing source errors from execution when destination is absent', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const destinationPath = path.join(projectRoot, '.cursor', 'rules', 'missing.md');
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, {
sourceRelativePath: 'missing/source.md',
strategy: 'copy-file',
}),
],
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('Missing source file for repair'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair rejects absolute and parent-relative source metadata outside the repository', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const outsideRoot = createTempDir('install-lifecycle-source-outside-');
const outsideSourcePath = path.join(outsideRoot, 'secret.txt');
fs.writeFileSync(outsideSourcePath, 'outside secret\n');
try {
const unsafeSources = [
outsideSourcePath,
path.relative(REPO_ROOT, outsideSourcePath),
];
for (const sourceRelativePath of unsafeSources) {
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const destinationPath = path.join(projectRoot, '.cursor', 'copied-secret.txt');
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, {
sourceRelativePath,
strategy: 'copy-file',
}),
],
});
const doctor = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(doctor.results[0].status, 'error');
assert.ok(
doctor.results[0].issues.some(
issue => issue.code === 'unsafe-repair-source'
)
);
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('unsafe repair source metadata'));
assert.ok(!result.results[0].error.includes(outsideSourcePath));
assert.ok(!fs.existsSync(destinationPath));
} finally {
cleanup(projectRoot);
}
}
} finally {
cleanup(homeDir);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('doctor and repair reject unsafe destinations before health inspection reads them', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const outsideRoot = createTempDir('install-lifecycle-destination-outside-');
const copySource = fs.readFileSync(
path.join(REPO_ROOT, 'rules', 'common', 'coding-style.md'),
'utf8'
);
const cases = [
{
name: 'matching copy',
kind: 'copy-file',
content: copySource,
overrides: { strategy: 'copy-file' },
},
{
name: 'drifted copy',
kind: 'copy-file',
content: 'outside drift\n',
overrides: { strategy: 'copy-file' },
},
{
name: 'rendered template',
kind: 'render-template',
content: 'managed template\n',
overrides: {
renderedContent: 'managed template\n',
strategy: 'render-template',
},
},
{
name: 'merged JSON',
kind: 'merge-json',
content: '{"managed":true,"outside":"sentinel"}\n',
overrides: {
mergePayload: { managed: true },
strategy: 'merge-json',
},
},
];
try {
for (const testCase of cases) {
const projectRoot = createTempDir('install-lifecycle-project-');
const destinationPath = path.join(outsideRoot, `${testCase.name}.txt`);
const originalExistsSync = fs.existsSync;
try {
fs.writeFileSync(destinationPath, testCase.content);
writeCursorState(projectRoot, {
operations: [
managedOperation(testCase.kind, destinationPath, testCase.overrides),
],
});
fs.existsSync = function existsSyncWithoutOutsideInspection(candidatePath) {
if (path.resolve(candidatePath) === path.resolve(destinationPath)) {
throw new Error(`unsafe destination inspected: ${testCase.name}`);
}
return originalExistsSync.call(fs, candidatePath);
};
const doctor = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
const repair = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(doctor.results[0].status, 'error');
assert.ok(
doctor.results[0].issues.some(
issue => issue.code === 'unsafe-managed-destination'
)
);
assert.strictEqual(repair.results[0].status, 'error');
assert.ok(repair.results[0].error.includes('unsafe managed destination'));
assert.strictEqual(
originalExistsSync.call(fs, destinationPath),
true,
`${testCase.name} destination should remain untouched`
);
} finally {
fs.existsSync = originalExistsSync;
cleanup(projectRoot);
}
}
} finally {
cleanup(homeDir);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('doctor reports drifted managed files as a warning', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
const sourcePath = path.join(REPO_ROOT, '.cursor', 'hooks.json');
const destinationPath = path.join(targetRoot, 'hooks.json');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, '{"drifted":true}\n');
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: ['platform-configs'],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['platform-configs'],
skippedModules: [],
},
operations: [
{
kind: 'copy-file',
moduleId: 'platform-configs',
sourcePath,
sourceRelativePath: '.cursor/hooks.json',
destinationPath,
strategy: 'sync-root-children',
ownership: 'managed',
scaffoldOnly: false,
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(report.results.length, 1);
assert.strictEqual(report.results[0].status, 'warning');
assert.ok(report.results[0].issues.some(issue => issue.code === 'drifted-managed-files'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('doctor reproduces install-time link rewrites for managed copy files', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.agents');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
const operations = ['code-review.md', 'testing.md'].map(fileName => ({
kind: 'copy-file',
moduleId: 'rules-core',
sourcePath: path.join(REPO_ROOT, 'rules', 'common', fileName),
sourceRelativePath: path.join('rules', 'common', fileName),
destinationPath: path.join(targetRoot, 'rules', `common-${fileName}`),
strategy: 'flatten-copy',
ownership: 'managed',
scaffoldOnly: false,
}));
const state = createInstallState({
adapter: { id: 'antigravity-project', target: 'antigravity', kind: 'project' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['rules-core'],
skippedModules: [],
},
operations,
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
applyInstallPlan({
mode: 'legacy',
target: 'antigravity',
adapter: { id: 'antigravity-project', target: 'antigravity', kind: 'project' },
targetRoot,
installRoot: targetRoot,
installStatePath: statePath,
operations,
warnings: [],
statePreview: state,
});
const report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['antigravity'],
});
assert.ok(!report.results[0].issues.some(issue => issue.code === 'drifted-managed-files'));
fs.writeFileSync(operations[0].destinationPath, 'customer edit\n');
const driftedReport = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['antigravity'],
});
assert.ok(driftedReport.results[0].issues.some(issue => issue.code === 'drifted-managed-files'));
const repair = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['antigravity'],
});
assert.strictEqual(repair.results[0].status, 'repaired');
assert.ok(
fs.readFileSync(operations[0].destinationPath, 'utf8').includes('(common-testing.md)')
);
const repairedState = readInstallState(statePath);
assert.match(repairedState.operations[0].contentSha256, /^[a-f0-9]{64}$/);
const repairedReport = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['antigravity'],
});
assert.ok(!repairedReport.results[0].issues.some(issue => issue.code === 'drifted-managed-files'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('doctor trusts a recorded installed digest before comparing a newer source tree', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const destinationPath = path.join(targetRoot, 'rules', 'coding-style.md');
const installedContent = 'installed from an older verified release\n';
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, installedContent);
const contentSha256 = crypto.createHash('sha256').update(installedContent).digest('hex');
const installStatePath = path.join(targetRoot, 'ecc-install-state.json');
writeState(installStatePath, createCursorStateOptions(projectRoot, {
operations: [managedOperation('copy-file', destinationPath, {
sourceRelativePath: path.join('rules', 'common', 'coding-style.md'),
contentSha256,
})],
}));
const report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.ok(!report.results[0].issues.some(issue => issue.code === 'drifted-managed-files'));
fs.writeFileSync(destinationPath, 'customer edit\n');
const drifted = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.ok(drifted.results[0].issues.some(issue => issue.code === 'drifted-managed-files'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('doctor reports manifest resolution drift for non-legacy installs', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
fs.mkdirSync(targetRoot, { recursive: true });
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot,
installStatePath: statePath,
request: {
profile: 'core',
modules: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['rules-core'],
skippedModules: [],
},
operations: [],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(report.results.length, 1);
assert.strictEqual(report.results[0].status, 'warning');
assert.ok(report.results[0].issues.some(issue => issue.code === 'resolution-drift'));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair restores render-template outputs from recorded rendered content', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(homeDir, '.claude');
const statePath = path.join(targetRoot, 'ecc', 'install-state.json');
const destinationPath = path.join(targetRoot, 'plugin.json');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, '{"drifted":true}\n');
writeState(statePath, {
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-claude-rules'],
skippedModules: [],
},
operations: [
{
kind: 'render-template',
moduleId: 'platform-configs',
sourceRelativePath: '.claude-plugin/plugin.json.template',
destinationPath,
strategy: 'render-template',
ownership: 'managed',
scaffoldOnly: false,
renderedContent: '{"ok":true}\n',
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['claude'],
});
assert.strictEqual(result.results[0].status, 'repaired');
assert.strictEqual(fs.readFileSync(destinationPath, 'utf8'), '{"ok":true}\n');
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair reapplies merge-json operations without clobbering unrelated keys', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
const destinationPath = path.join(targetRoot, 'hooks.json');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, JSON.stringify({
existing: true,
nested: {
enabled: false,
},
}, null, 2));
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-cursor-install'],
skippedModules: [],
},
operations: [
{
kind: 'merge-json',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/hooks.json',
destinationPath,
strategy: 'merge-json',
ownership: 'managed',
scaffoldOnly: false,
mergePayload: {
nested: {
enabled: true,
},
managed: 'yes',
},
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'repaired');
assert.deepStrictEqual(JSON.parse(fs.readFileSync(destinationPath, 'utf8')), {
existing: true,
nested: {
enabled: true,
},
managed: 'yes',
});
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair re-applies managed remove operations when files reappear', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
const destinationPath = path.join(targetRoot, 'legacy-note.txt');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, 'stale');
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-cursor-install'],
skippedModules: [],
},
operations: [
{
kind: 'remove',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/legacy-note.txt',
destinationPath,
strategy: 'remove',
ownership: 'managed',
scaffoldOnly: false,
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'repaired');
assert.ok(!fs.existsSync(destinationPath));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair rejects a symlink inserted while creating a missing destination parent', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const outsideRoot = createTempDir('install-lifecycle-outside-');
const targetRoot = path.join(projectRoot, '.cursor');
const destinationParent = path.join(targetRoot, 'late-parent');
const destinationPath = path.join(destinationParent, 'managed.md');
const outsideDestinationPath = path.join(outsideRoot, 'managed.md');
const originalMkdirSync = fs.mkdirSync;
let canonicalDestinationParent;
let insertedSymlink = false;
let result;
try {
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
],
});
canonicalDestinationParent = path.join(
fs.realpathSync(targetRoot),
path.basename(destinationParent)
);
fs.mkdirSync = function mkdirSyncWithLateSymlink(directoryPath, options) {
if (!insertedSymlink && path.resolve(directoryPath) === canonicalDestinationParent) {
originalMkdirSync.call(fs, path.dirname(canonicalDestinationParent), { recursive: true });
fs.symlinkSync(
outsideRoot,
canonicalDestinationParent,
process.platform === 'win32' ? 'junction' : 'dir'
);
insertedSymlink = true;
return undefined;
}
return originalMkdirSync.call(fs, directoryPath, options);
};
result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
} finally {
fs.mkdirSync = originalMkdirSync;
}
try {
assert.strictEqual(insertedSymlink, true);
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('outside the install root'));
assert.ok(!fs.existsSync(outsideDestinationPath));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('repair rejects an in-root final symlink without overwriting its victim', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const victimPath = path.join(targetRoot, 'victim.md');
const destinationPath = path.join(targetRoot, 'managed.md');
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(victimPath, 'victim sentinel\n');
try {
fs.symlinkSync(victimPath, destinationPath);
} catch {
console.log(' (symlink unsupported on this platform; skipping)');
return;
}
writeCursorState(projectRoot, {
operations: [
managedOperation('render-template', destinationPath, {
renderedContent: 'managed replacement\n',
strategy: 'render-template',
}),
],
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('final symlink'));
assert.strictEqual(fs.readFileSync(victimPath, 'utf8'), 'victim sentinel\n');
assert.strictEqual(fs.lstatSync(destinationPath).isSymbolicLink(), true);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair uses no-follow writes when a final destination becomes a symlink', () => {
if (!fs.constants.O_NOFOLLOW) {
console.log(' (O_NOFOLLOW unsupported on this platform; skipping)');
return;
}
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const outsideRoot = createTempDir('install-lifecycle-outside-');
const targetRoot = path.join(projectRoot, '.cursor');
const destinationPath = path.join(targetRoot, 'managed.md');
const outsideDestinationPath = path.join(outsideRoot, 'managed.md');
const originalOpenSync = fs.openSync;
let canonicalDestinationPath;
let insertedSymlink = false;
let result;
try {
fs.writeFileSync(outsideDestinationPath, 'outside sentinel\n');
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
],
});
canonicalDestinationPath = path.join(
fs.realpathSync(targetRoot),
path.basename(destinationPath)
);
fs.openSync = function openSyncWithLateSymlink(filePath, flags, mode) {
if (!insertedSymlink && path.resolve(filePath) === canonicalDestinationPath) {
fs.symlinkSync(outsideDestinationPath, canonicalDestinationPath);
insertedSymlink = true;
}
return originalOpenSync.call(fs, filePath, flags, mode);
};
result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
} finally {
fs.openSync = originalOpenSync;
}
try {
assert.strictEqual(insertedSymlink, true);
assert.strictEqual(result.results[0].status, 'error');
assert.strictEqual(
fs.readFileSync(outsideDestinationPath, 'utf8'),
'outside sentinel\n'
);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('repair revalidates a pinned write before a swapped parent can truncate outside files', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const outsideRoot = createTempDir('install-lifecycle-outside-');
const targetRoot = path.join(projectRoot, '.cursor');
const destinationParent = path.join(targetRoot, 'late-parent');
const backupParent = path.join(targetRoot, 'late-parent-backup');
const destinationPath = path.join(destinationParent, 'managed.md');
const outsideDestinationPath = path.join(outsideRoot, 'managed.md');
const originalOpenSync = fs.openSync;
let canonicalDestinationPath;
let insertedSymlink = false;
let result;
const symlinkProbe = path.join(targetRoot, 'parent-symlink-probe');
try {
fs.mkdirSync(targetRoot, { recursive: true });
fs.symlinkSync(
outsideRoot,
symlinkProbe,
process.platform === 'win32' ? 'junction' : 'dir'
);
fs.rmSync(symlinkProbe, { force: true });
} catch {
console.log(' (symlink unsupported on this platform; skipping)');
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
return;
}
try {
fs.mkdirSync(destinationParent, { recursive: true });
fs.writeFileSync(destinationPath, 'drifted managed content\n');
fs.writeFileSync(outsideDestinationPath, 'outside sentinel\n');
canonicalDestinationPath = fs.realpathSync(destinationPath);
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
],
});
fs.openSync = function openSyncWithLateParentSwap(filePath, flags, mode) {
const isDestinationWrite = path.resolve(filePath) === canonicalDestinationPath
&& typeof flags === 'number'
&& (flags & fs.constants.O_WRONLY) === fs.constants.O_WRONLY;
if (!insertedSymlink && isDestinationWrite) {
fs.renameSync(destinationParent, backupParent);
fs.symlinkSync(
outsideRoot,
destinationParent,
process.platform === 'win32' ? 'junction' : 'dir'
);
insertedSymlink = true;
}
return originalOpenSync.call(fs, filePath, flags, mode);
};
result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
} finally {
fs.openSync = originalOpenSync;
}
try {
assert.strictEqual(insertedSymlink, true);
assert.strictEqual(result.results[0].status, 'error');
assert.strictEqual(
fs.readFileSync(outsideDestinationPath, 'utf8'),
'outside sentinel\n'
);
assert.strictEqual(
fs.readFileSync(path.join(backupParent, 'managed.md'), 'utf8'),
'drifted managed content\n'
);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('repair refreshes only the adapter-derived install-state path', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const outsideRoot = createTempDir('install-lifecycle-outside-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const adapterStatePath = path.join(targetRoot, 'ecc-install-state.json');
const recordedStatePath = path.join(outsideRoot, 'recorded-state.json');
const stateOptions = createCursorStateOptions(projectRoot, {
installStatePath: recordedStatePath,
});
writeState(adapterStatePath, stateOptions);
fs.writeFileSync(recordedStatePath, 'outside sentinel\n');
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'ok');
assert.ok(fs.existsSync(adapterStatePath));
assert.strictEqual(
fs.readFileSync(recordedStatePath, 'utf8'),
'outside sentinel\n'
);
const refreshedState = readInstallState(adapterStatePath);
assert.strictEqual(refreshedState.target.root, targetRoot);
assert.strictEqual(refreshedState.target.installStatePath, adapterStatePath);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('uninstall restores JSON merged files from recorded previous content', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
const destinationPath = path.join(targetRoot, 'hooks.json');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, JSON.stringify({
existing: true,
managed: true,
}, null, 2));
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-cursor-install'],
skippedModules: [],
},
operations: [
{
kind: 'merge-json',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/hooks.json',
destinationPath,
strategy: 'merge-json',
ownership: 'managed',
scaffoldOnly: false,
mergePayload: {
managed: true,
},
previousContent: JSON.stringify({
existing: true,
}, null, 2),
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.deepStrictEqual(JSON.parse(fs.readFileSync(destinationPath, 'utf8')), {
existing: true,
});
assert.ok(!fs.existsSync(statePath));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('uninstall restores rendered template files from recorded previous content', () => {
const tempDir = createTempDir('install-lifecycle-');
try {
const targetRoot = path.join(tempDir, '.claude');
const statePath = path.join(targetRoot, 'ecc', 'install-state.json');
const destinationPath = path.join(targetRoot, 'plugin.json');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, '{"generated":true}\n');
writeInstallState(statePath, createInstallState({
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
targetRoot,
installStatePath: statePath,
request: {
profile: 'core',
modules: ['platform-configs'],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['platform-configs'],
skippedModules: [],
},
source: {
repoVersion: '1.8.0',
repoCommit: 'abc123',
manifestVersion: 1,
},
operations: [
{
kind: 'render-template',
moduleId: 'platform-configs',
sourceRelativePath: '.claude/plugin.json.template',
destinationPath,
strategy: 'render-template',
ownership: 'managed',
scaffoldOnly: false,
renderedContent: '{"generated":true}\n',
previousContent: '{"existing":true}\n',
},
],
}));
const result = uninstallInstalledStates({
homeDir: tempDir,
projectRoot: tempDir,
targets: ['claude'],
});
assert.strictEqual(result.summary.uninstalledCount, 1);
assert.strictEqual(fs.readFileSync(destinationPath, 'utf8'), '{"existing":true}\n');
assert.ok(!fs.existsSync(statePath));
} finally {
cleanup(tempDir);
}
})) passed++; else failed++;
if (test('uninstall restores files removed during install when previous content is recorded', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
const destinationPath = path.join(targetRoot, 'legacy-note.txt');
fs.mkdirSync(targetRoot, { recursive: true });
writeState(statePath, {
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-cursor-install'],
skippedModules: [],
},
operations: [
{
kind: 'remove',
moduleId: 'platform-configs',
sourceRelativePath: '.cursor/legacy-note.txt',
destinationPath,
strategy: 'remove',
ownership: 'managed',
scaffoldOnly: false,
previousContent: 'restore me\n',
},
],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.strictEqual(fs.readFileSync(destinationPath, 'utf8'), 'restore me\n');
assert.ok(!fs.existsSync(statePath));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('uninstall dry-run reports deduped managed removals without deleting files', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const destinationPath = path.join(targetRoot, 'rules', 'coding-style.md');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, 'managed\n');
const { installStatePath } = writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
],
});
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
dryRun: true,
});
assert.strictEqual(result.dryRun, true);
assert.strictEqual(result.results[0].status, 'planned');
assert.deepStrictEqual(result.results[0].plannedRemovals, [
destinationPath,
installStatePath,
]);
assert.ok(fs.existsSync(destinationPath));
assert.ok(fs.existsSync(installStatePath));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('uninstall reports invalid install states as errors', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const statePath = path.join(projectRoot, '.cursor', 'ecc-install-state.json');
fs.mkdirSync(path.dirname(statePath), { recursive: true });
fs.writeFileSync(statePath, '{not-json', 'utf8');
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('Failed to read install-state'));
assert.strictEqual(result.summary.errorCount, 1);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('uninstall removes only the adapter-derived install-state path', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const outsideRoot = createTempDir('install-lifecycle-outside-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const adapterStatePath = path.join(targetRoot, 'ecc-install-state.json');
const recordedStatePath = path.join(outsideRoot, 'recorded-state.json');
const stateOptions = createCursorStateOptions(projectRoot, {
installStatePath: recordedStatePath,
});
writeState(adapterStatePath, stateOptions);
fs.writeFileSync(recordedStatePath, 'outside sentinel\n');
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.ok(!fs.existsSync(adapterStatePath));
assert.strictEqual(
fs.readFileSync(recordedStatePath, 'utf8'),
'outside sentinel\n'
);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('uninstall removes copied files and cleans empty parent directories', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const destinationPath = path.join(targetRoot, 'rules', 'nested', 'managed.md');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, 'managed\n');
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
],
});
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.ok(result.results[0].removedPaths.includes(destinationPath));
assert.ok(!fs.existsSync(destinationPath));
assert.ok(!fs.existsSync(path.dirname(destinationPath)));
assert.ok(fs.existsSync(targetRoot));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('uninstall cleanup stops at the adapter-derived target root', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const cleanupBoundaryRoot = createTempDir('install-lifecycle-boundary-');
const projectRoot = path.join(cleanupBoundaryRoot, 'project');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const adapterStatePath = path.join(targetRoot, 'ecc-install-state.json');
const destinationPath = path.join(targetRoot, 'rules', 'nested', 'managed.md');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, 'managed\n');
const stateOptions = createCursorStateOptions(projectRoot, {
targetRoot: cleanupBoundaryRoot,
installStatePath: adapterStatePath,
operations: [
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
],
});
writeState(adapterStatePath, stateOptions);
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.ok(fs.existsSync(projectRoot));
assert.ok(fs.existsSync(targetRoot));
assert.ok(!fs.existsSync(destinationPath));
} finally {
cleanup(homeDir);
cleanup(cleanupBoundaryRoot);
}
})) passed++; else failed++;
if (test('uninstall handles merge-json subset removal and full-file deletion', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const partialProjectRoot = createTempDir('install-lifecycle-partial-');
const fullProjectRoot = createTempDir('install-lifecycle-full-');
try {
let targetRoot = path.join(partialProjectRoot, '.cursor');
let destinationPath = path.join(targetRoot, 'settings.json');
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(destinationPath, JSON.stringify({
keep: true,
managed: true,
nested: {
keep: true,
remove: true,
},
list: ['a', 'b'],
}, null, 2));
writeCursorState(partialProjectRoot, {
operations: [
managedOperation('merge-json', destinationPath, {
mergePayload: {
managed: true,
nested: { remove: true },
list: ['a', 'b'],
},
}),
],
});
let result = uninstallInstalledStates({
homeDir,
projectRoot: partialProjectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.deepStrictEqual(JSON.parse(fs.readFileSync(destinationPath, 'utf8')), {
keep: true,
nested: {
keep: true,
},
});
targetRoot = path.join(fullProjectRoot, '.cursor');
destinationPath = path.join(targetRoot, 'settings.json');
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(destinationPath, JSON.stringify({ managed: true }, null, 2));
writeCursorState(fullProjectRoot, {
operations: [
managedOperation('merge-json', destinationPath, {
mergePayload: { managed: true },
}),
],
});
result = uninstallInstalledStates({
homeDir,
projectRoot: fullProjectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.ok(!fs.existsSync(destinationPath));
} finally {
cleanup(homeDir);
cleanup(partialProjectRoot);
cleanup(fullProjectRoot);
}
})) passed++; else failed++;
if (test('uninstall handles merge-json edge shapes and absent destinations', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projects = [
createTempDir('install-lifecycle-current-primitive-'),
createTempDir('install-lifecycle-missing-key-'),
createTempDir('install-lifecycle-nested-delete-'),
createTempDir('install-lifecycle-array-root-'),
createTempDir('install-lifecycle-primitive-root-'),
createTempDir('install-lifecycle-absent-dest-'),
createTempDir('install-lifecycle-previous-json-'),
];
try {
const cases = [
{
projectRoot: projects[0],
initial: '"plain"',
payload: { managed: true },
expected: 'plain',
},
{
projectRoot: projects[1],
initial: { keep: true },
payload: { missing: true },
expected: { keep: true },
},
{
projectRoot: projects[2],
initial: { keep: true, nested: { remove: true } },
payload: { nested: { remove: true } },
expected: { keep: true },
},
{
projectRoot: projects[3],
initial: ['a', 'b'],
payload: ['a', 'b'],
removed: true,
},
{
projectRoot: projects[4],
initial: true,
payload: true,
removed: true,
},
{
projectRoot: projects[5],
payload: { managed: true },
absent: true,
},
{
projectRoot: projects[6],
initial: { generated: true },
payload: { generated: true },
previousJson: { restored: true },
expected: { restored: true },
},
];
for (const testCase of cases) {
const targetRoot = path.join(testCase.projectRoot, '.cursor');
const destinationPath = path.join(targetRoot, 'settings.json');
fs.mkdirSync(targetRoot, { recursive: true });
if (!testCase.absent) {
fs.writeFileSync(
destinationPath,
typeof testCase.initial === 'string'
? `${testCase.initial}\n`
: JSON.stringify(testCase.initial, null, 2)
);
}
writeCursorState(testCase.projectRoot, {
operations: [
managedOperation('merge-json', destinationPath, {
mergePayload: testCase.payload,
previousJson: testCase.previousJson,
}),
],
});
const result = uninstallInstalledStates({
homeDir,
projectRoot: testCase.projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
if (testCase.removed || testCase.absent) {
assert.ok(!fs.existsSync(destinationPath));
} else {
assert.deepStrictEqual(JSON.parse(fs.readFileSync(destinationPath, 'utf8')), testCase.expected);
}
}
} finally {
cleanup(homeDir);
for (const projectRoot of projects) {
cleanup(projectRoot);
}
}
})) passed++; else failed++;
if (test('uninstall removes generated render-template files and no-backup remove operations are no-ops', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const templatePath = path.join(targetRoot, 'generated', 'plugin.json');
const removedPath = path.join(targetRoot, 'already-removed.txt');
fs.mkdirSync(path.dirname(templatePath), { recursive: true });
fs.writeFileSync(templatePath, '{"generated":true}\n');
writeCursorState(projectRoot, {
operations: [
managedOperation('render-template', templatePath, {
renderedContent: '{"generated":true}\n',
}),
managedOperation('remove', removedPath),
],
});
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.ok(result.results[0].removedPaths.includes(templatePath));
assert.ok(!fs.existsSync(templatePath));
assert.ok(!fs.existsSync(path.dirname(templatePath)));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('uninstall removes an in-root final symlink without deleting its victim', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const victimPath = path.join(targetRoot, 'victim.md');
const destinationPath = path.join(targetRoot, 'managed.md');
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(victimPath, 'victim sentinel\n');
try {
fs.symlinkSync(victimPath, destinationPath);
} catch {
console.log(' (symlink unsupported on this platform; skipping)');
return;
}
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
],
});
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.ok(!fs.existsSync(destinationPath));
assert.strictEqual(fs.readFileSync(victimPath, 'utf8'), 'victim sentinel\n');
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('uninstall rejects a symlink inserted after initial destination validation', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const outsideRoot = createTempDir('install-lifecycle-outside-');
const targetRoot = path.join(projectRoot, '.cursor');
const destinationParent = path.join(targetRoot, 'late-parent');
const backupParent = path.join(targetRoot, 'late-parent-backup');
const destinationPath = path.join(destinationParent, 'managed.md');
const outsideDestinationPath = path.join(outsideRoot, 'managed.md');
const originalExistsSync = fs.existsSync;
let canonicalDestinationParent;
let canonicalDestinationPath;
let insertedSymlink = false;
let result;
try {
fs.mkdirSync(destinationParent, { recursive: true });
fs.writeFileSync(destinationPath, 'managed\n');
fs.writeFileSync(outsideDestinationPath, 'outside sentinel\n');
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
],
});
canonicalDestinationPath = fs.realpathSync(destinationPath);
canonicalDestinationParent = path.dirname(canonicalDestinationPath);
fs.existsSync = function existsSyncWithLateSymlink(candidatePath) {
if (!insertedSymlink && path.resolve(candidatePath) === canonicalDestinationPath) {
fs.renameSync(canonicalDestinationParent, backupParent);
fs.symlinkSync(
outsideRoot,
canonicalDestinationParent,
process.platform === 'win32' ? 'junction' : 'dir'
);
insertedSymlink = true;
}
return originalExistsSync.call(fs, candidatePath);
};
result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
} finally {
fs.existsSync = originalExistsSync;
}
try {
assert.strictEqual(insertedSymlink, true);
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('outside the install root'));
assert.strictEqual(
fs.readFileSync(outsideDestinationPath, 'utf8'),
'outside sentinel\n'
);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('uninstall restores previous JSON snapshots for template and remove operations', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const templatePath = path.join(targetRoot, 'plugin.json');
const removedPath = path.join(targetRoot, 'legacy.json');
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(templatePath, '{"generated":true}\n');
writeCursorState(projectRoot, {
operations: [
managedOperation('render-template', templatePath, {
previousJson: { existing: true },
renderedContent: '{"generated":true}\n',
}),
managedOperation('remove', removedPath, {
previousJson: { restored: true },
}),
],
});
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.deepStrictEqual(JSON.parse(fs.readFileSync(templatePath, 'utf8')), {
existing: true,
});
assert.deepStrictEqual(JSON.parse(fs.readFileSync(removedPath, 'utf8')), {
restored: true,
});
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('uninstall reports unsupported operations and missing merge payloads as errors', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const unsupportedProjectRoot = createTempDir('install-lifecycle-unsupported-');
const missingPayloadProjectRoot = createTempDir('install-lifecycle-missing-payload-');
try {
let targetRoot = path.join(unsupportedProjectRoot, '.cursor');
let destinationPath = path.join(targetRoot, 'custom.txt');
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(destinationPath, 'custom\n');
writeCursorState(unsupportedProjectRoot, {
operations: [
managedOperation('custom-kind', destinationPath),
],
});
let result = uninstallInstalledStates({
homeDir,
projectRoot: unsupportedProjectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('Unsupported uninstall operation kind'));
targetRoot = path.join(missingPayloadProjectRoot, '.cursor');
destinationPath = path.join(targetRoot, 'settings.json');
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(destinationPath, '{"managed":true}\n');
writeCursorState(missingPayloadProjectRoot, {
operations: [
managedOperation('merge-json', destinationPath),
],
});
result = uninstallInstalledStates({
homeDir,
projectRoot: missingPayloadProjectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('Missing merge payload for uninstall'));
} finally {
cleanup(homeDir);
cleanup(unsupportedProjectRoot);
cleanup(missingPayloadProjectRoot);
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();