test: cover install lifecycle edge paths

This commit is contained in:
Affaan Mustafa 2026-04-29 17:56:25 -04:00
parent 63485a26bf
commit b40de37ccb

View File

@ -10,6 +10,7 @@ const path = require('path');
const {
buildDoctorReport,
discoverInstalledStates,
normalizeTargets,
repairInstalledStates,
uninstallInstalledStates,
} = require('../../scripts/lib/install-lifecycle');
@ -52,12 +53,79 @@ function writeState(filePath, options) {
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 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-');
@ -127,6 +195,42 @@ function runTests() {
}
})) 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-');
@ -184,6 +288,189 @@ function runTests() {
}
})) 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-');
@ -244,6 +531,201 @@ function runTests() {
}
})) 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('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 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 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('doctor reports drifted managed files as a warning', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
@ -731,6 +1213,394 @@ function runTests() {
}
})) 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 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 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 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);
}