fix(opencode): guard legacy cleanup and portable consent fixtures

Preserve contributor history and current-main behavior while resolving the exact reviewed follow-up.

Source-PR: https://github.com/affaan-m/ECC/pull/3008
Source-Parent: d0dc1fcb08
Review-Manifest-SHA256: 8f15febd33dfa9f9ac6e70cb41f75ffee7aca47dd21782e841c33d4874341d6e
This commit is contained in:
affaan-m
2026-09-28 00:54:21 -04:00
parent d0dc1fcb08
commit ef8c2d8b9c
11 changed files with 529 additions and 92 deletions
+3 -2
View File
@@ -1964,7 +1964,7 @@ function preflightOpenCodeHookDeactivation(record, context, options = {}) {
// Migration does not replay operations into the old root. Inspect existing
// activations there, without treating historical merge-json records as new
// writes; the canonical destination is validated separately below.
assertOpenCodeRepairHookDeactivation({ ...legacyPlan, operations: [] }, { requireInactive: true });
assertOpenCodeRepairHookDeactivation({ ...legacyPlan, operations: [] }, { allowVerifiedLegacyRemoval: true });
assertOpenCodeRepairHookDeactivation(rawPlan, options);
return;
}
@@ -2298,9 +2298,10 @@ function repairInstalledStates(options = {}) {
homeDir: context.homeDir, projectRoot: context.projectRoot,
repoRoot: context.projectRoot, env: context.env,
});
const { getOpenCodeInstallRoots } = require('./install/apply');
const { getOpenCodeInstallRoots, assertOpenCodeLeaseCoverage } = require('./install/apply');
const roots = getOpenCodeInstallRoots({ adapter, targetRoot, homeDir: context.homeDir });
return withOpenCodeInstallLocks(roots, lease => {
assertOpenCodeLeaseCoverage({ adapter, targetRoot, homeDir: context.homeDir }, lease);
// Discovery precedes acquisition. Never repair from that stale state.
record = buildDiscoveryRecord(adapter, context, record.legacyLayout === 'opencode'
? getLegacyOpencodeLocation(context.homeDir) : null);
+49 -20
View File
@@ -14,8 +14,8 @@ const {
disableOpenCodeHookPluginRegistration,
getDisabledOpenCodePluginContent,
getRecordedHookConsent,
isOpenCodeHookActivationOperation,
isOpenCodePluginEntrypoint,
getOpenCodeActivationPathKind,
getOpenCodeSourceActivationKind,
planMaterializesHookRuntime,
shouldDisableOpenCodeHooks,
} = require('./hook-consent');
@@ -42,7 +42,8 @@ const {
prepareUserOwnedFileGuard,
preserveUnwrittenFiles,
} = require('./ownership-guard');
const { cleanupLegacyOpencodeInstall, getLegacyLocationForPlan } = require('./opencode-legacy-migration');
const { cleanupLegacyOpencodeInstall, getLegacyLocationForPlan, inspectLegacyOpencodeState,
verifyManagedLegacyFile } = require('./opencode-legacy-migration');
const { writeFileNoFollow } = require('./guarded-write');
const { withOpenCodeInstallLocks } = require('./opencode-install-lock');
const {
@@ -313,12 +314,7 @@ function getOpenCodeActivationKind(plan, operation) {
const relative = operation.destinationPath
? path.relative(plan.targetRoot, operation.destinationPath).split(path.sep).join('/').toLowerCase()
: '';
if (/^plugins\/(?:[^/]+\.(?:[cm]?js|ts)|[^/]+\/(?:index\.(?:[cm]?js|ts)|package\.json))$/.test(relative)) {
return 'plugin';
}
if (relative === 'opencode.json') return 'config';
if (isOpenCodePluginEntrypoint(operation)) return 'plugin';
return isOpenCodeHookActivationOperation(operation) ? 'config' : null;
return getOpenCodeActivationPathKind(relative) || getOpenCodeSourceActivationKind(operation);
}
function readOpenCodeAliasForAttribution(plan, destinationPath) {
@@ -473,8 +469,11 @@ function assertOpenCodeHookDeactivationReady(plan, options = {}) {
const desired = new Map(plan.operations.filter(operation => getOpenCodeActivationKind(plan, operation))
.map(operation => [comparablePath(operation.destinationPath), operation]));
const snapshot = new Map();
for (const [key, operation] of openCodeActivationCandidates(plan, [...previous.values()])) {
for (const [key, operation] of openCodeActivationCandidates(plan, [...previous.values(), ...(options.legacyOperations || [])])) {
const kind = getOpenCodeActivationKind(plan, operation);
if (kind === 'package') {
throw new Error(`Unsupported OpenCode package metadata deactivation: ${operation.destinationPath}`);
}
const expectedTransform = kind === 'plugin'
? 'opencode-disable-plugin-entrypoint' : 'opencode-disable-ecc-hooks';
const replacement = desired.get(key);
@@ -498,8 +497,14 @@ function assertOpenCodeHookDeactivationReady(plan, options = {}) {
}
continue;
}
if (kind === 'plugin' && inactive) continue;
if (inactive && (kind === 'plugin' || options.allowVerifiedLegacyRemoval)) continue;
const recorded = previous.get(key);
if (options.allowVerifiedLegacyRemoval && recorded) {
const verified = verifyManagedLegacyFile(recorded, {
targetRoot: plan.targetRoot, installStatePath: plan.installStatePath,
}, plan.sourceRoot);
if (verified.destinationPath && verified.digest === digest) continue;
}
if (!replacement || replacement.kind !== 'copy-file'
|| replacement.contentTransform !== expectedTransform
|| !recorded || recorded.contentSha256 !== digest) {
@@ -530,17 +535,29 @@ function getOpenCodeActivationWriteOptions(operation, snapshot) {
function getOpenCodeInstallRoots(plan) {
const roots = [plan.targetRoot];
const legacy = getLegacyLocationForPlan(plan);
if (legacy) {
try {
fs.lstatSync(legacy.targetRoot);
roots.push(legacy.targetRoot);
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
}
const inspection = inspectLegacyOpencodeState(legacy);
if (inspection.status === 'unreadable') throw new Error(inspection.error);
if (inspection.status === 'valid') roots.push(legacy.targetRoot);
return roots;
}
function inspectLegacyOpenCodeDeactivation(plan) {
const location = getLegacyLocationForPlan(plan);
if (!location || comparablePath(location.targetRoot) === comparablePath(plan.targetRoot)) return null;
const inspection = inspectLegacyOpencodeState(location);
if (inspection.status === 'unreadable') throw new Error(inspection.error);
if (inspection.status !== 'valid') return null;
const legacyPlan = { ...plan, ...location, operations: [] };
assertOpenCodeHookDeactivationReady(legacyPlan, { allowVerifiedLegacyRemoval: true });
return { plan: legacyPlan, operations: inspection.state.operations.filter(operation => operation.ownership === 'managed') };
}
function assertOpenCodeLeaseCoverage(plan, lease) {
if (plan.adapter?.target !== 'opencode') return;
// Reuse checks opaque ownership without acquiring extra roots out of order.
withOpenCodeInstallLocks(getOpenCodeInstallRoots(plan), () => {}, lease);
}
function findPreviousManagedHooks(previousState, plan, operation) {
if (
!previousState
@@ -704,7 +721,7 @@ function applyInstallPlan(plan, dependencies = {}) {
assertSafeInstallOperation(plan, { destinationPath: plan.installStatePath });
return withOpenCodeInstallLocks(
getOpenCodeInstallRoots(plan),
() => applyInstallPlanLocked(plan, dependencies, false),
lease => applyInstallPlanLocked(plan, { ...dependencies, opencodeLease: lease }, false),
dependencies.opencodeLease
);
}
@@ -732,6 +749,8 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
if (typeof beforeInstallStateRead === 'function') {
beforeInstallStateRead({ plan });
}
assertOpenCodeLeaseCoverage(plan, dependencies.opencodeLease);
const legacyActivation = inspectLegacyOpenCodeDeactivation(plan);
const activationSnapshot = assertOpenCodeHookDeactivationReady(plan);
const migration = prepareExcludedPathsReconciliation(
plan,
@@ -953,6 +972,9 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
];
}
assertOpenCodeLeaseCoverage(appliedPlan, dependencies.opencodeLease);
// Recheck removable bytes after canonical writes, before legacy cleanup.
inspectLegacyOpenCodeDeactivation(appliedPlan);
let opencodeMigrationWarnings = [];
try {
const opencodeMigration = cleanupLegacyOpencodeInstall(appliedPlan);
@@ -968,6 +990,12 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
];
}
if (legacyActivation) {
assertOpenCodeHookDeactivationReady(legacyActivation.plan, {
requireInactive: true, legacyOperations: legacyActivation.operations,
});
}
let excludedPathsRemoved = [];
let excludedPathsWarnings = [];
try {
@@ -1001,6 +1029,7 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
module.exports = {
applyInstallPlan,
assertOpenCodeActivationUnchanged,
assertOpenCodeLeaseCoverage,
assertOpenCodeHookDeactivationReady,
getOpenCodeActivationKind,
getOpenCodeActivationWriteOptions,
+21 -5
View File
@@ -69,10 +69,22 @@ function disableOpenCodeHookPluginRegistration(content, sourceRelativePath) {
}, null, 2)}\n`;
}
function getOpenCodeActivationPathKind(value) {
const relative = normalizeOperationPath(value);
if (relative === 'opencode.json') return 'config';
if (/^plugins\/[^/]+\/package\.json$/.test(relative)) return 'package';
if (/^plugins\/(?:[^/]+\.(?:[cm]?js|ts)|[^/]+\/index\.(?:[cm]?js|ts))$/.test(relative)) return 'plugin';
return null;
}
function getOpenCodeSourceActivationKind(operation = {}) {
const source = normalizeOperationPath(operation.sourceRelativePath);
if (!source.startsWith('.opencode/')) return null;
return getOpenCodeActivationPathKind(source.replace(/^\.opencode\/(?:dist\/)?/, ''));
}
function isOpenCodePluginEntrypoint(operation = {}) {
return /^\.opencode\/(?:dist\/)?plugins\/[^/]+\.(?:[cm]?js|ts)$/.test(
normalizeOperationPath(operation.sourceRelativePath)
);
return getOpenCodeSourceActivationKind(operation) === 'plugin';
}
function getDisabledOpenCodePluginContent() {
@@ -82,8 +94,7 @@ function getDisabledOpenCodePluginContent() {
}
function isOpenCodeHookActivationOperation(operation = {}) {
return normalizeOperationPath(operation.sourceRelativePath) === '.opencode/opencode.json'
|| isOpenCodePluginEntrypoint(operation);
return getOpenCodeSourceActivationKind(operation) !== null;
}
function isHookRuntimeOperation(operation = {}) {
@@ -146,6 +157,9 @@ function withoutHookRuntimeId(values) {
}
function withoutOpenCodeHookActivation(operation) {
if (getOpenCodeSourceActivationKind(operation) === 'package') {
throw new Error(`Unsupported OpenCode package metadata deactivation: ${operation.sourceRelativePath}`);
}
if (
!isOpenCodeHookActivationOperation(operation)
|| operation.kind !== 'copy-file'
@@ -310,6 +324,8 @@ module.exports = {
disableUnselectedOpenCodeHooks,
disableOpenCodeHookPluginRegistration,
getDisabledOpenCodePluginContent,
getOpenCodeActivationPathKind,
getOpenCodeSourceActivationKind,
formatHookCapabilityDisclosure,
getRecordedHookConsent,
isHookRuntimeOperation,
@@ -4,7 +4,7 @@ const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { readInstallState } = require('../install-state');
const { readInstallState, validateInstallState } = require('../install-state');
const { assertWithinTrustedRoot } = require('../path-safety');
const OPENCODE_TARGET = 'opencode';
@@ -65,10 +65,14 @@ function inspectLegacyOpencodeState(location) {
return { status: 'absent', state: null, error: null };
}
try {
if (!pathExists(location.installStatePath)) {
if (!pathExists(location.targetRoot)) {
return { status: 'absent', state: null, error: null };
}
const rootStat = fs.lstatSync(location.targetRoot);
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
return { status: 'invalid', state: null, error: null };
}
if (!pathExists(location.installStatePath)) return { status: 'absent', state: null, error: null };
const stateStat = fs.lstatSync(location.installStatePath);
if (
!rootStat.isDirectory()
@@ -78,7 +82,11 @@ function inspectLegacyOpencodeState(location) {
) {
return { status: 'invalid', state: null, error: null };
}
const state = readInstallState(location.installStatePath);
const { content } = hashFileNoFollow(location.installStatePath);
let state;
try { state = JSON.parse(content.toString('utf8')); }
catch { return { status: 'invalid', state: null, error: null }; }
if (!validateInstallState(state).valid) return { status: 'invalid', state: null, error: null };
const isOpencode = state.target.target === OPENCODE_TARGET
|| state.target.id === 'opencode-home';
if (
@@ -98,17 +106,17 @@ function inspectLegacyOpencodeState(location) {
}
}
function hashFileNoFollow(filePath) {
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
const descriptor = fs.openSync(filePath, flags);
function hashFileNoFollow(filePath, fileSystem = fs) {
const flags = fileSystem.constants.O_RDONLY | (fileSystem.constants.O_NOFOLLOW || 0);
const descriptor = fileSystem.openSync(filePath, flags);
try {
const before = fs.fstatSync(descriptor, { bigint: true });
const before = fileSystem.fstatSync(descriptor, { bigint: true });
if (!before.isFile()) {
throw new Error(`Refusing to read a non-file at ${filePath}`);
}
const content = fs.readFileSync(descriptor);
const after = fs.fstatSync(descriptor, { bigint: true });
const finalPathStat = fs.lstatSync(filePath, { bigint: true });
const content = fileSystem.readFileSync(descriptor);
const after = fileSystem.fstatSync(descriptor, { bigint: true });
const finalPathStat = fileSystem.lstatSync(filePath, { bigint: true });
const unchanged = before.dev === after.dev
&& before.ino === after.ino
&& before.size === after.size
@@ -123,11 +131,12 @@ function hashFileNoFollow(filePath) {
throw new Error(`Refusing to read a file that changed during validation: ${filePath}`);
}
return {
content,
digest: crypto.createHash('sha256').update(content).digest('hex'),
stat: after,
};
} finally {
fs.closeSync(descriptor);
fileSystem.closeSync(descriptor);
}
}
@@ -202,7 +211,7 @@ function verifyManagedLegacyFile(operation, location, sourceRoot) {
if (source.digest !== destination.digest) {
return { retainedPath: destinationPath };
}
return { destinationPath, stat: destination.stat };
return { destinationPath, digest: destination.digest, stat: destination.stat };
}
function pathExistsWith(fileSystem, filePath) {
@@ -257,6 +266,13 @@ function removeVerifiedLegacyFile(entry, location, fileSystem = fs) {
identityError.code = 'ESTALE';
throw identityError;
}
// Recheck bytes after quarantine: an in-place edit retains the same inode.
// Production cleanup entries carry the digest verified against ledger/source.
if (entry.digest && hashFileNoFollow(quarantinePath, fileSystem).digest !== entry.digest) {
const changed = new Error(`Legacy OpenCode file changed during quarantine: ${safePath}`);
changed.code = 'ESTALE';
throw changed;
}
fileSystem.rmSync(quarantinePath);
fileSystem.rmdirSync(quarantineDir);
return true;
@@ -396,4 +412,5 @@ module.exports = {
getLegacyLocationForPlan,
inspectLegacyOpencodeState,
removeVerifiedLegacyFile,
verifyManagedLegacyFile,
};
+10 -4
View File
@@ -157,10 +157,16 @@ test('a parent replacement before native open is refused even when the file iden
assert.strictEqual(typeof descriptor, 'number');
assert.strictEqual(closes, 1);
assert.strictEqual(truncates, 0);
const replacementIdentity = fs.statSync(file, { bigint: true });
assert.strictEqual(replacementIdentity.dev, fileIdentity.dev);
assert.strictEqual(replacementIdentity.ino, fileIdentity.ino);
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'old activation bytes');
const postconditionFd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
try {
const replacementIdentity = fs.fstatSync(postconditionFd, { bigint: true });
assert.ok(replacementIdentity.isFile());
assert.strictEqual(replacementIdentity.dev, fileIdentity.dev);
assert.strictEqual(replacementIdentity.ino, fileIdentity.ino);
assert.strictEqual(fs.readFileSync(postconditionFd, 'utf8'), 'old activation bytes');
} finally {
fs.closeSync(postconditionFd);
}
assert.ok(fs.statSync(`${parent}.old`).isDirectory());
});
test('a denied native open preserves its cause without closing an unallocated descriptor', ({ file, options }) => {
@@ -0,0 +1,44 @@
'use strict';
const fs = require('fs');
const path = require('path');
const Module = require('module');
// A private CommonJS graph for filesystem fault fixtures. Dependencies share
// this graph (including opaque lock identities), never the process module cache.
function createFileSystemLoader(fileSystem) {
const sourceRoot = path.resolve(__dirname, '../../../scripts');
const cache = new Map();
function load(filename) {
const absolute = path.resolve(filename);
const relative = path.relative(sourceRoot, absolute);
if (relative.startsWith('..') || path.isAbsolute(relative) || !absolute.endsWith('.js')) {
throw new Error('Fixture loader requires a JavaScript module under scripts/');
}
if (cache.has(absolute)) return cache.get(absolute).exports;
const child = new Module(absolute, module);
child.filename = absolute;
child.paths = Module._nodeModulePaths(path.dirname(absolute));
const nativeRequire = Module.createRequire(absolute);
child.require = request => {
if (request === 'fs' || request === 'node:fs') return fileSystem;
const resolved = nativeRequire.resolve(request);
const dependency = path.relative(sourceRoot, resolved);
if (path.isAbsolute(resolved) && !dependency.startsWith('..')
&& !path.isAbsolute(dependency) && resolved.endsWith('.js')) return load(resolved);
return nativeRequire(request);
};
cache.set(absolute, child);
try {
child._compile(fs.readFileSync(absolute, 'utf8'), absolute);
child.loaded = true;
return child.exports;
} catch (error) {
cache.delete(absolute);
throw error;
}
}
return load;
}
module.exports = { createFileSystemLoader };
+20
View File
@@ -11,6 +11,7 @@ const {
formatHookCapabilityDisclosure,
getRecordedHookConsent,
isHookRuntimeOperation,
isOpenCodePluginEntrypoint,
planMaterializesHookRuntime,
resolveHookConsentFlags,
withHookConsent,
@@ -202,6 +203,25 @@ function runTests() {
assert.strictEqual(getRecordedHookConsent({ operations: [{ kind: 'update-claude-settings' }] }), 'enabled');
})) passed++; else failed++;
if (test('source classification covers nested JavaScript but refuses package metadata deactivation', () => {
for (const extension of ['ts', 'js', 'mjs', 'cjs']) {
for (const sourceRelativePath of [`.opencode/plugins/custom/index.${extension}`,
`.OPENCODE\\DIST\\PLUGINS\\CUSTOM\\INDEX.${extension.toUpperCase()}`]) {
const operation = { kind: 'copy-file', moduleId: 'platform-configs', sourceRelativePath };
assert.strictEqual(isOpenCodePluginEntrypoint(operation), true, sourceRelativePath);
const plan = withHookConsent({ target: 'opencode', operations: [operation], selectedModuleIds: [] });
assert.strictEqual(plan.operations[0].contentTransform, 'opencode-disable-plugin-entrypoint');
}
}
const operation = { kind: 'copy-file', sourceRelativePath: '.opencode/plugins/custom/package.json' };
for (const decision of [null, 'declined']) {
assert.throws(() => withHookConsent({ target: 'opencode', operations: [operation], selectedModuleIds: [] }, decision),
/unsupported.*package/i);
}
assert.strictEqual(isOpenCodePluginEntrypoint(operation), false);
assert.strictEqual(isOpenCodePluginEntrypoint({ sourceRelativePath: '.opencode/plugins/lib/utility.js' }), false);
})) passed++; else failed++;
if (test('formats one numbered disclosure line per capability group', () => {
const disclosure = formatHookCapabilityDisclosure();
const lines = disclosure.split('\n');
+74 -29
View File
@@ -7,6 +7,7 @@ const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { createFileSystemLoader } = require('./helpers/load-with-file-system');
const {
buildDoctorReport,
@@ -206,11 +207,11 @@ function writeOpencodeState(homeDir, overrides = {}) {
};
}
function writeRecordedOpenCodeActivation(homeDir) {
function writeRecordedOpenCodeActivation(homeDir, sourceRoot = REPO_ROOT) {
const targetRoot = path.join(homeDir, '.config', 'opencode');
const operations = ['opencode.json', 'plugins/ecc-hooks.ts', 'plugins/index.ts'].map(relativePath => {
const sourceRelativePath = `.opencode/${relativePath}`;
const content = fs.readFileSync(path.join(REPO_ROOT, sourceRelativePath));
const content = fs.readFileSync(path.join(sourceRoot, sourceRelativePath));
const destinationPath = path.join(targetRoot, relativePath);
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, content);
@@ -227,6 +228,32 @@ function writeRecordedOpenCodeActivation(homeDir) {
});
}
// These race fixtures read public source as inert bytes into a private source
// tree. Fake payload construction must never move/write repository build output.
function withPrivateOpenCodeSource(homeDir, callback) {
const sourceRoot = path.join(homeDir, 'source');
fs.mkdirSync(path.join(sourceRoot, 'manifests'), { recursive: true });
const files = {
'package.json': { name: 'private-opencode-race-fixture', version: CURRENT_PACKAGE_VERSION },
'manifests/install-modules.json': { version: CURRENT_MANIFEST_VERSION, modules: [{
id: 'platform-configs', kind: 'platform', description: 'Private race fixture.',
paths: ['.opencode'], targets: ['opencode'], dependencies: [],
defaultInstall: false, cost: 'light', stability: 'stable',
}] },
'manifests/install-profiles.json': { version: 1, profiles: {} },
'manifests/install-components.json': { version: 1, components: [] },
};
for (const [relative, value] of Object.entries(files)) {
fs.writeFileSync(path.join(sourceRoot, relative), formatJson(value));
}
for (const relative of ['opencode.json', 'plugins/ecc-hooks.ts', 'plugins/index.ts']) {
const destination = path.join(sourceRoot, '.opencode', relative);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.copyFileSync(path.join(REPO_ROOT, '.opencode', relative), destination);
}
callback(sourceRoot);
}
function withTemporarilyMovedPath(filePath, callback) {
if (!fs.existsSync(filePath)) {
try {
@@ -2159,28 +2186,33 @@ function runTests() {
if (test('OpenCode repair preserves activation bytes changed between health inspection and write', () => {
const homeDir = createTempDir('install-lifecycle-opencode-health-race-');
const fileSystem = { ...fs };
const cacheEntry = require.cache[require.resolve('../../scripts/lib/install-lifecycle')];
const isolatedRepair = createFileSystemLoader(fileSystem)(
require.resolve('../../scripts/lib/install-lifecycle')
).repairInstalledStates;
const originalOpenSync = fs.openSync;
const originalReadFileSync = fs.readFileSync;
const originalCloseSync = fs.closeSync;
const descriptors = new Set();
try {
withTemporarilyMovedPath(path.join(REPO_ROOT, '.opencode', 'dist'), () => {
const recorded = writeRecordedOpenCodeActivation(homeDir);
withPrivateOpenCodeSource(homeDir, sourceRoot => {
const recorded = writeRecordedOpenCodeActivation(homeDir, sourceRoot);
const pluginPath = path.join(recorded.targetRoot, 'plugins', 'ecc-hooks.ts');
const canonicalPluginPath = fs.realpathSync(pluginPath);
const changedContent = 'globalThis.userChangedHook = true;\n';
const stateBefore = fs.readFileSync(recorded.installStatePath);
let changed = false;
fs.openSync = function trackActivationDescriptor(candidate, ...args) {
fileSystem.openSync = function trackActivationDescriptor(candidate, ...args) {
const descriptor = originalOpenSync.call(fs, candidate, ...args);
if (typeof candidate === 'string' && [pluginPath, canonicalPluginPath].includes(path.resolve(candidate))) descriptors.add(descriptor);
return descriptor;
};
fs.closeSync = function releaseActivationDescriptor(descriptor) {
fileSystem.closeSync = function releaseActivationDescriptor(descriptor) {
descriptors.delete(descriptor);
return originalCloseSync.call(fs, descriptor);
};
fs.readFileSync = function mutateAfterHealthRead(candidate, ...args) {
fileSystem.readFileSync = function mutateAfterHealthRead(candidate, ...args) {
const content = originalReadFileSync.call(fs, candidate, ...args);
// Lifecycle reads pass an encoding argument; the consent snapshot
// reads the descriptor as raw bytes with no second argument.
@@ -2192,8 +2224,8 @@ function runTests() {
};
let result;
try {
result = repairInstalledStates({
repoRoot: REPO_ROOT, homeDir, projectRoot: homeDir, targets: ['opencode'],
result = isolatedRepair({
repoRoot: sourceRoot, homeDir, projectRoot: homeDir, targets: ['opencode'],
buildOpencodePayload(repoRoot) {
const distDir = path.join(repoRoot, '.opencode', 'dist');
fs.mkdirSync(path.join(distDir, 'plugins'), { recursive: true });
@@ -2202,9 +2234,9 @@ function runTests() {
},
});
} finally {
fs.openSync = originalOpenSync;
fs.readFileSync = originalReadFileSync;
fs.closeSync = originalCloseSync;
fileSystem.openSync = originalOpenSync;
fileSystem.readFileSync = originalReadFileSync;
fileSystem.closeSync = originalCloseSync;
}
assert.strictEqual(changed, true, 'The health-read boundary was exercised');
assert.strictEqual(result.results[0].status, 'error');
@@ -2219,39 +2251,48 @@ function runTests() {
assert.notStrictEqual(result.results[0].stateRefreshed, true);
});
} finally {
fs.openSync = originalOpenSync;
fs.readFileSync = originalReadFileSync;
fs.closeSync = originalCloseSync;
fileSystem.openSync = originalOpenSync;
fileSystem.readFileSync = originalReadFileSync;
fileSystem.closeSync = originalCloseSync;
cleanup(homeDir);
assert.strictEqual(descriptors.size, 0, 'Every tracked descriptor must close');
assert.strictEqual(fs.openSync, originalOpenSync, 'Native fs must remain untouched');
assert.strictEqual(fs.closeSync, originalCloseSync);
assert.strictEqual(require.cache[require.resolve('../../scripts/lib/install-lifecycle')], cacheEntry);
}
})) passed++; else failed++;
if (test('OpenCode repair rejects an active alias introduced during writes before refreshing state', () => {
const homeDir = createTempDir('install-lifecycle-opencode-alias-race-');
const fileSystem = { ...fs };
const cacheEntry = require.cache[require.resolve('../../scripts/lib/install-lifecycle')];
const isolatedRepair = createFileSystemLoader(fileSystem)(
require.resolve('../../scripts/lib/install-lifecycle')
).repairInstalledStates;
const originalOpenSync = fs.openSync;
const originalWriteFileSync = fs.writeFileSync;
const originalWriteSync = fs.writeSync;
const originalCloseSync = fs.closeSync;
const descriptors = new Set();
try {
withTemporarilyMovedPath(path.join(REPO_ROOT, '.opencode', 'dist'), () => {
const recorded = writeRecordedOpenCodeActivation(homeDir);
withPrivateOpenCodeSource(homeDir, sourceRoot => {
const recorded = writeRecordedOpenCodeActivation(homeDir, sourceRoot);
const pluginPath = path.join(recorded.targetRoot, 'plugins', 'index.ts');
const canonicalPluginPath = fs.realpathSync(pluginPath);
const aliasPath = path.join(recorded.targetRoot, 'plugins', 'index.js');
const aliasContent = fs.readFileSync(path.join(REPO_ROOT, '.opencode', 'plugins', 'index.ts'), 'utf8');
const aliasContent = fs.readFileSync(path.join(sourceRoot, '.opencode', 'plugins', 'index.ts'), 'utf8');
const stateBefore = fs.readFileSync(recorded.installStatePath);
let inserted = false;
fs.openSync = function trackPluginWriteDescriptor(candidate, ...args) {
fileSystem.openSync = function trackPluginWriteDescriptor(candidate, ...args) {
const descriptor = originalOpenSync.call(fs, candidate, ...args);
if (typeof candidate === 'string' && [pluginPath, canonicalPluginPath].includes(path.resolve(candidate))) descriptors.add(descriptor);
return descriptor;
};
fs.closeSync = function releasePluginWriteDescriptor(descriptor) {
fileSystem.closeSync = function releasePluginWriteDescriptor(descriptor) {
descriptors.delete(descriptor);
return originalCloseSync.call(fs, descriptor);
};
fs.writeSync = function insertAliasAfterPluginWrite(candidate, ...args) {
fileSystem.writeSync = function insertAliasAfterPluginWrite(candidate, ...args) {
const result = originalWriteSync.call(fs, candidate, ...args);
if (!inserted && descriptors.has(candidate)) {
inserted = true;
@@ -2261,8 +2302,8 @@ function runTests() {
};
let result;
try {
result = repairInstalledStates({
repoRoot: REPO_ROOT, homeDir, projectRoot: homeDir, targets: ['opencode'],
result = isolatedRepair({
repoRoot: sourceRoot, homeDir, projectRoot: homeDir, targets: ['opencode'],
buildOpencodePayload(repoRoot) {
const distDir = path.join(repoRoot, '.opencode', 'dist');
fs.mkdirSync(path.join(distDir, 'plugins'), { recursive: true });
@@ -2271,9 +2312,9 @@ function runTests() {
},
});
} finally {
fs.openSync = originalOpenSync;
fs.writeSync = originalWriteSync;
fs.closeSync = originalCloseSync;
fileSystem.openSync = originalOpenSync;
fileSystem.writeSync = originalWriteSync;
fileSystem.closeSync = originalCloseSync;
}
assert.strictEqual(inserted, true, 'The plugin-write boundary was exercised');
assert.strictEqual(result.results[0].status, 'error');
@@ -2285,10 +2326,14 @@ function runTests() {
assert.notStrictEqual(result.results[0].stateRefreshed, true);
});
} finally {
fs.openSync = originalOpenSync;
fs.writeSync = originalWriteSync;
fs.closeSync = originalCloseSync;
fileSystem.openSync = originalOpenSync;
fileSystem.writeSync = originalWriteSync;
fileSystem.closeSync = originalCloseSync;
cleanup(homeDir);
assert.strictEqual(descriptors.size, 0, 'Every tracked descriptor must close');
assert.strictEqual(fs.openSync, originalOpenSync, 'Native fs must remain untouched');
assert.strictEqual(fs.closeSync, originalCloseSync);
assert.strictEqual(require.cache[require.resolve('../../scripts/lib/install-lifecycle')], cacheEntry);
}
})) passed++; else failed++;
+199 -3
View File
@@ -6,10 +6,14 @@ const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { applyInstallPlan } = require('../../scripts/lib/install/apply');
const { repairInstalledStates } = require('../../scripts/lib/install-lifecycle');
const { createFileSystemLoader } = require('./helpers/load-with-file-system');
const fileSystem = { ...fs };
const load = createFileSystemLoader(fileSystem);
const { applyInstallPlan } = load(require.resolve('../../scripts/lib/install/apply'));
const { withHookConsent } = require('../../scripts/lib/install/hook-consent');
const { repairInstalledStates, buildDoctorReport } = load(require.resolve('../../scripts/lib/install-lifecycle'));
const { createInstallState, readInstallState, writeInstallState } = require('../../scripts/lib/install-state');
const { withOpenCodeInstallLocks } = require('../../scripts/lib/install/opencode-install-lock');
const { withOpenCodeInstallLocks } = load(require.resolve('../../scripts/lib/install/opencode-install-lock'));
const SOURCE_RELATIVE_PATH = path.join('skills', 'skill-comply', 'SKILL.md');
const SKILL_CONTENT = '---\nname: skill-comply\ndescription: Synthetic migration fixture.\n---\n\n# Inert fixture\n';
@@ -131,6 +135,43 @@ function assertBothLocksReleased(value) {
}
}
function addLegacyActivations(value, consent = null) {
const manifestPath = path.join(value.sourceRoot, 'manifests', 'install-modules.json');
const manifest = JSON.parse(fs.readFileSync(manifestPath));
manifest.modules.push({ id: 'platform-configs', kind: 'platform', description: 'Inert config fixture.',
paths: ['.opencode'], targets: ['opencode'], dependencies: [], defaultInstall: false,
cost: 'light', stability: 'stable' });
writeJson(manifestPath, manifest);
const state = readInstallState(value.legacyStatePath);
state.request.modules.push('platform-configs');
state.request.hookConsent = consent;
state.resolution.selectedModules.push('platform-configs');
for (const [relative, content] of [
['opencode.json', '{"plugin":["./plugins"],"userSetting":true}\n'],
['plugins/ecc-hooks.ts', 'export default async () => ({ "session.created": () => {} });\n'],
]) {
const sourceRelativePath = `.opencode/${relative}`;
const sourcePath = path.join(value.sourceRoot, sourceRelativePath);
const destinationPath = path.join(value.legacyRoot, relative);
for (const file of [sourcePath, destinationPath]) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, content);
}
state.operations.push({ kind: 'copy-file', moduleId: 'platform-configs', sourceRelativePath,
destinationPath, ownership: 'managed', scaffoldOnly: false, strategy: 'preserve-relative-path',
contentSha256: sha256(content) });
}
writeInstallState(value.legacyStatePath, state);
return state;
}
function buildInertPayload(sourceRoot) {
const dist = path.join(sourceRoot, '.opencode', 'dist');
fs.mkdirSync(path.join(dist, 'plugins'), { recursive: true });
fs.mkdirSync(path.join(dist, 'tools'), { recursive: true });
fs.writeFileSync(path.join(dist, 'index.js'), 'module.exports = {};\n');
}
function runTests() {
let passed = 0;
let failed = 0;
@@ -188,6 +229,161 @@ function runTests() {
assertSourceUnchanged(value);
assertBothLocksReleased(value);
});
for (const unrelated of ['directory', 'file', 'symlink', 'invalid-state', 'malformed-state', 'non-ECC-state']) {
test(`canonical apply ignores an unrelated legacy ${unrelated} without locking it`, value => {
fs.rmSync(value.legacyRoot, { recursive: true, force: true });
if (unrelated === 'file') fs.writeFileSync(value.legacyRoot, 'user file');
else if (unrelated === 'symlink') {
fs.mkdirSync(value.canonicalRoot, { recursive: true });
fs.symlinkSync(value.canonicalRoot, value.legacyRoot, process.platform === 'win32' ? 'junction' : 'dir');
} else {
fs.mkdirSync(value.legacyRoot, { recursive: true });
if (unrelated === 'malformed-state') fs.writeFileSync(value.legacyStatePath, '{malformed');
if (unrelated === 'invalid-state') writeJson(value.legacyStatePath, { unrelated: true });
if (unrelated === 'non-ECC-state') {
const state = { ...value.canonicalPlan.statePreview, target: { id: 'user-tool', target: 'user-tool',
root: value.legacyRoot, installStatePath: value.legacyStatePath } };
writeJson(value.legacyStatePath, state);
}
}
const previousLedger = fs.existsSync(value.legacyStatePath)
? fs.readFileSync(value.legacyStatePath) : null;
const original = fileSystem.openSync;
let legacyLocks = 0;
fileSystem.openSync = (candidate, ...args) => {
if (typeof candidate === 'string' && candidate.startsWith(lockPath(value.legacyRoot))) { legacyLocks++; throw new Error('Unrelated legacy root must not be locked'); }
return original(candidate, ...args);
};
try {
const plan = withHookConsent({ ...value.canonicalPlan, operations: [],
statePreview: { ...value.canonicalPlan.statePreview, operations: [] } });
assert.strictEqual(applyInstallPlan(plan).applied, true);
assert.strictEqual(legacyLocks, 0);
if (previousLedger) assert.deepStrictEqual(fs.readFileSync(value.legacyStatePath), previousLedger);
if (unrelated === 'file') assert.strictEqual(fs.readFileSync(value.legacyRoot, 'utf8'), 'user file');
if (unrelated === 'symlink') assert.ok(fs.lstatSync(value.legacyRoot).isSymbolicLink());
} finally { fileSystem.openSync = original; }
assert.strictEqual(fs.existsSync(lockPath(value.canonicalRoot)), false);
});
}
test('unreadable legacy ECC state remains an explicit failure before writes', value => {
const original = fileSystem.openSync;
let attempted = false;
fileSystem.openSync = (candidate, ...args) => {
if (candidate === value.legacyStatePath) {
attempted = true;
throw Object.assign(new Error('Synthetic unreadable legacy state'), { code: 'EACCES' });
}
return original(candidate, ...args);
};
try {
assert.throws(() => applyInstallPlan(value.canonicalPlan), /Unable to inspect legacy/);
assert.ok(attempted);
assert.strictEqual(fs.existsSync(value.canonicalStatePath), false);
} finally { fileSystem.openSync = original; }
});
for (const boundary of ['beforeInstallStateRead', 'beforeInstallStateWrite']) {
test(`new valid legacy state at ${boundary} is never cleaned without its lock`, value => {
const originalState = fs.readFileSync(value.legacyStatePath);
fs.rmSync(value.legacyRoot, { recursive: true, force: true });
let injected = false;
assert.throws(() => applyInstallPlan(value.canonicalPlan, {
[boundary]() {
injected = true;
fs.mkdirSync(path.dirname(value.legacyFile), { recursive: true });
fs.writeFileSync(value.legacyFile, SKILL_CONTENT);
fs.writeFileSync(value.legacyStatePath, originalState);
},
}), /lease does not cover/);
assert.ok(injected);
assert.deepStrictEqual(fs.readFileSync(value.legacyStatePath), originalState);
assert.strictEqual(fs.readFileSync(value.legacyFile, 'utf8'), SKILL_CONTENT);
assert.strictEqual(fs.existsSync(lockPath(value.canonicalRoot)), false);
assert.strictEqual(fs.existsSync(lockPath(value.legacyRoot)), false);
});
}
for (const consent of [null, 'declined']) {
test(`verified active legacy copies migrate under ${consent || 'default'} consent with no hooks`, value => {
addLegacyActivations(value, consent);
const before = fs.readFileSync(value.legacyStatePath);
const doctor = buildDoctorReport({ repoRoot: value.sourceRoot, homeDir: value.homeDir,
projectRoot: value.homeDir, targets: ['opencode'], env: {} });
assert.ok(doctor.results[0].issues.some(issue => issue.code === 'legacy-opencode-layout'));
assert.ok(!doctor.results[0].issues.some(issue => issue.code === 'opencode-hook-consent-violation'));
assert.deepStrictEqual(fs.readFileSync(value.legacyStatePath), before);
let builds = 0;
const result = repair(value, sourceRoot => {
builds++;
for (const target of [value.canonicalRoot, value.legacyRoot]) assert.ok(fs.existsSync(lockPath(target)));
buildInertPayload(sourceRoot);
});
assert.strictEqual(result.results[0].status, 'repaired', JSON.stringify(result));
assert.strictEqual(builds, 1);
const state = readInstallState(value.canonicalStatePath);
assert.ok(!state.resolution.selectedModules.includes('hooks-runtime'));
assert.notStrictEqual(state.request.hookConsent, 'enabled');
assert.strictEqual(fs.readFileSync(path.join(value.canonicalRoot, 'plugins/ecc-hooks.ts'), 'utf8'),
'export default async () => ({});\n');
assert.deepStrictEqual(JSON.parse(fs.readFileSync(path.join(value.canonicalRoot, 'opencode.json'))).plugin, []);
for (const relative of ['opencode.json', 'plugins/ecc-hooks.ts', 'ecc-install-state.json']) {
assert.strictEqual(fs.existsSync(path.join(value.legacyRoot, relative)), false);
}
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);
const plugin = path.join(value.legacyRoot, 'plugins/ecc-hooks.ts');
const source = path.join(value.sourceRoot, '.opencode/plugins/ecc-hooks.ts');
if (defect === 'modified') fs.writeFileSync(plugin, 'user edit\n');
if (defect === 'missing-digest') delete state.operations[2].contentSha256;
if (defect === 'source-mismatch') fs.writeFileSync(source, 'different trusted source\n');
if (defect === 'source-missing') fs.unlinkSync(source);
if (defect === 'symlink') { fs.unlinkSync(plugin); fs.symlinkSync(source, plugin); }
if (defect === 'unrecorded-alias') fs.copyFileSync(source, path.join(value.legacyRoot, 'plugins/index.js'));
if (defect === 'merge-json') state.operations[1].kind = 'merge-json';
writeInstallState(value.legacyStatePath, state);
const before = fs.readFileSync(value.legacyStatePath);
let builds = 0;
const result = repair(value, () => { builds++; throw new Error('Must refuse before build'); });
assert.strictEqual(result.results[0].status, 'error', defect);
assert.strictEqual(builds, 0);
assert.deepStrictEqual(fs.readFileSync(value.legacyStatePath), before);
assert.strictEqual(fs.existsSync(value.canonicalStatePath), false);
assertBothLocksReleased(value);
});
}
for (const failure of ['late-edit', 'remove-error', 'quarantine-edit']) {
test(`legacy ${failure} never reports successful deactivation`, value => {
addLegacyActivations(value);
const plugin = path.join(value.legacyRoot, 'plugins/ecc-hooks.ts');
const originalRename = fileSystem.renameSync;
const before = fs.readFileSync(value.legacyStatePath);
let failedRemoval = false;
fileSystem.renameSync = (from, ...args) => {
if (failure === 'remove-error' && from === plugin) {
failedRemoval = true;
throw Object.assign(new Error('Synthetic removal denial'), { code: 'EACCES' });
}
if (failure === 'quarantine-edit' && from === plugin) fs.writeFileSync(plugin, 'user edit at quarantine\n');
return originalRename(from, ...args);
};
try {
const result = repair(value, sourceRoot => {
buildInertPayload(sourceRoot);
if (failure === 'late-edit') fs.writeFileSync(plugin, 'user edit after preflight\n');
});
assert.strictEqual(result.results[0].status, 'error', JSON.stringify(result));
assert.notStrictEqual(result.results[0].stateRefreshed, true);
assert.deepStrictEqual(fs.readFileSync(value.legacyStatePath), before);
if (failure === 'late-edit') assert.strictEqual(fs.readFileSync(plugin, 'utf8'), 'user edit after preflight\n');
else if (failure === 'quarantine-edit') assert.strictEqual(fs.readFileSync(plugin, 'utf8'), 'user edit at quarantine\n');
else assert.ok(failedRemoval, 'The actual cleanup removal was attempted');
} finally { fileSystem.renameSync = originalRename; }
assertBothLocksReleased(value);
});
}
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
return { passed, failed };
}
+76 -13
View File
@@ -5,10 +5,13 @@ const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { applyInstallPlan } = require('../../scripts/lib/install/apply');
const { createFileSystemLoader } = require('./helpers/load-with-file-system');
const fileSystem = { ...fs };
const load = createFileSystemLoader(fileSystem);
const { applyInstallPlan } = load(require.resolve('../../scripts/lib/install/apply'));
const { withHookConsent } = require('../../scripts/lib/install/hook-consent');
const { createInstallState, readInstallState, writeInstallState } = require('../../scripts/lib/install-state');
const { buildDoctorReport, repairInstalledStates } = require('../../scripts/lib/install-lifecycle');
const { buildDoctorReport, repairInstalledStates } = load(require.resolve('../../scripts/lib/install-lifecycle'));
const REPO_ROOT = path.resolve(__dirname, '../..');
const sha256 = value => crypto.createHash('sha256').update(value).digest('hex');
@@ -98,8 +101,16 @@ function fixture(callback, enabled = false) {
const basePlan = { target: 'opencode', adapter, homeDir, sourceRoot, targetRoot,
installRoot: targetRoot, installStatePath, operations, warnings: [],
selectedModuleIds: state.resolution.selectedModules, statePreview: state };
callback({ root, homeDir, sourceRoot, targetRoot, installStatePath, state, basePlan,
declinePlan: withHookConsent(basePlan, 'declined') });
const previousCwd = process.cwd();
try {
// Hosted Windows checkouts and os.tmpdir() may be on different drives.
// Anchor relative-root callers inside their private fixture on every OS.
process.chdir(root);
callback({ root, homeDir, sourceRoot, targetRoot, installStatePath, state, basePlan,
declinePlan: withHookConsent(basePlan, 'declined') });
} finally {
process.chdir(previousCwd);
}
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
@@ -112,11 +123,11 @@ function repair(value, extra = {}) {
}
function withWritableOpenMutation(filePath, mutate, callback) {
const originalOpen = fs.openSync;
const originalClose = fs.closeSync;
const originalOpen = fileSystem.openSync;
const originalClose = fileSystem.closeSync;
let injected = false;
const descriptors = new Set();
fs.openSync = function (candidate, flags, ...rest) {
fileSystem.openSync = function (candidate, flags, ...rest) {
const writable = typeof flags === 'number'
? Boolean(flags & (fs.constants.O_WRONLY | fs.constants.O_RDWR))
: /[wa+]/.test(flags);
@@ -129,7 +140,7 @@ function withWritableOpenMutation(filePath, mutate, callback) {
if (matches && writable) descriptors.add(fd);
return fd;
};
fs.closeSync = function (fd) {
fileSystem.closeSync = function (fd) {
descriptors.delete(fd);
return originalClose.call(fs, fd);
};
@@ -138,8 +149,8 @@ function withWritableOpenMutation(filePath, mutate, callback) {
assert.ok(injected, 'The destination writable-open boundary must be exercised');
assert.strictEqual(descriptors.size, 0, 'Every owned writable descriptor must close');
} finally {
fs.openSync = originalOpen;
fs.closeSync = originalClose;
fileSystem.openSync = originalOpen;
fileSystem.closeSync = originalClose;
}
}
@@ -158,6 +169,27 @@ function runTests() {
try { callback(); passed++; console.log(` PASS ${name}`); }
catch (error) { failed++; console.error(` FAIL ${name}: ${error.stack}`); }
};
test('relative-root fixture remains relative across Windows checkout and temp drives', () => {
const source = 'C:\\private\\source';
assert.strictEqual(path.win32.isAbsolute(path.win32.relative('D:\\checkout', source)), true);
const relative = path.win32.relative('C:\\private', source);
assert.strictEqual(relative, 'source');
assert.strictEqual(path.win32.isAbsolute(relative), false);
assert.strictEqual(path.win32.resolve('C:\\private', relative), source);
});
test('private filesystem injection never patches the process filesystem or module cache', () => fixture(value => {
const nativeOpen = fs.openSync;
const nativeClose = fs.closeSync;
const cachedApply = require.cache[require.resolve('../../scripts/lib/install/apply')];
const destination = path.join(value.targetRoot, 'plugins/ecc-hooks.ts');
withWritableOpenMutation(destination, () => {
assert.strictEqual(fs.openSync, nativeOpen);
assert.strictEqual(fs.closeSync, nativeClose);
}, () => assert.strictEqual(applyInstallPlan(value.declinePlan).applied, true));
assert.strictEqual(fs.openSync, nativeOpen);
assert.strictEqual(fs.closeSync, nativeClose);
assert.strictEqual(require.cache[require.resolve('../../scripts/lib/install/apply')], cachedApply);
}));
for (const relativePath of ['plugins/ecc-hooks.ts', 'opencode.json']) {
for (const mode of ['apply', 'repair']) {
test(`${mode} preserves a same-inode ${relativePath} edit at writable open`, () => fixture(value => {
@@ -366,6 +398,37 @@ function runTests() {
});
}));
}
for (const consent of [null, 'declined', 'enabled']) {
test(`nested JavaScript entrypoint applies with ${consent || 'default'} consent semantics`, () => fixture(value => {
const sourceRelativePath = '.opencode/plugins/custom/index.js';
const sourcePath = path.join(value.sourceRoot, sourceRelativePath);
const destinationPath = path.join(value.targetRoot, 'plugins/custom/index.js');
const content = 'export default async () => ({ event: () => {} });\n';
fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
fs.writeFileSync(sourcePath, content);
const operation = { kind: 'copy-file', moduleId: 'platform-configs', sourceRelativePath,
sourcePath, destinationPath, ownership: 'managed', scaffoldOnly: false };
const selectedModuleIds = consent === 'enabled' ? ['platform-configs', 'hooks-runtime'] : ['platform-configs'];
const plan = withHookConsent({ ...value.basePlan, selectedModuleIds,
operations: [...value.basePlan.operations, operation] }, consent);
assert.strictEqual(applyInstallPlan(plan).applied, true);
assert.strictEqual(fs.readFileSync(destinationPath, 'utf8'), consent === 'enabled'
? content : 'export default async () => ({});\n');
}));
}
test('nested package metadata fails before writing and never receives a JavaScript tombstone', () => fixture(value => {
const destinationPath = path.join(value.targetRoot, 'plugins/custom/package.json');
const sourcePath = path.join(value.sourceRoot, '.opencode/plugins/custom/package.json');
fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
fs.writeFileSync(sourcePath, '{"main":"index.js"}\n');
const before = fs.readFileSync(value.installStatePath);
const operation = { kind: 'copy-file', sourceRelativePath: '.opencode/plugins/custom/package.json',
sourcePath, destinationPath, ownership: 'managed' };
assert.throws(() => applyInstallPlan(withHookConsent({ ...value.basePlan,
operations: [...value.basePlan.operations, operation] }, 'declined')), /unsupported.*package/i);
assert.strictEqual(fs.existsSync(destinationPath), false);
assert.deepStrictEqual(fs.readFileSync(value.installStatePath), before);
}));
for (const rootForm of ['relative', 'missing']) {
for (const consent of [null, 'declined']) {
test(`${rootForm} source root keeps historical refusal for fresh ${consent || 'default'} apply`, () => fixture(value => {
@@ -523,9 +586,9 @@ function runTests() {
const destination = path.join(value.targetRoot, 'plugins', planned ? 'ecc-hooks.ts' : 'index.js');
const content = planned ? fs.readFileSync(destination) : Buffer.from('// unreadable user plugin\n');
if (!planned) fs.writeFileSync(destination, content);
const originalOpen = fs.openSync;
const originalOpen = fileSystem.openSync;
let refusedReads = 0;
fs.openSync = function (candidate, ...args) {
fileSystem.openSync = function (candidate, ...args) {
if (typeof candidate === 'string' && path.resolve(candidate) === destination) {
refusedReads++;
throw Object.assign(new Error('Synthetic plugin read permission denied'), { code: 'EACCES' });
@@ -537,7 +600,7 @@ function runTests() {
else assert.strictEqual(applyInstallPlan(value.declinePlan).applied, true);
assert.ok(refusedReads > 0, 'The permission boundary must be exercised');
} finally {
fs.openSync = originalOpen;
fileSystem.openSync = originalOpen;
}
assert.deepStrictEqual(fs.readFileSync(destination), content);
if (!planned) assert.ok(!readInstallState(value.installStatePath).operations.some(operation => operation.destinationPath === destination));
+4 -4
View File
@@ -118,7 +118,7 @@ function canonicalPlan(homeDir, env) {
console.log('\n=== Testing OpenCode legacy migration ===\n');
test('legacy inspection distinguishes absent, invalid, and unreadable state', () => {
test('legacy inspection distinguishes absent and invalid state without claiming ownership', () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-legacy-inspect-'));
try {
const location = getLegacyOpencodeLocation(homeDir);
@@ -131,9 +131,9 @@ test('legacy inspection distinguishes absent, invalid, and unreadable state', ()
fs.rmSync(location.installStatePath, { recursive: true, force: true });
fs.writeFileSync(location.installStatePath, '{not-json', 'utf8');
const unreadable = inspectLegacyOpencodeState(location);
assert.strictEqual(unreadable.status, 'unreadable');
assert.ok(unreadable.error.includes(location.installStatePath));
const malformed = inspectLegacyOpencodeState(location);
assert.strictEqual(malformed.status, 'invalid');
assert.strictEqual(malformed.error, null);
assert.deepStrictEqual(cleanupLegacyOpencodeInstall(null), {
detected: false,