fix(opencode): never report repair success while the pre-fix stub is retained

This commit is contained in:
Ali Arslan committed 2026-10-29 01:06:32 +05:00
1 parent 770eb29b9f
commit d2df54a2ef
2 files changed
+24 -3

No files matched your search

+5 -3
View File
@@ -447,9 +447,11 @@ function openCodeActivationCandidates(plan, previousOperations) {
return candidates;
}
function activationIsInactive(kind, operation, content) {
function activationIsInactive(kind, operation, content, strict = false) {
if (kind === 'plugin') {
return isInertOpenCodePluginStub(content);
// The final gate accepts only the V2-valid stub: the pre-fix one is inert but OpenCode rejects it.
return strict ? content.toString('utf8') === getDisabledOpenCodePluginContent()
: isInertOpenCodePluginStub(content);
}
const text = content.toString('utf8');
const relativePath = operation.sourceRelativePath || operation.destinationPath;
@@ -505,7 +507,7 @@ function assertOpenCodeHookDeactivationReady(plan, options = {}) {
const digest = content === null ? null : crypto.createHash('sha256').update(content).digest('hex');
snapshot.set(key, digest);
if (content === null) continue;
const inactive = activationIsInactive(kind, operation, content);
const inactive = activationIsInactive(kind, operation, content, options.requireInactive);
if (options.requireInactive) {
if (!inactive) {
throw new Error(`OpenCode hook activation remains active at ${operation.destinationPath}`);
@@ -347,6 +347,25 @@ function runTests() {
assert.strictEqual(fs.readFileSync(path.join(value.canonicalRoot, 'plugins/ecc-hooks.ts'), 'utf8'),
getDisabledOpenCodePluginContent());
});
test('a retained pre-fix declined factory stub never reports a successful repair', value => {
const state = addLegacyActivations(value, 'declined');
const oldStub = 'export default async () => ({});\n';
const stubPath = path.join(value.legacyRoot, 'plugins/ecc-hooks.ts');
fs.writeFileSync(stubPath, oldStub);
state.operations.find(operation => operation.destinationPath === stubPath).contentSha256 = sha256(oldStub);
writeInstallState(value.legacyStatePath, state);
const originalRename = fileSystem.renameSync;
fileSystem.renameSync = (from, ...args) => {
if (from === stubPath) throw Object.assign(new Error('Synthetic removal denial'), { code: 'EACCES' });
return originalRename(from, ...args);
};
try {
const result = repair(value, buildInertPayload);
assert.strictEqual(result.results[0].status, 'error', JSON.stringify(result));
assert.strictEqual(fs.readFileSync(stubPath, 'utf8'), oldStub);
} finally { fileSystem.renameSync = originalRename; }
assertBothLocksReleased(value);
});
for (const defect of ['modified', 'missing-digest', 'source-mismatch', 'source-missing', 'symlink', 'unrecorded-alias', 'merge-json']) {
test(`legacy ${defect} activation refuses before build and preserves ownership`, value => {
const state = addLegacyActivations(value);