mirror of
https://github.com/Jeuners/ECC.git
synced 2026-09-09 15:02:30 +02:00
fix(install): dedupe copy-file operations sharing a destination (#2429)
OpenCode ships override command files under .opencode/commands/ that shadow the generic commands/*.md sources. The manifest install plan recorded both writes against the same destination, so `ecc doctor` reported perpetual drift for the 29 shadowed command files and `ecc repair` "fixed" that phantom drift by copying the generic source over the correct override, corrupting the installed command while never clearing the warning. Dedupe copy-file operations by destination in createManifestInstallPlan, keeping the last writer to match the sequential apply order. install, repair, and doctor all consume this one builder, so a fresh install is clean and a single repair rewrites drifted state green. Fixes #2414
This commit is contained in:
parent
52f7e82a61
commit
c8c83ef428
2 changed files with 84 additions and 1 deletions
|
|
@ -14,6 +14,7 @@ const {
|
|||
createLegacyCompatInstallPlan,
|
||||
createLegacyInstallPlan,
|
||||
createManifestInstallPlan,
|
||||
dedupeCopyFileOperations,
|
||||
listAvailableLanguages,
|
||||
} = require('../../scripts/lib/install-executor');
|
||||
|
||||
|
|
@ -428,6 +429,59 @@ function runTests() {
|
|||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('dedupeCopyFileOperations keeps the last writer per destination (issue #2414)', () => {
|
||||
// Mirrors the OpenCode command scenario: a generic commands/<name>.md source
|
||||
// (preserve-relative-path) and an override .opencode/commands/<name>.md source
|
||||
// (sync-root-children) both write the same destination. Before the fix both
|
||||
// ops were recorded, so `doctor` reported perpetual drift and `repair`
|
||||
// clobbered the override. Only the last writer (the override) should survive.
|
||||
const dest = '/home/.opencode/commands/build-fix.md';
|
||||
const operations = [
|
||||
{ kind: 'copy-file', sourceRelativePath: 'commands/build-fix.md', destinationPath: dest, strategy: 'preserve-relative-path' },
|
||||
{ kind: 'copy-file', sourceRelativePath: '.opencode/commands/build-fix.md', destinationPath: dest, strategy: 'sync-root-children' },
|
||||
{ kind: 'copy-file', sourceRelativePath: 'commands/other.md', destinationPath: '/home/.opencode/commands/other.md', strategy: 'preserve-relative-path' },
|
||||
];
|
||||
|
||||
const deduped = dedupeCopyFileOperations(operations);
|
||||
|
||||
const destinations = deduped
|
||||
.filter(operation => operation.kind === 'copy-file')
|
||||
.map(operation => operation.destinationPath);
|
||||
assert.deepStrictEqual(
|
||||
destinations,
|
||||
[dest, '/home/.opencode/commands/other.md'],
|
||||
'each copy-file destination must appear exactly once'
|
||||
);
|
||||
const survivor = deduped.find(operation => operation.destinationPath === dest);
|
||||
assert.strictEqual(
|
||||
survivor.sourceRelativePath,
|
||||
'.opencode/commands/build-fix.md',
|
||||
'the last writer (override) must win, not the generic source'
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('dedupeCopyFileOperations leaves non copy-file operations and order intact', () => {
|
||||
// merge-json operations legitimately accumulate into a shared config file, so
|
||||
// multiple writes to one destination must be preserved; only redundant
|
||||
// copy-file writes are collapsed, and surviving ops keep their relative order.
|
||||
const mergeDest = '/home/.opencode/opencode.json';
|
||||
const operations = [
|
||||
{ kind: 'merge-json', sourceRelativePath: 'a.json', destinationPath: mergeDest },
|
||||
{ kind: 'copy-file', sourceRelativePath: 'src/x.md', destinationPath: '/home/x.md' },
|
||||
{ kind: 'merge-json', sourceRelativePath: 'b.json', destinationPath: mergeDest },
|
||||
{ kind: 'copy-file', sourceRelativePath: 'other/x.md', destinationPath: '/home/x.md' },
|
||||
{ kind: 'remove', destinationPath: '/home/legacy.md' },
|
||||
];
|
||||
|
||||
const deduped = dedupeCopyFileOperations(operations);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
deduped.map(operation => `${operation.kind}:${operation.sourceRelativePath || operation.destinationPath}`),
|
||||
['merge-json:a.json', 'merge-json:b.json', 'copy-file:other/x.md', 'remove:/home/legacy.md'],
|
||||
'both merge-json writes and the remove op survive; only the shadowed copy-file is dropped, order preserved'
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue