mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-20 15:01:46 +02:00
fix(opencode): preserve CommonJS runtime scope and user script ownership
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
@@ -1727,6 +1727,7 @@ function repairInstalledStates(options = {}) {
|
||||
}
|
||||
|
||||
const rawPlan = createRepairPlanFromRecord(record, context);
|
||||
require('./install/apply').assertOpenCodeScriptBoundary(rawPlan);
|
||||
const {
|
||||
migration,
|
||||
plan: desiredPlan,
|
||||
@@ -1811,6 +1812,9 @@ function repairInstalledStates(options = {}) {
|
||||
}
|
||||
|
||||
for (const operation of repairOperations) {
|
||||
if (operation.sourceRelativePath === 'manifests/install-assets/commonjs-scripts-package.json') {
|
||||
require('./install/apply').assertOpenCodeScriptBoundary(desiredPlan, new Set(repairedPaths));
|
||||
}
|
||||
if (desiredPlan.target === 'opencode') {
|
||||
const { assertOpenCodeActivationUnchanged } = require('./install/apply');
|
||||
assertOpenCodeActivationUnchanged(desiredPlan, operation, activationSnapshot);
|
||||
@@ -1996,7 +2000,18 @@ function uninstallInstalledStates(options = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
for (const operation of operations) {
|
||||
const { isScriptBoundary, retainScriptBoundary } = require('./install/opencode-script-boundary');
|
||||
// Remove owned script bodies first. Keep their CommonJS scope while any
|
||||
// retained or user-created JavaScript still relies on that boundary.
|
||||
const orderedOperations = record.adapter.target === 'opencode'
|
||||
? [...operations.filter(operation => !isScriptBoundary(operation, record.targetRoot)),
|
||||
...operations.filter(operation => isScriptBoundary(operation, record.targetRoot))]
|
||||
: operations;
|
||||
for (const operation of orderedOperations) {
|
||||
if (record.adapter.target === 'opencode' && retainScriptBoundary(operation, record.targetRoot)) {
|
||||
retainedPaths.push(operation.destinationPath);
|
||||
continue;
|
||||
}
|
||||
const outcome = executeUninstallOperation(operation, record.targetRoot, {
|
||||
preserveDriftedCopies: true,
|
||||
target: record.adapter.target,
|
||||
|
||||
@@ -94,7 +94,7 @@ module.exports = createInstallTargetAdapter({
|
||||
const modules = Array.isArray(input.modules)
|
||||
? input.modules
|
||||
: (input.module ? [input.module] : []);
|
||||
return modules.flatMap(module => (Array.isArray(module.paths) ? module.paths : [])
|
||||
const operations = modules.flatMap(module => (Array.isArray(module.paths) ? module.paths : [])
|
||||
.filter(sourcePath => !isForeignPlatformPath(sourcePath, adapter.target))
|
||||
.flatMap(sourcePath => {
|
||||
const operation = adapter.createScaffoldOperation(module.id, sourcePath, input);
|
||||
@@ -111,6 +111,18 @@ module.exports = createInstallTargetAdapter({
|
||||
contentTransform: 'opencode-home-skills-path',
|
||||
})];
|
||||
}));
|
||||
const scriptOperation = operations.find(operation => (
|
||||
String(operation.sourceRelativePath).replace(/\\/g, '/').startsWith('scripts/')
|
||||
));
|
||||
if (!scriptOperation) return operations;
|
||||
// OpenCode's root is ESM, while ECC runtime scripts remain CommonJS.
|
||||
// Record this boundary so install, repair and uninstall share ownership.
|
||||
return [...operations, createManagedOperation({
|
||||
moduleId: scriptOperation.moduleId,
|
||||
sourceRelativePath: 'manifests/install-assets/commonjs-scripts-package.json',
|
||||
destinationPath: path.join(adapter.resolveRoot(input), 'scripts', 'package.json'),
|
||||
strategy: 'preserve-relative-path',
|
||||
})];
|
||||
},
|
||||
validate: defaultValidateOpencodeHome,
|
||||
});
|
||||
|
||||
@@ -58,6 +58,7 @@ const {
|
||||
} = require('./stale-operations-reconciliation');
|
||||
const { buildInstallIndex, rewriteRelativeLinks } = require('./link-rewrite');
|
||||
const { transformInstallContent } = require('./content-transform');
|
||||
const { assertScriptBoundary } = require('./opencode-script-boundary');
|
||||
|
||||
function isMarkdownPath(filePath) {
|
||||
return /\.(md|mdx|markdown)$/i.test(String(filePath || ''));
|
||||
@@ -295,6 +296,22 @@ function readPreviousInstallState(plan) {
|
||||
return readInstallState(plan.installStatePath);
|
||||
}
|
||||
|
||||
function assertOpenCodeScriptBoundary(plan, writtenDestinations = new Set()) {
|
||||
if (plan.adapter?.target !== 'opencode') return;
|
||||
assertSafeInstallOperation(plan, { destinationPath: plan.installStatePath });
|
||||
assertScriptBoundary(plan, {
|
||||
readFile: destinationPath => readInstalledFileNoFollow(plan, { destinationPath }),
|
||||
previousOperations: readPreviousInstallState(plan)?.operations || [],
|
||||
writtenDestinations,
|
||||
expectedContent(operation) {
|
||||
if (!operation || operation.kind !== 'copy-file') return null;
|
||||
const source = fs.readFileSync(operation.sourcePath);
|
||||
return operation.contentTransform
|
||||
? Buffer.from(transformInstallContent(operation, source.toString('utf8'))) : source;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function comparablePath(filePath) {
|
||||
const resolved = path.resolve(filePath);
|
||||
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
||||
@@ -690,6 +707,7 @@ function preflightQwenAgentOperations(plan) {
|
||||
|
||||
function previewInstallPlan(plan) {
|
||||
preflightQwenAgentOperations(plan);
|
||||
assertOpenCodeScriptBoundary(plan);
|
||||
assertOpenCodeHookDeactivationReady(plan);
|
||||
const migration = prepareStaleOperationsReconciliation(
|
||||
plan,
|
||||
@@ -757,6 +775,7 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
|
||||
if (typeof beforeInstallStateRead === 'function') {
|
||||
beforeInstallStateRead({ plan });
|
||||
}
|
||||
assertOpenCodeScriptBoundary(plan);
|
||||
assertOpenCodeLeaseCoverage(plan, dependencies.opencodeLease);
|
||||
const legacyActivation = inspectLegacyOpenCodeDeactivation(plan);
|
||||
const activationSnapshot = assertOpenCodeHookDeactivationReady(plan);
|
||||
@@ -809,6 +828,9 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
|
||||
beforeOperationWrite({ plan: appliedPlan, operation });
|
||||
}
|
||||
assertNoNewUserOwnedFile(migration, operation, appliedPlan);
|
||||
if (operation.sourceRelativePath === 'manifests/install-assets/commonjs-scripts-package.json') {
|
||||
assertOpenCodeScriptBoundary(appliedPlan, writtenDestinations);
|
||||
}
|
||||
assertOpenCodeActivationUnchanged(appliedPlan, operation, activationSnapshot);
|
||||
|
||||
if (
|
||||
@@ -1059,6 +1081,7 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
|
||||
|
||||
module.exports = {
|
||||
applyInstallPlan,
|
||||
assertOpenCodeScriptBoundary,
|
||||
assertOpenCodeActivationUnchanged,
|
||||
assertOpenCodeLeaseCoverage,
|
||||
assertOpenCodeHookDeactivationReady,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SOURCE = 'manifests/install-assets/commonjs-scripts-package.json';
|
||||
const key = file => process.platform === 'win32' ? path.resolve(file).toLowerCase() : path.resolve(file);
|
||||
const markerPath = root => path.join(root, 'scripts', 'package.json');
|
||||
const isScriptBoundary = (operation, root) => operation.kind === 'copy-file'
|
||||
&& operation.sourceRelativePath === SOURCE && key(operation.destinationPath) === key(markerPath(root));
|
||||
|
||||
// A nested package owns its own module scope. Symlinks and uncertain trees are
|
||||
// preserved conservatively rather than followed while deciding scope changes.
|
||||
function scopedJavaScriptFiles(directory, { nested = false, depth = 0 } = {}) {
|
||||
if (depth > 40) throw new Error('OpenCode scripts tree exceeds the safe inspection depth');
|
||||
const stat = fs.lstatSync(directory, { throwIfNoEntry: false });
|
||||
if (!stat) return [];
|
||||
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`Unsafe OpenCode scripts directory: ${directory}`);
|
||||
const packageStat = fs.lstatSync(path.join(directory, 'package.json'), { throwIfNoEntry: false });
|
||||
if (nested && packageStat?.isFile()) return [];
|
||||
const files = [];
|
||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||
const file = path.join(directory, entry.name);
|
||||
if (entry.isSymbolicLink()) throw new Error(`Unsafe OpenCode scripts symlink: ${file}`);
|
||||
if (entry.isDirectory()) files.push(...scopedJavaScriptFiles(file, { nested: true, depth: depth + 1 }));
|
||||
else if (entry.isFile() && entry.name.toLowerCase().endsWith('.js')) files.push(file);
|
||||
if (files.length > 12000) throw new Error('OpenCode scripts tree exceeds the safe inspection count');
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function assertScriptBoundary(plan, { readFile, previousOperations = [], writtenDestinations = new Set(), expectedContent }) {
|
||||
if (plan.adapter?.target !== 'opencode'
|
||||
|| !plan.operations.some(operation => isScriptBoundary(operation, plan.targetRoot))) return;
|
||||
const marker = markerPath(plan.targetRoot);
|
||||
const content = readFile(marker);
|
||||
if (content !== null) {
|
||||
const metadata = JSON.parse(content.toString('utf8'));
|
||||
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)
|
||||
|| (metadata.type !== undefined && metadata.type !== 'commonjs')) {
|
||||
throw new Error(`Refusing OpenCode CommonJS runtime migration: preserve the existing user module boundary at ${marker}.`);
|
||||
}
|
||||
// Existing user-owned CommonJS metadata is skipped by the ownership guard;
|
||||
// retaining its bytes does not change anyone's module interpretation.
|
||||
return;
|
||||
}
|
||||
const previous = new Map(previousOperations.filter(operation => operation.ownership === 'managed')
|
||||
.map(operation => [key(operation.destinationPath), operation]));
|
||||
const written = new Set([...writtenDestinations].map(key));
|
||||
const planned = new Map(plan.operations.map(operation => [key(operation.destinationPath), operation]));
|
||||
for (const file of scopedJavaScriptFiles(path.dirname(marker))) {
|
||||
const operation = previous.get(key(file));
|
||||
const bytes = readFile(file);
|
||||
const expected = written.has(key(file))
|
||||
? expectedContent(planned.get(key(file))) : null;
|
||||
const digest = bytes === null ? null : crypto.createHash('sha256').update(bytes).digest('hex');
|
||||
const expectedDigest = expected === null
|
||||
? operation?.contentSha256 : crypto.createHash('sha256').update(expected).digest('hex');
|
||||
if (!expectedDigest || digest !== expectedDigest) {
|
||||
throw new Error(`Refusing OpenCode CommonJS runtime migration: unowned or edited JavaScript at ${file} would change module scope. Give user scripts their own package.json scope before retrying.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function retainScriptBoundary(operation, targetRoot) {
|
||||
if (!isScriptBoundary(operation, targetRoot)) return false;
|
||||
try { return scopedJavaScriptFiles(path.dirname(operation.destinationPath)).length > 0; }
|
||||
catch { return true; }
|
||||
}
|
||||
|
||||
module.exports = { assertScriptBoundary, isScriptBoundary, retainScriptBoundary };
|
||||
@@ -0,0 +1,150 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
const { createManifestInstallPlan, applyInstallPlan } = require('../../scripts/lib/install-executor');
|
||||
const { withHookConsent } = require('../../scripts/lib/install/hook-consent');
|
||||
const { repairInstalledStates, uninstallInstalledStates } = require('../../scripts/lib/install-lifecycle');
|
||||
const { readInstallState } = require('../../scripts/lib/install-state');
|
||||
|
||||
const repo = path.resolve(__dirname, '../..');
|
||||
const build = spawnSync(process.execPath, [path.join(repo, 'scripts/build-opencode.js')], { encoding: 'utf8' });
|
||||
assert.strictEqual(build.status, 0, build.stderr || build.stdout);
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
function test(name, fn) {
|
||||
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-opencode-boundary-')));
|
||||
const home = path.join(dir, 'home');
|
||||
const project = path.join(dir, 'project');
|
||||
fs.mkdirSync(home);
|
||||
fs.mkdirSync(project);
|
||||
const root = path.join(home, '.config/opencode');
|
||||
const scripts = path.join(root, 'scripts');
|
||||
const marker = path.join(scripts, 'package.json');
|
||||
const options = { repoRoot: repo, homeDir: home, projectRoot: project, env: {}, targets: ['opencode'] };
|
||||
const plan = () => withHookConsent(createManifestInstallPlan({
|
||||
sourceRoot: repo, homeDir: home, projectRoot: project, env: {}, target: 'opencode',
|
||||
moduleIds: ['platform-configs', 'hooks-runtime'],
|
||||
}), 'enabled');
|
||||
const write = (file, bytes) => { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, bytes); };
|
||||
const probe = file => {
|
||||
const result = spawnSync(process.execPath, ['--input-type=module', '-e',
|
||||
'const m=await import(requirePath); console.log(m.default ?? m.value);'.replace('requirePath', JSON.stringify(require('url').pathToFileURL(file).href))],
|
||||
{ encoding: 'utf8', cwd: project });
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
return result.stdout.trim();
|
||||
};
|
||||
try { fn({ home, project, root, scripts, marker, options, plan, write, probe }); passed++; console.log(`PASS ${name}`); }
|
||||
catch (error) { failed++; console.error(`FAIL ${name}: ${error.stack}`); }
|
||||
finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
||||
}
|
||||
|
||||
test('refuses unowned ESM scripts before copies or install-state writes', f => {
|
||||
f.write(path.join(f.root, 'package.json'), '{"type":"module"}\n');
|
||||
const sentinel = path.join(f.scripts, 'user.js');
|
||||
f.write(sentinel, 'export const value = 42;\n');
|
||||
assert.strictEqual(f.probe(sentinel), '42');
|
||||
const plan = f.plan();
|
||||
assert.throws(() => applyInstallPlan(plan), /unowned or edited JavaScript/);
|
||||
assert.ok(!fs.existsSync(f.marker));
|
||||
assert.ok(!fs.existsSync(plan.installStatePath));
|
||||
assert.ok(!fs.existsSync(path.join(f.root, 'plugins')));
|
||||
assert.strictEqual(f.probe(sentinel), '42');
|
||||
});
|
||||
|
||||
test('preserves an unowned CommonJS boundary without repair or uninstall adoption', f => {
|
||||
const bytes = '{"type":"commonjs","user":"keep"}\n';
|
||||
f.write(f.marker, bytes);
|
||||
const sentinel = path.join(f.scripts, 'user.js');
|
||||
f.write(sentinel, 'module.exports = 43;\n');
|
||||
const plan = f.plan();
|
||||
applyInstallPlan(plan);
|
||||
const owned = () => readInstallState(plan.installStatePath).operations.some(operation => operation.destinationPath === f.marker);
|
||||
assert.strictEqual(owned(), false);
|
||||
assert.strictEqual(repairInstalledStates(f.options).summary.errorCount, 0);
|
||||
assert.strictEqual(owned(), false);
|
||||
assert.strictEqual(fs.readFileSync(f.marker, 'utf8'), bytes);
|
||||
assert.strictEqual(f.probe(sentinel), '43');
|
||||
assert.strictEqual(uninstallInstalledStates(f.options).summary.errorCount, 0);
|
||||
assert.strictEqual(fs.readFileSync(f.marker, 'utf8'), bytes);
|
||||
assert.strictEqual(f.probe(sentinel), '43');
|
||||
});
|
||||
|
||||
test('refuses an existing user ESM package boundary without changing it', f => {
|
||||
const bytes = '{"type":"module","user":"keep"}\n';
|
||||
f.write(f.marker, bytes);
|
||||
const sentinel = path.join(f.scripts, 'user.js');
|
||||
f.write(sentinel, 'export const value = 44;\n');
|
||||
const plan = f.plan();
|
||||
assert.throws(() => applyInstallPlan(plan), /existing user module boundary/);
|
||||
assert.strictEqual(fs.readFileSync(f.marker, 'utf8'), bytes);
|
||||
assert.ok(!fs.existsSync(plan.installStatePath));
|
||||
assert.strictEqual(f.probe(sentinel), '44');
|
||||
});
|
||||
|
||||
test('a nested user ESM scope remains unchanged across install and uninstall', f => {
|
||||
f.write(path.join(f.scripts, 'user/package.json'), '{"type":"module"}\n');
|
||||
const sentinel = path.join(f.scripts, 'user/index.js');
|
||||
f.write(sentinel, 'export const value = 45;\n');
|
||||
const explicitModule = path.join(f.scripts, 'user-module.mjs');
|
||||
f.write(explicitModule, 'export const value = 48;\n');
|
||||
applyInstallPlan(f.plan());
|
||||
assert.strictEqual(f.probe(sentinel), '45');
|
||||
assert.strictEqual(f.probe(explicitModule), '48');
|
||||
assert.strictEqual(uninstallInstalledStates(f.options).summary.errorCount, 0);
|
||||
assert.ok(!fs.existsSync(f.marker));
|
||||
assert.strictEqual(f.probe(sentinel), '45');
|
||||
assert.strictEqual(f.probe(explicitModule), '48');
|
||||
});
|
||||
|
||||
test('partial uninstall retains an owned boundary while user JavaScript relies on it', f => {
|
||||
const plan = f.plan();
|
||||
applyInstallPlan(plan);
|
||||
const sentinel = path.join(f.scripts, 'user.js');
|
||||
f.write(sentinel, 'module.exports = 46;\n');
|
||||
const result = uninstallInstalledStates(f.options);
|
||||
assert.strictEqual(result.results[0].status, 'partial');
|
||||
assert.ok(result.results[0].retainedPaths.includes(f.marker));
|
||||
assert.strictEqual(f.probe(sentinel), '46');
|
||||
assert.ok(readInstallState(plan.installStatePath).operations.some(operation => operation.destinationPath === f.marker));
|
||||
fs.unlinkSync(sentinel);
|
||||
assert.strictEqual(uninstallInstalledStates(f.options).summary.errorCount, 0);
|
||||
assert.ok(!fs.existsSync(f.marker));
|
||||
assert.ok(!fs.existsSync(plan.installStatePath));
|
||||
});
|
||||
|
||||
test('rechecks newly introduced user JavaScript before boundary publication', f => {
|
||||
const sentinel = path.join(f.scripts, 'user.js');
|
||||
const plan = f.plan();
|
||||
assert.throws(() => applyInstallPlan(plan, { beforeOperationWrite({ operation }) {
|
||||
if (operation.destinationPath === f.marker) f.write(sentinel, 'export const value = 47;\n');
|
||||
} }), /unowned or edited JavaScript/);
|
||||
assert.ok(!fs.existsSync(f.marker));
|
||||
assert.strictEqual(f.probe(sentinel), '47');
|
||||
});
|
||||
|
||||
test('refuses a concurrent edit to a just-installed script before boundary publication', f => {
|
||||
const plan = f.plan();
|
||||
const sentinel = path.join(f.scripts, 'hooks/skill-run-tracker.js');
|
||||
assert.throws(() => applyInstallPlan(plan, { beforeOperationWrite({ operation }) {
|
||||
if (operation.destinationPath === f.marker) f.write(sentinel, 'export const value = 49;\n');
|
||||
} }), /unowned or edited JavaScript/);
|
||||
assert.ok(!fs.existsSync(f.marker));
|
||||
assert.strictEqual(f.probe(sentinel), '49');
|
||||
});
|
||||
|
||||
test('repair restores a missing owned boundary and preserves runtime script ownership', f => {
|
||||
const plan = f.plan();
|
||||
applyInstallPlan(plan);
|
||||
fs.unlinkSync(f.marker);
|
||||
assert.strictEqual(repairInstalledStates(f.options).summary.errorCount, 0);
|
||||
assert.strictEqual(JSON.parse(fs.readFileSync(f.marker, 'utf8')).type, 'commonjs');
|
||||
assert.ok(readInstallState(plan.installStatePath).operations.some(operation => operation.destinationPath === f.marker));
|
||||
});
|
||||
|
||||
console.log(`Passed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
process.exitCode = failed ? 1 : 0;
|
||||
@@ -111,8 +111,9 @@ function createModularInstallRoot(options = {}) {
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(root, "package.json"),
|
||||
JSON.stringify({ name: "ecc-universal", version: "0.0.0-test" })
|
||||
JSON.stringify({ name: "ecc-universal", version: "0.0.0-test", type: "module" })
|
||||
)
|
||||
fs.writeFileSync(path.join(root, "scripts", "package.json"), JSON.stringify({ type: "commonjs" }))
|
||||
|
||||
// The plugin's tools import @opencode-ai/plugin, which resolves through
|
||||
// node_modules — a real installation has it, an isolated temp dir would not.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
const { pathToFileURL } = require('url');
|
||||
const { createManifestInstallPlan, applyInstallPlan } = require('../../scripts/lib/install-executor');
|
||||
const { withHookConsent } = require('../../scripts/lib/install/hook-consent');
|
||||
|
||||
const repo = path.resolve(__dirname, '../..');
|
||||
const fixture = fs.mkdtempSync(path.join(repo, 'tests', '.opencode-modular-runtime-'));
|
||||
const home = path.join(fixture, 'home');
|
||||
const project = path.join(fixture, 'project');
|
||||
fs.mkdirSync(home);
|
||||
fs.mkdirSync(project);
|
||||
let failed = 0;
|
||||
try {
|
||||
const build = spawnSync(process.execPath, [path.join(repo, 'scripts/build-opencode.js')], { encoding: 'utf8' });
|
||||
assert.strictEqual(build.status, 0, build.stderr || build.stdout);
|
||||
const plan = withHookConsent(createManifestInstallPlan({
|
||||
sourceRoot: repo, homeDir: home, projectRoot: project, env: {},
|
||||
target: 'opencode', moduleIds: ['platform-configs', 'hooks-runtime'],
|
||||
}), 'enabled');
|
||||
applyInstallPlan(plan);
|
||||
assert.ok(!fs.existsSync(path.join(plan.targetRoot, 'skills')), 'Optional skills must stay absent');
|
||||
const marker = path.join(plan.targetRoot, 'scripts/package.json');
|
||||
assert.strictEqual(JSON.parse(fs.readFileSync(marker, 'utf8')).type, 'commonjs');
|
||||
assert.ok(plan.statePreview.operations.some(operation => operation.destinationPath === marker));
|
||||
const input = `
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
const root = process.argv[1];
|
||||
const logs = [];
|
||||
const context = { client: { app: { log: ({ body }) => { logs.push(body); return Promise.resolve(); } } },
|
||||
$: () => { throw new Error('Offline fixture must not execute shell commands'); },
|
||||
directory: process.cwd(), worktree: process.cwd() };
|
||||
const loaded = [];
|
||||
for (const name of fs.readdirSync(path.join(root, 'plugins')).filter(name => /\\.ts$/.test(name))) {
|
||||
const mod = await import(pathToFileURL(path.join(root, 'plugins', name)).href);
|
||||
for (const entry of new Set(Object.values(mod))) loaded.push(await entry(context));
|
||||
}
|
||||
const live = loaded.filter(hooks => typeof hooks['tool.execute.after'] === 'function');
|
||||
assert.equal(live.length, 1, 'Local discovery must register exactly one live ECC plugin');
|
||||
await live[0]['tool.execute.after']({ tool: 'skill', args: { name: 'code-review', arguments: 'private prompt sentinel' } }, {});
|
||||
assert.ok(!logs.some(entry => /telemetry disabled/.test(entry.message)));
|
||||
const runs = fs.readFileSync(path.join(process.env.ECC_AGENT_DATA_HOME, 'state/skill-runs.jsonl'), 'utf8');
|
||||
assert.equal(runs.trim().split('\\n').length, 1);
|
||||
assert.equal(JSON.parse(runs).skill_id, 'code-review');
|
||||
assert.ok(!runs.includes('private prompt sentinel'));
|
||||
const output = { env: {} };
|
||||
await live[0]['shell.env']({ cwd: process.cwd() }, output);
|
||||
assert.equal(output.env.CLAUDE_PLUGIN_ROOT, fs.realpathSync(root));
|
||||
console.log('PASS: one live installed plugin, one private skill record, canonical root export');
|
||||
`;
|
||||
const result = spawnSync(process.execPath, ['--loader',
|
||||
pathToFileURL(path.join(repo, 'tests/fixtures/opencode-ts-loader.mjs')).href,
|
||||
'--input-type=module', '-e', input, plan.targetRoot], {
|
||||
cwd: project, encoding: 'utf8', timeout: 30000,
|
||||
env: { ...process.env, HOME: home, USERPROFILE: home,
|
||||
CLAUDE_CONFIG_DIR: path.join(home, '.claude'), ECC_AGENT_DATA_HOME: path.join(home, '.claude'),
|
||||
XDG_CONFIG_HOME: path.join(home, '.config'), OPENCODE_CONFIG_DIR: plan.targetRoot,
|
||||
CLAUDE_PLUGIN_ROOT: '', CLAUDE_CODE_PACKAGE_MANAGER: 'bun', ECC_HOOK_PROFILE: 'standard', ECC_DISABLED_HOOKS: '' },
|
||||
});
|
||||
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
||||
console.log(result.stdout.trim());
|
||||
const standalone = spawnSync(process.execPath,
|
||||
[path.join(plan.targetRoot, 'scripts/hooks/post-bash-command-log.js'), 'audit'], {
|
||||
cwd: project, encoding: 'utf8', input: JSON.stringify({ tool_name: 'Bash', tool_input: { command: 'echo standalone-offline' } }),
|
||||
env: { ...process.env, HOME: home, USERPROFILE: home, CLAUDE_CONFIG_DIR: path.join(home, '.claude') },
|
||||
});
|
||||
assert.strictEqual(standalone.status, 0, standalone.stderr);
|
||||
assert.match(fs.readFileSync(path.join(home, '.claude/bash-commands.log'), 'utf8'), /standalone-offline/);
|
||||
} catch (error) {
|
||||
failed = 1;
|
||||
console.error(error.stack);
|
||||
} finally {
|
||||
fs.rmSync(fixture, { recursive: true, force: true });
|
||||
}
|
||||
console.log(`Passed: ${1 - failed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
process.exitCode = failed;
|
||||
Reference in New Issue
Block a user