mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-20 16:47:59 +02:00
372 lines
16 KiB
JavaScript
372 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { test } = require('node:test');
|
|
|
|
const { createInstallPlanFromRequest } = require('../../scripts/lib/install/runtime');
|
|
const { applyInstallPlan, previewInstallPlan } = require('../../scripts/lib/install/apply');
|
|
const {
|
|
buildDoctorReport, repairInstalledStates, uninstallInstalledStates,
|
|
} = require('../../scripts/lib/install-lifecycle');
|
|
const { readInstallState, writeInstallState } = require('../../scripts/lib/install-state');
|
|
|
|
const SHARED_FILES = ['config.toml', 'AGENTS.md'];
|
|
const TEMPLATES = {
|
|
'config.toml': '# ECC defaults\nmodel = "example-model"\n',
|
|
'AGENTS.md': '# ECC instructions\n\nFollow the project conventions.\n',
|
|
};
|
|
|
|
function writeFile(filePath, content) {
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
fs.writeFileSync(filePath, content);
|
|
}
|
|
|
|
function createFixture(t) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-codex-preservation-'));
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const sourceRoot = path.join(root, 'source');
|
|
const homeDir = path.join(root, 'home');
|
|
const projectRoot = path.join(root, 'project');
|
|
const json = (relativePath, value) => writeFile(
|
|
path.join(sourceRoot, relativePath), `${JSON.stringify(value, null, 2)}\n`
|
|
);
|
|
json('package.json', { version: '1.0.0' });
|
|
json('manifests/install-modules.json', {
|
|
version: 1,
|
|
modules: [{
|
|
id: 'platform-configs', kind: 'platform', description: 'Codex configuration fixture',
|
|
paths: ['.codex'], targets: ['codex'], dependencies: [],
|
|
defaultInstall: true, cost: 'light', stability: 'stable',
|
|
}, {
|
|
id: 'helper-scripts', kind: 'platform', description: 'Independent helper fixture',
|
|
paths: ['scripts'], targets: ['codex'], dependencies: [],
|
|
defaultInstall: false, cost: 'light', stability: 'stable',
|
|
}],
|
|
});
|
|
json('manifests/install-profiles.json', {
|
|
version: 1, profiles: { minimal: { description: 'Fixture', modules: ['platform-configs'] } },
|
|
});
|
|
for (const name of SHARED_FILES) writeFile(path.join(sourceRoot, '.codex', name), TEMPLATES[name]);
|
|
writeFile(path.join(sourceRoot, 'scripts', 'independent-helper.js'), 'module.exports = "helper";\n');
|
|
fs.mkdirSync(homeDir, { recursive: true });
|
|
fs.mkdirSync(projectRoot, { recursive: true });
|
|
const options = { sourceRoot, homeDir, projectRoot, env: {} };
|
|
const lifecycleOptions = { repoRoot: sourceRoot, homeDir, projectRoot, targets: ['codex'], env: {} };
|
|
const plan = (moduleIds = ['platform-configs']) => createInstallPlanFromRequest({
|
|
mode: 'manifest', target: 'codex', profileId: null, moduleIds,
|
|
includeComponentIds: [], excludeComponentIds: [], hookConsent: 'declined',
|
|
}, options);
|
|
return {
|
|
sourceRoot,
|
|
destination: name => path.join(homeDir, '.codex', name),
|
|
statePath: path.join(homeDir, '.codex', 'ecc-install-state.json'),
|
|
plan,
|
|
install: moduleIds => applyInstallPlan(plan(moduleIds)),
|
|
repair: (dryRun = false) => repairInstalledStates({ ...lifecycleOptions, dryRun }),
|
|
doctor: () => buildDoctorReport(lifecycleOptions),
|
|
uninstall: () => uninstallInstalledStates(lifecycleOptions),
|
|
};
|
|
}
|
|
|
|
function lifecycleResult(report) {
|
|
assert.equal(report.results.length, 1);
|
|
assert.notEqual(report.results[0].status, 'error', report.results[0].error);
|
|
return report.results[0];
|
|
}
|
|
|
|
function assertPreserved(fixture, name, content) {
|
|
assert.deepEqual(fs.readFileSync(fixture.destination(name)), Buffer.from(content));
|
|
}
|
|
|
|
function assertUnmanaged(fixture, name) {
|
|
assert.ok(!readInstallState(fixture.statePath).operations.some(operation => (
|
|
operation.destinationPath === fixture.destination(name) && operation.ownership === 'managed'
|
|
)), `${name} must not remain managed after preserving user content`);
|
|
}
|
|
|
|
function assertWarning(result, name) {
|
|
assert.ok((result.warnings || []).some(warning => (
|
|
warning.includes(name) && /skip|preserv|user-owned|modif/i.test(warning)
|
|
)), `Expected an explicit preservation warning for ${name}`);
|
|
}
|
|
|
|
function editAfterRepairInspection(fixture, name, content, action) {
|
|
const originalOpen = fs.openSync;
|
|
const originalClose = fs.closeSync;
|
|
const inspectedDescriptors = new Set();
|
|
let injected = false;
|
|
fs.openSync = function (filePath, ...args) {
|
|
const descriptor = originalOpen.call(fs, filePath, ...args);
|
|
if (!injected && filePath === fixture.destination(name)
|
|
&& new Error().stack.includes('inspectManagedOperation')) {
|
|
inspectedDescriptors.add(descriptor);
|
|
}
|
|
return descriptor;
|
|
};
|
|
fs.closeSync = function (descriptor) {
|
|
const result = originalClose.call(fs, descriptor);
|
|
if (!injected && inspectedDescriptors.delete(descriptor)) {
|
|
// Inspection has read the previous bytes. Simulate an editor saving next,
|
|
// before repair checkpoints or refreshes state; no digest-refresh hook is used.
|
|
injected = true;
|
|
writeFile(fixture.destination(name), content);
|
|
}
|
|
return result;
|
|
};
|
|
try {
|
|
return { result: action(), injected };
|
|
} finally {
|
|
fs.openSync = originalOpen;
|
|
fs.closeSync = originalClose;
|
|
}
|
|
}
|
|
|
|
for (const name of SHARED_FILES) {
|
|
for (const stage of ['bridge', 'no-op refresh']) {
|
|
test(`repair ${stage} does not claim a concurrent edit to Codex ${name}`, t => {
|
|
const fixture = createFixture(t);
|
|
fixture.install();
|
|
const previousOperation = readInstallState(fixture.statePath).operations.find(operation => (
|
|
operation.destinationPath === fixture.destination(name)
|
|
));
|
|
if (stage === 'bridge') {
|
|
writeFile(path.join(fixture.sourceRoot, '.codex', name),
|
|
`${TEMPLATES[name]}\n# Updated upstream template\n`);
|
|
}
|
|
const content = `${TEMPLATES[name]}\r\n# Saved after repair inspected the file\r\n`;
|
|
|
|
const { result: report, injected } = editAfterRepairInspection(
|
|
fixture, name, content, () => fixture.repair()
|
|
);
|
|
|
|
assert.ok(injected, 'The simulated edit must occur after repair inspection');
|
|
assert.equal(report.results.length, 1);
|
|
if (stage === 'bridge') {
|
|
assert.equal(report.results[0].status, 'error');
|
|
assert.match(report.results[0].error, /Refusing.*user configuration.*changed after planning/);
|
|
} else {
|
|
assert.equal(report.results[0].status, 'ok');
|
|
}
|
|
assertPreserved(fixture, name, content);
|
|
const refreshedOperation = readInstallState(fixture.statePath).operations.find(operation => (
|
|
operation.destinationPath === fixture.destination(name) && operation.ownership === 'managed'
|
|
));
|
|
assert.ok(refreshedOperation,
|
|
'Repair must retain the previous ledger entry for configuration it did not write');
|
|
assert.equal(refreshedOperation.contentSha256, previousOperation.contentSha256,
|
|
'Repair must retain the previous digest for configuration it did not write');
|
|
lifecycleResult(fixture.uninstall());
|
|
assertPreserved(fixture, name, content);
|
|
});
|
|
}
|
|
|
|
test(`selective reinstall releases edited Codex ${name} retained from an earlier module`, t => {
|
|
const fixture = createFixture(t);
|
|
fixture.install();
|
|
const content = `${TEMPLATES[name]}\n# Keep this across unrelated module installations\n`;
|
|
writeFile(fixture.destination(name), content);
|
|
const selectivePlan = fixture.plan(['helper-scripts']);
|
|
assert.ok(!selectivePlan.operations.some(operation => (
|
|
operation.destinationPath === fixture.destination(name)
|
|
)), 'The edited configuration must not be in the selected module operations');
|
|
|
|
const result = applyInstallPlan(selectivePlan);
|
|
|
|
assertPreserved(fixture, name, content);
|
|
assertUnmanaged(fixture, name);
|
|
assertWarning(result, name);
|
|
lifecycleResult(fixture.repair());
|
|
assertPreserved(fixture, name, content);
|
|
assertUnmanaged(fixture, name);
|
|
lifecycleResult(fixture.uninstall());
|
|
assertPreserved(fixture, name, content);
|
|
});
|
|
|
|
test(`reinstall rejects a last-minute edit to Codex ${name} without claiming the edited bytes`, t => {
|
|
const fixture = createFixture(t);
|
|
fixture.install();
|
|
const destination = fixture.destination(name);
|
|
const previousOperation = readInstallState(fixture.statePath).operations.find(operation => (
|
|
operation.destinationPath === destination
|
|
));
|
|
const rawPlan = fixture.plan();
|
|
// Write the other file first to exercise the partial-install checkpoint on failure.
|
|
const plan = {
|
|
...rawPlan,
|
|
operations: [
|
|
...rawPlan.operations.filter(operation => operation.destinationPath !== destination),
|
|
...rawPlan.operations.filter(operation => operation.destinationPath === destination),
|
|
],
|
|
};
|
|
const content = `${TEMPLATES[name]}\r\n# Saved while ECC was running\r\n`;
|
|
let wroteAnotherFile = false;
|
|
let injectedEdit = false;
|
|
|
|
assert.throws(() => applyInstallPlan(plan, {
|
|
beforeOperationWrite({ operation }) {
|
|
if (operation.destinationPath !== destination) {
|
|
wroteAnotherFile = true;
|
|
return;
|
|
}
|
|
assert.ok(wroteAnotherFile, 'The failure must exercise a partial install');
|
|
writeFile(destination, content);
|
|
injectedEdit = true;
|
|
},
|
|
}), /Refusing.*user configuration.*changed after planning/);
|
|
|
|
assert.ok(injectedEdit);
|
|
assertPreserved(fixture, name, content);
|
|
const checkpointOperation = readInstallState(fixture.statePath).operations.find(operation => (
|
|
operation.destinationPath === destination && operation.ownership === 'managed'
|
|
));
|
|
assert.ok(checkpointOperation,
|
|
'A failure checkpoint must retain the previous ledger entry');
|
|
assert.equal(checkpointOperation.contentSha256, previousOperation.contentSha256,
|
|
'A failure checkpoint must retain the old digest, never adopt the user edit');
|
|
lifecycleResult(fixture.uninstall());
|
|
assertPreserved(fixture, name, content);
|
|
});
|
|
|
|
test(`repeated repair keeps edited Codex ${name} unmanaged while repairing an ECC script`, t => {
|
|
const fixture = createFixture(t);
|
|
const scriptName = path.join('scripts', 'ecc-helper.js');
|
|
const scriptContent = 'module.exports = "ECC helper";\n';
|
|
writeFile(path.join(fixture.sourceRoot, '.codex', scriptName), scriptContent);
|
|
fixture.install();
|
|
const content = `${TEMPLATES[name]}\n# Keep my preferences\n`;
|
|
writeFile(fixture.destination(name), content);
|
|
|
|
lifecycleResult(fixture.repair());
|
|
assertPreserved(fixture, name, content);
|
|
assertUnmanaged(fixture, name);
|
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
const before = lifecycleResult(fixture.doctor());
|
|
assert.ok(!before.issues.some(issue => issue.code === 'drifted-managed-files'));
|
|
writeFile(fixture.destination(scriptName), 'damaged ECC helper\n');
|
|
const damaged = lifecycleResult(fixture.doctor());
|
|
assert.ok(damaged.issues.some(issue => issue.code === 'drifted-managed-files'));
|
|
|
|
const result = lifecycleResult(fixture.repair());
|
|
|
|
assert.ok(result.repairedPaths.includes(fixture.destination(scriptName)));
|
|
assertPreserved(fixture, scriptName, scriptContent);
|
|
assertPreserved(fixture, name, content);
|
|
assertUnmanaged(fixture, name);
|
|
const after = lifecycleResult(fixture.doctor());
|
|
assert.ok(!after.issues.some(issue => issue.code === 'drifted-managed-files'));
|
|
}
|
|
lifecycleResult(fixture.uninstall());
|
|
assertPreserved(fixture, name, content);
|
|
assert.ok(!fs.existsSync(fixture.destination(scriptName)));
|
|
});
|
|
|
|
for (const action of ['reinstall', 'repair']) {
|
|
test(`${action} preserves edited Codex ${name} and leaves it safe to uninstall`, t => {
|
|
const fixture = createFixture(t);
|
|
fixture.install();
|
|
const content = `${TEMPLATES[name]}\r\n# Personal preferences — 保留\r\n`;
|
|
writeFile(fixture.destination(name), content);
|
|
|
|
const result = action === 'reinstall' ? fixture.install() : lifecycleResult(fixture.repair());
|
|
|
|
assertPreserved(fixture, name, content);
|
|
assertWarning(result, name);
|
|
assertUnmanaged(fixture, name);
|
|
lifecycleResult(fixture.uninstall());
|
|
assertPreserved(fixture, name, content);
|
|
});
|
|
}
|
|
|
|
test(`dry runs warn about edited Codex ${name} without changing files or state`, t => {
|
|
const fixture = createFixture(t);
|
|
fixture.install();
|
|
const content = `${TEMPLATES[name]}\n# User customization\n`;
|
|
writeFile(fixture.destination(name), content);
|
|
const previousState = fs.readFileSync(fixture.statePath);
|
|
|
|
const preview = previewInstallPlan(fixture.plan());
|
|
const repairPreview = lifecycleResult(fixture.repair(true));
|
|
|
|
assertPreserved(fixture, name, content);
|
|
assert.deepEqual(fs.readFileSync(fixture.statePath), previousState);
|
|
assertWarning(preview, name);
|
|
assertWarning(repairPreview, name);
|
|
assert.ok(!preview.operations.some(operation => operation.destinationPath === fixture.destination(name)));
|
|
assert.ok(!repairPreview.plannedRepairs.includes(fixture.destination(name)));
|
|
});
|
|
|
|
test(`pre-existing Codex ${name} survives install, repair and uninstall`, t => {
|
|
const fixture = createFixture(t);
|
|
const content = '# Personal file before ECC installation\r\n保持原样\r\n';
|
|
writeFile(fixture.destination(name), content);
|
|
|
|
assertWarning(fixture.install(), name);
|
|
assertPreserved(fixture, name, content);
|
|
assertUnmanaged(fixture, name);
|
|
const repairResult = lifecycleResult(fixture.repair());
|
|
assertPreserved(fixture, name, content);
|
|
assertWarning(repairResult, name);
|
|
assertUnmanaged(fixture, name);
|
|
lifecycleResult(fixture.uninstall());
|
|
assertPreserved(fixture, name, content);
|
|
});
|
|
|
|
for (const action of ['reinstall', 'repair']) {
|
|
test(`${action} preserves Codex ${name} when legacy state lacks its content digest`, t => {
|
|
const fixture = createFixture(t);
|
|
fixture.install();
|
|
const state = readInstallState(fixture.statePath);
|
|
writeInstallState(fixture.statePath, {
|
|
...state,
|
|
operations: state.operations.map(operation => {
|
|
if (operation.destinationPath !== fixture.destination(name)) return operation;
|
|
const { contentSha256: _contentSha256, ...legacyOperation } = operation;
|
|
return legacyOperation;
|
|
}),
|
|
});
|
|
// Even bytes equal to today's template cannot prove ownership without a recorded digest.
|
|
const content = fs.readFileSync(fixture.destination(name));
|
|
const result = action === 'reinstall' ? fixture.install() : lifecycleResult(fixture.repair());
|
|
|
|
assertPreserved(fixture, name, content);
|
|
assertWarning(result, name);
|
|
assertUnmanaged(fixture, name);
|
|
lifecycleResult(fixture.uninstall());
|
|
assertPreserved(fixture, name, content);
|
|
});
|
|
}
|
|
|
|
for (const action of ['reinstall', 'repair']) {
|
|
test(`${action} updates unedited Codex ${name} when the template changes`, t => {
|
|
const fixture = createFixture(t);
|
|
fixture.install();
|
|
const updated = `${TEMPLATES[name]}\n# New upstream default\n`;
|
|
writeFile(path.join(fixture.sourceRoot, '.codex', name), updated);
|
|
|
|
if (action === 'reinstall') fixture.install();
|
|
else lifecycleResult(fixture.repair());
|
|
|
|
assertPreserved(fixture, name, updated);
|
|
assert.ok(readInstallState(fixture.statePath).operations.some(operation => (
|
|
operation.destinationPath === fixture.destination(name) && operation.ownership === 'managed'
|
|
)));
|
|
lifecycleResult(fixture.uninstall());
|
|
assert.ok(!fs.existsSync(fixture.destination(name)));
|
|
});
|
|
}
|
|
|
|
test(`repair restores missing managed Codex ${name}`, t => {
|
|
const fixture = createFixture(t);
|
|
fixture.install();
|
|
const installedContent = fs.readFileSync(fixture.destination(name));
|
|
fs.unlinkSync(fixture.destination(name));
|
|
|
|
lifecycleResult(fixture.repair());
|
|
|
|
assertPreserved(fixture, name, installedContent);
|
|
});
|
|
}
|