diff --git a/docs/releases/2.2.1/patch-execution.md b/docs/releases/2.2.1/patch-execution.md index bc8fcd2d3..f0faf4332 100644 --- a/docs/releases/2.2.1/patch-execution.md +++ b/docs/releases/2.2.1/patch-execution.md @@ -82,8 +82,26 @@ Successful managed-file upgrades retain their existing replacement semantics. ## Completion evidence -First batch 82bfd225 passed 4,215/4,215 tests and lint. Integrated full validation -and hosted checks are pending. Signing remains unavailable locally. +First batch 82bfd225 passed 4,215/4,215 tests and lint. The first combined run +at 8cc31f1e passed 4,370/4,372 tests, with 89.27% line and 81.52% branch coverage. +Its two failures exposed guided setup reporting success after a late collision +was filtered. Full-preview revalidation fixes that interaction; all 22 guided +setup tests now pass, including initially identical unowned files before later +writes. Final full-suite and hosted validation are pending. + +Windows hosted checks exposed fixture-owned descriptor cleanup and directory +rename assumptions in two new settings tests. The repaired fixtures preserve +Windows OS-refusal assertions and ECC parent-identity checks. CodeQL findings +338-341 were confined to test-source patterns; minimal assertion/interception +changes preserve coverage without alert dismissals. Hosted rescanning remains +required. + +The first combined packed artifact passed the isolated macOS lifecycle, 13 +memory MCP regressions, 12 actual Codex/Hermes protocol sessions, and 196 +GateGuard cases including quoted, unquoted, and tab-stripped heredocs. Package +helpers, public CLI aliases, and dry-run entrypoints were exercised from the +installed archive, not just the source checkout. Final source must be repacked +after the guided-setup integration repair. Signing remains unavailable locally. Pending final hosted validation, signed tag, publication, registry integrity readback, and clean lifecycle canaries. This document does not claim diff --git a/docs/releases/2.2.1/release-notes.md b/docs/releases/2.2.1/release-notes.md index d56c4f4ae..5bd695d1c 100644 --- a/docs/releases/2.2.1/release-notes.md +++ b/docs/releases/2.2.1/release-notes.md @@ -14,9 +14,12 @@ and signing evidence are tracked separately in the release checklist. absolute exemptions remain supported ([#2921](https://github.com/affaan-m/ECC/issues/2921)). - Installer writes reject collisions with untracked user-owned files. Failed - installs checkpoint only files they actually wrote, preserving the previous + installs refresh ownership hashes only for files they actually wrote, preserving the previous ownership hashes of untouched managed files ([#2964](https://github.com/affaan-m/ECC/issues/2964)). +- Guided setup revalidates its preview before ownership filtering, so files + appearing between preview and apply cause a clear retry instead of a false + success. Existing identical user files stay outside ECC ownership. - Uninstall respects `ECC_DRY_RUN=1`, including legacy Codex paths, and rejects invalid dry-run values instead of silently allowing deletion ([#2952](https://github.com/affaan-m/ECC/issues/2952)). diff --git a/scripts/lib/multi-harness-setup.js b/scripts/lib/multi-harness-setup.js index 30b9d469d..3b9b9eee0 100644 --- a/scripts/lib/multi-harness-setup.js +++ b/scripts/lib/multi-harness-setup.js @@ -373,7 +373,9 @@ async function applyPreflightedManagedPlan(entry) { : preflightManagedPlan(entry.preview.plan); const ownedDestinations = new Set(preview.ownershipSnapshot.destinations); let expectedStateFingerprint = preview.ownershipSnapshot.stateFingerprint; - let operationIndex = 0; + const expectedOperations = new Map(preview.operations.map(operation => [ + canonicalPath(operation.destinationPath), operation, + ])); const assertStateUnchanged = () => ( assertInstallStateUnchanged(preview.plan, expectedStateFingerprint) ); @@ -381,26 +383,35 @@ async function applyPreflightedManagedPlan(entry) { assertStateUnchanged(); expectedStateFingerprint = fingerprintInstallStateValue(state); }; + const assertOperationUnchanged = operation => { + const destination = canonicalPath(operation.destinationPath); + const expected = expectedOperations.get(destination); + const currentClassification = classifyManagedOperation(operation, ownedDestinations); + if ( + !expected + || expected.kind !== operation.kind + || expected.classification !== currentClassification + ) { + throw new Error( + `Refusing to write ${operation.destinationPath}: destination changed after Kimi preflight.` + ); + } + return destination; + }; const result = require('./install-executor').applyInstallPlan(preview.plan, { - beforeInstallStateRead: assertStateUnchanged, + beforeInstallStateRead() { + assertStateUnchanged(); + // Check the original preview before ownership filtering can skip a late + // collision. Guided setup must report the changed plan as a failure. + for (const operation of preview.plan.operations) { + assertOperationUnchanged(operation); + } + }, beforeOperationWrite({ operation }) { assertStateUnchanged(); - const expected = preview.operations[operationIndex]; - const currentClassification = classifyManagedOperation(operation, ownedDestinations); - const destination = canonicalPath(operation.destinationPath); - if ( - !expected - || expected.kind !== operation.kind - || canonicalPath(expected.destinationPath) !== destination - || expected.classification !== currentClassification - ) { - throw new Error( - `Refusing to write ${operation.destinationPath}: destination changed after Kimi preflight.` - ); - } + const destination = assertOperationUnchanged(operation); ownedDestinations.add(destination); - operationIndex += 1; }, beforeInstallStateWrite: prepareInstallStateWrite, }); diff --git a/tests/lib/multi-harness-setup.test.js b/tests/lib/multi-harness-setup.test.js index f6098e4b5..2858a7ab4 100644 --- a/tests/lib/multi-harness-setup.test.js +++ b/tests/lib/multi-harness-setup.test.js @@ -569,6 +569,38 @@ function writeManagedState(plan, overrides = {}) { } }); + await test('preserves an initially identical user file without shifting later write checks', async () => { + const root = tempDir('ecc-guided-identical-preserved-'); + const projection = require('../../scripts/lib/install-state-store-sync'); + const originalProjection = projection.projectCanonicalInstallState; + // This case verifies canonical ownership, not the optional derived cache. + projection.projectCanonicalInstallState = async () => ({ status: 'projected' }); + try { + const source = path.join(root, 'source.md'); + const userFile = path.join(root, '.kimi-code', 'rules', 'existing.md'); + const newFile = path.join(root, '.kimi-code', 'rules', 'new.md'); + writeFile(source, 'ecc\n'); + writeFile(userFile, 'ecc\n'); + const plan = managedPlan(root, [ + stateOperation(userFile, { sourcePath: source }), + stateOperation(newFile, { sourcePath: source }), + ]); + const result = await applyMultiHarnessPlan({ + harnesses: [{ id: 'kimi', preview: preflightManagedPlan(plan) }], + request: { harnesses: ['kimi'] }, + }); + assert.strictEqual(result.status, 'complete'); + assert.strictEqual(fs.readFileSync(userFile, 'utf8'), 'ecc\n'); + assert.strictEqual(fs.readFileSync(newFile, 'utf8'), 'ecc\n'); + const state = JSON.parse(fs.readFileSync(plan.installStatePath, 'utf8')); + assert.ok(!state.operations.some(operation => operation.destinationPath === userFile)); + assert.ok(state.operations.some(operation => operation.destinationPath === newFile)); + } finally { + projection.projectCanonicalInstallState = originalProjection; + fs.rmSync(root, { recursive: true, force: true }); + } + }); + await test('refuses conflicting JSON created after preview but before apply', async () => { const root = tempDir('ecc-guided-late-json-collision-'); try {