mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-17 23:28:04 +02:00
Generalize PR #2981 ownership protection to every managed target. Reject mismatched target state and preserve files that appear during writes or failed-install checkpoints. Keep prior hashes for managed files a failed attempt never writes. Integrate PR #2980 preview wording and global dry-run propagation, with PR #2956 fail-closed environment validation and CLI/legacy regression coverage. Fixes #2964. Fixes #2952. Co-authored-by: ilkmajans-cpu <ilkmajans-cpu@users.noreply.github.com> Co-authored-by: wellkilo <wellkilo@foxmail.com>
181 lines
9.3 KiB
JavaScript
181 lines
9.3 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { execFileSync } = require('child_process');
|
|
const { createManifestInstallPlan } = require('../../scripts/lib/install/plan');
|
|
const { applyInstallPlan, previewInstallPlan } = require('../../scripts/lib/install/apply');
|
|
const { listInstallTargetAdapters } = require('../../scripts/lib/install-targets/registry');
|
|
const { uninstallInstalledStates } = require('../../scripts/lib/install-lifecycle');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
function test(name, fn) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-ownership-'));
|
|
try {
|
|
const projectRoot = path.join(root, 'project');
|
|
const homeDir = path.join(root, 'home');
|
|
fs.mkdirSync(projectRoot);
|
|
fs.mkdirSync(homeDir);
|
|
fn({ projectRoot, homeDir, env: {} });
|
|
passed++;
|
|
console.log(` PASS ${name}`);
|
|
} catch (error) {
|
|
failed++;
|
|
console.error(` FAIL ${name}: ${error.stack}`);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function readState(plan) {
|
|
return JSON.parse(fs.readFileSync(plan.installStatePath, 'utf8'));
|
|
}
|
|
|
|
for (const adapter of listInstallTargetAdapters()) {
|
|
test(`${adapter.target}: preserve user files through preview, install, reinstall and uninstall`, context => {
|
|
const nativeTarget = ['codex', 'gemini', 'opencode'].includes(adapter.target);
|
|
const resolved = createManifestInstallPlan({
|
|
...context, target: adapter.target,
|
|
moduleIds: [nativeTarget ? 'platform-configs' : 'rules-core'],
|
|
// This test exercises ownership of source files, not plugin compilation.
|
|
exemptValidationCodes: ['opencode-plugin-not-built'],
|
|
});
|
|
const operation = resolved.operations.find(item => item.kind === 'copy-file');
|
|
assert.ok(operation, 'target must produce a real copy operation');
|
|
const plan = {
|
|
...resolved, operations: [operation],
|
|
statePreview: { ...resolved.statePreview, operations: [operation] },
|
|
};
|
|
const destination = operation.destinationPath;
|
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
fs.writeFileSync(destination, 'User-authored content\n');
|
|
const preview = previewInstallPlan(plan);
|
|
assert.ok(preview.skippedOperations.some(item => item.destinationPath === destination));
|
|
assert.ok(!preview.statePreview.operations.some(item => item.destinationPath === destination));
|
|
assert.ok(!fs.existsSync(plan.installStatePath), 'preview must not create state');
|
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
const installed = applyInstallPlan(plan);
|
|
assert.strictEqual(fs.readFileSync(destination, 'utf8'), 'User-authored content\n');
|
|
assert.ok(installed.warnings.some(warning => warning.includes('Skipped user-owned file')));
|
|
assert.ok(!readState(plan).operations.some(item => item.destinationPath === destination));
|
|
}
|
|
const result = uninstallInstalledStates({ ...context, targets: [adapter.target] });
|
|
assert.strictEqual(result.summary.errorCount, 0);
|
|
assert.strictEqual(fs.readFileSync(destination, 'utf8'), 'User-authored content\n');
|
|
assert.ok(!fs.existsSync(plan.installStatePath));
|
|
});
|
|
}
|
|
|
|
test('Antigravity transforms preserve a conflicting agent and still update managed files', context => {
|
|
const plan = createManifestInstallPlan({ ...context, target: 'antigravity', moduleIds: ['agents-core'] });
|
|
const userOperation = plan.operations.find(item => item.sourceRelativePath === 'agents/architect.md');
|
|
fs.mkdirSync(path.dirname(userOperation.destinationPath), { recursive: true });
|
|
fs.writeFileSync(userOperation.destinationPath, 'My architect\n');
|
|
applyInstallPlan(plan);
|
|
const managed = plan.operations.find(item => item.destinationPath !== userOperation.destinationPath);
|
|
const original = fs.readFileSync(managed.destinationPath, 'utf8');
|
|
fs.writeFileSync(managed.destinationPath, 'old managed version\n');
|
|
applyInstallPlan(plan);
|
|
assert.strictEqual(fs.readFileSync(managed.destinationPath, 'utf8'), original);
|
|
assert.strictEqual(fs.readFileSync(userOperation.destinationPath, 'utf8'), 'My architect\n');
|
|
assert.ok(!readState(plan).operations.some(item => item.destinationPath === userOperation.destinationPath));
|
|
});
|
|
|
|
for (const field of ['id', 'root', 'installStatePath']) {
|
|
test(`rejects previous state with mismatched target ${field}`, context => {
|
|
const plan = createManifestInstallPlan({ ...context, target: 'antigravity', moduleIds: ['rules-core'] });
|
|
applyInstallPlan(plan);
|
|
const operation = plan.operations[0];
|
|
const state = readState(plan);
|
|
const mismatched = { ...state, target: { ...state.target, [field]: `${state.target[field]}-other` } };
|
|
fs.writeFileSync(plan.installStatePath, JSON.stringify(mismatched));
|
|
fs.writeFileSync(operation.destinationPath, 'User file\n');
|
|
assert.throws(() => applyInstallPlan(plan), /install-state target does not match/);
|
|
assert.strictEqual(fs.readFileSync(operation.destinationPath, 'utf8'), 'User file\n');
|
|
assert.deepStrictEqual(readState(plan), mismatched);
|
|
});
|
|
}
|
|
|
|
test('preserves multiple user files created at the write boundary without checkpoint ownership', context => {
|
|
const plan = createManifestInstallPlan({ ...context, target: 'antigravity', moduleIds: ['rules-core'] });
|
|
const collisions = plan.operations.slice(0, 2).map(operation => operation.destinationPath);
|
|
assert.throws(() => applyInstallPlan(plan, {
|
|
beforeOperationWrite({ operation }) {
|
|
if (operation.destinationPath === collisions[0]) {
|
|
for (const collision of collisions) {
|
|
fs.mkdirSync(path.dirname(collision), { recursive: true });
|
|
fs.writeFileSync(collision, 'Concurrent user file\n');
|
|
}
|
|
}
|
|
},
|
|
}), /user-owned file appeared/);
|
|
for (const collision of collisions) {
|
|
assert.strictEqual(fs.readFileSync(collision, 'utf8'), 'Concurrent user file\n');
|
|
assert.ok(!readState(plan).operations.some(item => item.destinationPath === collision));
|
|
}
|
|
uninstallInstalledStates({ ...context, targets: ['antigravity'] });
|
|
for (const collision of collisions) {
|
|
assert.strictEqual(fs.readFileSync(collision, 'utf8'), 'Concurrent user file\n');
|
|
}
|
|
});
|
|
|
|
test('failed reinstall preserves prior hashes for modified managed files it never wrote', context => {
|
|
const plan = createManifestInstallPlan({ ...context, target: 'antigravity', moduleIds: ['rules-core'] });
|
|
applyInstallPlan(plan);
|
|
const modified = plan.operations[1].destinationPath;
|
|
const prior = readState(plan).operations.find(operation => operation.destinationPath === modified);
|
|
fs.writeFileSync(modified, 'My modified managed file\n');
|
|
assert.throws(() => applyInstallPlan(plan, {
|
|
beforeOperationWrite() { throw new Error('injected early failure'); },
|
|
}), /injected early failure/);
|
|
assert.strictEqual(readState(plan).operations.find(operation => operation.destinationPath === modified).contentSha256,
|
|
prior.contentSha256, 'unattempted managed files must keep their prior digest');
|
|
uninstallInstalledStates({ ...context, targets: ['antigravity'] });
|
|
assert.strictEqual(fs.readFileSync(modified, 'utf8'), 'My modified managed file\n');
|
|
});
|
|
|
|
test('partial install checkpoints never claim skipped user files', context => {
|
|
const plan = createManifestInstallPlan({ ...context, target: 'antigravity', moduleIds: ['rules-core'] });
|
|
const userOperation = plan.operations[0];
|
|
fs.mkdirSync(path.dirname(userOperation.destinationPath), { recursive: true });
|
|
fs.writeFileSync(userOperation.destinationPath, 'Keep this file\n');
|
|
assert.throws(() => applyInstallPlan(plan, {
|
|
beforeOperationWrite() { throw new Error('injected write failure'); },
|
|
}), /injected write failure/);
|
|
assert.ok(!readState(plan).operations.some(item => item.destinationPath === userOperation.destinationPath));
|
|
uninstallInstalledStates({ ...context, targets: ['antigravity'] });
|
|
assert.strictEqual(fs.readFileSync(userOperation.destinationPath, 'utf8'), 'Keep this file\n');
|
|
});
|
|
|
|
test('global CLI dry-run preserves installed files, state and canonical database', context => {
|
|
const plan = createManifestInstallPlan({ ...context, target: 'cursor', moduleIds: ['rules-core'] });
|
|
applyInstallPlan(plan);
|
|
const stateBefore = fs.readFileSync(plan.installStatePath);
|
|
const operation = plan.operations[0];
|
|
const fileBefore = fs.readFileSync(operation.destinationPath);
|
|
const env = {
|
|
...process.env, HOME: context.homeDir, USERPROFILE: context.homeDir,
|
|
CODEX_HOME: path.join(context.homeDir, '.codex'),
|
|
XDG_CONFIG_HOME: path.join(context.homeDir, '.config'),
|
|
ECC_DRY_RUN: '0',
|
|
};
|
|
const cli = path.join(__dirname, '../../scripts/ecc.js');
|
|
for (const args of [['--dry-run', 'uninstall'], ['uninstall', '--dry-run']]) {
|
|
const stdout = execFileSync(process.execPath, [cli, ...args, '--target', 'cursor'], {
|
|
cwd: context.projectRoot, env, encoding: 'utf8', timeout: 30000,
|
|
});
|
|
assert.match(stdout, /WOULD UNINSTALL/);
|
|
assert.match(stdout, /Would remove:/);
|
|
assert.doesNotMatch(stdout, /Status: UNINSTALLED|Removed paths:/);
|
|
assert.deepStrictEqual(fs.readFileSync(plan.installStatePath), stateBefore);
|
|
assert.deepStrictEqual(fs.readFileSync(operation.destinationPath), fileBefore);
|
|
assert.deepStrictEqual(fs.readdirSync(context.homeDir), [], 'dry-run must not initialize canonical state');
|
|
}
|
|
});
|
|
|
|
console.log(`Results: Passed: ${passed}, Failed: ${failed}`);
|
|
process.exitCode = failed ? 1 : 0;
|