Merge PR #3256: reconcile obsolete managed installer files safely

Preserve main OpenCode lease and activation gates and native Claude namespace receipts. Detect same-size in-place edits using bigint modification and change times during hashing and before deletion; verify both races and actual public CLI reinstall lifecycles.
This commit is contained in:
haelyra
2026-10-10 17:49:10 -04:00
6 changed files with 981 additions and 9 deletions
@@ -5,6 +5,8 @@ const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const { createHash } = require('crypto');
const { readInstallState, writeInstallState } = require('../../scripts/lib/install-state');
const cli = path.resolve(__dirname, '../../scripts/ecc.js');
let passed = 0;
@@ -51,6 +53,37 @@ for (const target of ['claude', 'claude-project']) {
const report = JSON.parse(run(['doctor', '--target', target, '--json']).stdout);
assert.strictEqual(report.summary.errorCount, 0);
assert.strictEqual(report.summary.warningCount, 0);
// Seed obsolete managed records from an older install, then exercise the
// public reinstall instead of calling reconciliation directly.
const statePath = path.join(installRoot, 'ecc/install-state.json');
const obsoleteContent = '# obsolete managed rule\n';
const stalePaths = ['obsolete-unchanged.md', 'obsolete-edited.md']
.map(name => path.join(installRoot, 'rules/ecc/common', name));
const state = readInstallState(statePath);
for (const file of stalePaths) {
fs.writeFileSync(file, obsoleteContent);
state.operations.push({
kind: 'copy-file', moduleId: 'rules-core',
sourceRelativePath: `rules/common/${path.basename(file)}`,
destinationPath: file, strategy: 'preserve-relative-path',
ownership: 'managed', scaffoldOnly: false,
contentTransform: 'claude-manual-plugin-namespace',
contentSha256: createHash('sha256').update(obsoleteContent).digest('hex'),
});
}
writeInstallState(statePath, state);
const userEdit = '# user wants to keep this obsolete rule\n';
fs.writeFileSync(stalePaths[1], userEdit);
const reinstall = JSON.parse(run([...installArgs, '--json']).stdout).result;
assert.ok(!fs.existsSync(stalePaths[0]), 'Unmodified obsolete managed files should be removed');
assert.strictEqual(fs.readFileSync(stalePaths[1], 'utf8'), userEdit);
assert.deepStrictEqual(reinstall.reconciledStalePaths, [stalePaths[0]]);
assert.strictEqual(reinstall.warnings.filter(warning => warning.startsWith('Preserved orphaned file ')).length, 1);
assert.ok(readInstallState(statePath).operations.every(operation => !stalePaths.includes(operation.destinationPath)));
assertNativeNames();
const afterReinstall = JSON.parse(run(['doctor', '--target', target, '--json']).stdout);
assert.strictEqual(afterReinstall.summary.errorCount, 0);
assert.strictEqual(afterReinstall.summary.warningCount, 0);
fs.appendFileSync(rule, '\nlocal drift\n');
run(['repair', '--target', target, '--json']);
assertNativeNames();
@@ -62,6 +95,7 @@ for (const target of ['claude', 'claude-project']) {
run(['uninstall', '--target', target, '--json']);
assert.ok(!fs.existsSync(rule));
assert.strictEqual(fs.readFileSync(userFile, 'utf8'), 'keep this user file\n');
assert.strictEqual(fs.readFileSync(stalePaths[1], 'utf8'), userEdit);
passed++;
console.log(` PASS ${target} install, doctor, repair, reinstall and uninstall preserve native names`);
} catch (error) {