mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-30 22:13:28 +08:00
test: cover install lifecycle edge paths
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user