mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-28 10:49:45 +02:00
fix(install): merge state across selective installs
This commit is contained in:
@@ -133,19 +133,13 @@ function isManagedOperation(operation) {
|
||||
}
|
||||
|
||||
function uniqueOperations(operations) {
|
||||
const seen = new Set();
|
||||
return operations.filter(operation => {
|
||||
const key = [
|
||||
operation.kind,
|
||||
normalizeSourceRelativePath(operation.sourceRelativePath) || operation.sourceRelativePath,
|
||||
comparablePath(operation.destinationPath),
|
||||
].join('\0');
|
||||
if (seen.has(key)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
const byDestination = new Map();
|
||||
for (const operation of operations) {
|
||||
// A target path has one current owner. Later operations come from the
|
||||
// newest plan and replace stale metadata for the same destination.
|
||||
byDestination.set(comparablePath(operation.destinationPath), operation);
|
||||
}
|
||||
return [...byDestination.values()];
|
||||
}
|
||||
|
||||
function buildState(statePreview, operations) {
|
||||
@@ -236,14 +230,18 @@ function createFileConflictWarning(destinationPath, retainsLegacy) {
|
||||
return `Skipped user-owned Claude skill file ${destinationPath}: the existing file is not recorded in ECC install-state.${legacySuffix}`;
|
||||
}
|
||||
|
||||
function createDisabledMigration(plan) {
|
||||
function createDisabledMigration(plan, previousState) {
|
||||
const finalState = buildState(plan.statePreview, [
|
||||
...((previousState && previousState.operations) || []),
|
||||
...plan.statePreview.operations,
|
||||
]);
|
||||
return {
|
||||
enabled: false,
|
||||
appliedOperations: [...plan.operations],
|
||||
skippedOperations: [],
|
||||
warnings: [],
|
||||
bridgeState: plan.statePreview,
|
||||
finalState: plan.statePreview,
|
||||
bridgeState: finalState,
|
||||
finalState,
|
||||
legacyOperationsToRemove: [],
|
||||
requiresBridgeState: false,
|
||||
};
|
||||
@@ -331,11 +329,16 @@ function buildMigrationStates(plan, previousState, previous, classification) {
|
||||
const legacyOperationsToRemove = legacyOperations.filter(operation => (
|
||||
!retainedLegacyOperations.has(operation)
|
||||
));
|
||||
const removedLegacyDestinations = new Set(
|
||||
legacyOperationsToRemove.map(operation => comparablePath(operation.destinationPath))
|
||||
);
|
||||
const finalOperations = [
|
||||
...((previousState && previousState.operations) || []).filter(operation => (
|
||||
!removedLegacyDestinations.has(comparablePath(operation.destinationPath))
|
||||
)),
|
||||
...plan.statePreview.operations.filter(operation => (
|
||||
!skippedDestinations.has(comparablePath(operation.destinationPath))
|
||||
)),
|
||||
...retainedLegacyOperations,
|
||||
];
|
||||
const bridgeOperations = [
|
||||
...((previousState && previousState.operations) || []),
|
||||
@@ -352,14 +355,13 @@ function buildMigrationStates(plan, previousState, previous, classification) {
|
||||
}
|
||||
|
||||
function prepareClaudeSkillMigration(plan) {
|
||||
const target = plan && plan.adapter && plan.adapter.target;
|
||||
if (!CLAUDE_TARGETS.has(target)) {
|
||||
return createDisabledMigration(plan);
|
||||
}
|
||||
|
||||
const previousState = pathExists(plan.installStatePath)
|
||||
? readInstallState(plan.installStatePath)
|
||||
: null;
|
||||
const target = plan && plan.adapter && plan.adapter.target;
|
||||
if (!CLAUDE_TARGETS.has(target)) {
|
||||
return createDisabledMigration(plan, previousState);
|
||||
}
|
||||
const currentGroups = groupCurrentSkillOperations(plan);
|
||||
const previous = classifyPreviousOperations(plan, previousState);
|
||||
const classification = classifySkillConflicts(currentGroups, previous);
|
||||
|
||||
@@ -433,6 +433,72 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('merges managed operations across selective installs for the same target', () => {
|
||||
const fixture = createFixture();
|
||||
try {
|
||||
applyInstallPlan(fixture.plan);
|
||||
|
||||
const extraSourceRelativePath = path.join('skills', 'extra-skill', 'SKILL.md');
|
||||
const extraSourcePath = path.join(fixture.sourceRoot, extraSourceRelativePath);
|
||||
const extraDestinationPath = path.join(
|
||||
fixture.targetRoot,
|
||||
'skills',
|
||||
'extra-skill',
|
||||
'SKILL.md'
|
||||
);
|
||||
fs.mkdirSync(path.dirname(extraSourcePath), { recursive: true });
|
||||
fs.writeFileSync(extraSourcePath, '# Extra ECC skill\n');
|
||||
const extraOperation = createOperation(
|
||||
'skill-extra',
|
||||
fixture.sourceRoot,
|
||||
extraSourceRelativePath,
|
||||
extraDestinationPath
|
||||
);
|
||||
const extraPlan = {
|
||||
...fixture.plan,
|
||||
operations: [extraOperation],
|
||||
statePreview: {
|
||||
...fixture.plan.statePreview,
|
||||
request: {
|
||||
...fixture.plan.statePreview.request,
|
||||
modules: [],
|
||||
includeComponents: ['skill-extra'],
|
||||
},
|
||||
resolution: {
|
||||
selectedModules: [],
|
||||
skippedModules: [],
|
||||
},
|
||||
operations: [extraOperation],
|
||||
},
|
||||
};
|
||||
|
||||
applyInstallPlan(extraPlan);
|
||||
const stateAfterExtraInstall = readInstallState(fixture.installStatePath);
|
||||
assert.ok(fixture.operations.every(operation => (
|
||||
stateAfterExtraInstall.operations.some(recorded => (
|
||||
recorded.destinationPath === operation.destinationPath
|
||||
))
|
||||
)));
|
||||
assert.ok(stateAfterExtraInstall.operations.some(operation => (
|
||||
operation.destinationPath === extraDestinationPath
|
||||
)));
|
||||
|
||||
const retry = applyInstallPlan(fixture.plan);
|
||||
assert.deepStrictEqual(retry.skippedOperations, []);
|
||||
const stateAfterRetry = readInstallState(fixture.installStatePath);
|
||||
assert.ok(stateAfterRetry.operations.some(operation => (
|
||||
operation.destinationPath === extraDestinationPath
|
||||
)));
|
||||
|
||||
const uninstall = runUninstall(fixture);
|
||||
assert.strictEqual(uninstall.summary.errorCount, 0);
|
||||
assert.ok(fixture.operations.every(operation => !fs.existsSync(operation.destinationPath)));
|
||||
assert.ok(!fs.existsSync(extraDestinationPath));
|
||||
} finally {
|
||||
cleanup(fixture.tempDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('tracks a partial migration so retry and uninstall remain safe', () => {
|
||||
const fixture = createFixture();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user