From faaa21c4e44835507bb3bd28823479ff75c208e5 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Fri, 21 Aug 2026 02:19:17 +0800 Subject: [PATCH] fix(install): guard state before selective merge --- scripts/lib/install/apply.js | 4 +++ scripts/lib/multi-harness-setup.js | 1 + .../install-claude-skill-migration.test.js | 25 +++++++++++++++++-- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/scripts/lib/install/apply.js b/scripts/lib/install/apply.js index 7da51910c..340b9204a 100644 --- a/scripts/lib/install/apply.js +++ b/scripts/lib/install/apply.js @@ -337,8 +337,12 @@ function previewInstallPlan(plan) { function applyInstallPlan(plan, dependencies = {}) { const persistInstallState = dependencies.writeInstallState || writeInstallState; + const beforeInstallStateRead = dependencies.beforeInstallStateRead; const beforeOperationWrite = dependencies.beforeOperationWrite; const beforeInstallStateWrite = dependencies.beforeInstallStateWrite; + if (typeof beforeInstallStateRead === 'function') { + beforeInstallStateRead({ plan }); + } const migration = prepareClaudeSkillMigration(plan); const appliedPlan = { ...plan, diff --git a/scripts/lib/multi-harness-setup.js b/scripts/lib/multi-harness-setup.js index fdf2354a2..214f58f25 100644 --- a/scripts/lib/multi-harness-setup.js +++ b/scripts/lib/multi-harness-setup.js @@ -327,6 +327,7 @@ async function applyPreflightedManagedPlan(entry) { ); const result = require('./install-executor').applyInstallPlan(preview.plan, { + beforeInstallStateRead: assertStateUnchanged, beforeOperationWrite({ operation }) { assertStateUnchanged(); const expected = preview.operations[operationIndex]; diff --git a/tests/lib/install-claude-skill-migration.test.js b/tests/lib/install-claude-skill-migration.test.js index d02ef00c6..a396ef389 100644 --- a/tests/lib/install-claude-skill-migration.test.js +++ b/tests/lib/install-claude-skill-migration.test.js @@ -490,12 +490,33 @@ function runTests() { operation.destinationPath === extraDestinationPath ))); + const updatedExtraOperation = { + ...extraOperation, + moduleId: 'skill-extra-updated', + }; + applyInstallPlan({ + ...extraPlan, + operations: [updatedExtraOperation], + statePreview: { + ...extraPlan.statePreview, + operations: [updatedExtraOperation], + }, + }); + const stateAfterMetadataUpdate = readInstallState(fixture.installStatePath); + const updatedExtraRecords = stateAfterMetadataUpdate.operations.filter(operation => ( + operation.destinationPath === extraDestinationPath + )); + assert.strictEqual(updatedExtraRecords.length, 1); + assert.strictEqual(updatedExtraRecords[0].moduleId, 'skill-extra-updated'); + const retry = applyInstallPlan(fixture.plan); assert.deepStrictEqual(retry.skippedOperations, []); const stateAfterRetry = readInstallState(fixture.installStatePath); - assert.ok(stateAfterRetry.operations.some(operation => ( + const retainedExtraRecords = stateAfterRetry.operations.filter(operation => ( operation.destinationPath === extraDestinationPath - ))); + )); + assert.strictEqual(retainedExtraRecords.length, 1); + assert.strictEqual(retainedExtraRecords[0].moduleId, 'skill-extra-updated'); const uninstall = runUninstall(fixture); assert.strictEqual(uninstall.summary.errorCount, 0);