mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-28 12:35:13 +02:00
360 lines
21 KiB
JavaScript
360 lines
21 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 { withFixture } = require('./helpers/context-fixture');
|
|
const store = require('../../scripts/lib/context-profile-store');
|
|
const native = () => require('../../scripts/lib/context-profile-native');
|
|
|
|
function fixture(callback) {
|
|
return withFixture(repoRoot => {
|
|
const parent = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'ecc-native-test-'));
|
|
const options = { stateRoot: path.join(parent, 'managed'), nativeRoot: path.join(parent, 'native'), codexPath: process.execPath };
|
|
try {
|
|
store.applyStore({ repoRoot, stateRoot: options.stateRoot, target: 'codex' });
|
|
return callback(options, repoRoot, parent);
|
|
} finally { fs.rmSync(parent, { recursive: true, force: true }); }
|
|
});
|
|
}
|
|
|
|
function provider(overrides = {}) {
|
|
const calls = [];
|
|
const execute = (command, args, options) => {
|
|
calls.push({ command, args, options });
|
|
assert.equal(options.killSignal, 'SIGKILL');
|
|
assert.equal(options.env.OPENAI_API_KEY, undefined);
|
|
assert.equal(options.env.ANTHROPIC_API_KEY, undefined);
|
|
for (const key of ['NODE_OPTIONS', 'CODEX_CONFIG', 'HTTP_PROXY', 'AWS_ACCESS_KEY_ID']) assert.equal(options.env[key], undefined);
|
|
if (args[0] === '--version') return { status: 0, stdout: overrides.version || 'codex-cli 0.154.0\n' };
|
|
if (overrides.failInstall && args[1] === 'add') return { status: 1, stderr: 'provider-specific detail' };
|
|
if (args[0] === 'plugin' && args[1] === 'add') {
|
|
const base = path.dirname(options.env.HOME);
|
|
const marketplace = JSON.parse(fs.readFileSync(path.join(base, 'marketplace/.agents/plugins/marketplace.json')));
|
|
const cache = path.join(options.env.CODEX_HOME, 'plugins/cache', marketplace.name, 'ecc-context-carrier/local');
|
|
fs.mkdirSync(path.dirname(cache), { recursive: true });
|
|
fs.cpSync(path.join(base, 'marketplace/carrier'), cache, { recursive: true });
|
|
}
|
|
return { status: 0, stdout: '{}' };
|
|
};
|
|
const discover = (_command, options) => {
|
|
const base = path.dirname(options.env.HOME);
|
|
const marketplace = JSON.parse(fs.readFileSync(path.join(base, 'marketplace/.agents/plugins/marketplace.json')));
|
|
const cache = path.join(options.env.CODEX_HOME, 'plugins/cache', marketplace.name, 'ecc-context-carrier/local');
|
|
const skills = fs.readdirSync(path.join(cache, 'skills')).map(name => ({ name: `ecc-context-carrier:${name}`,
|
|
pluginId: `ecc-context-carrier@${marketplace.name}`, enabled: true, scope: 'user',
|
|
path: path.join(cache, 'skills', name, 'SKILL.md') }));
|
|
if (overrides.alter) overrides.alter({ skills, cache });
|
|
return { data: [{ cwd: options.cwd, errors: [], skills }] };
|
|
};
|
|
return { execute, discover, calls, ...overrides };
|
|
}
|
|
|
|
test('native preview is deterministic and never invokes the provider or creates a home', () => fixture(options => {
|
|
const first = native().previewNativeProfile(options);
|
|
assert.deepEqual(first, native().previewNativeProfile(options));
|
|
assert.equal(first.active, false);
|
|
assert.equal(first.status, 'proposed');
|
|
assert.equal(fs.existsSync(options.nativeRoot), false);
|
|
}));
|
|
|
|
test('native prepare verifies exact installed bytes and returns isolated session paths', () => fixture(options => {
|
|
const dependency = provider();
|
|
const result = native().prepareNativeProfile(options, dependency);
|
|
assert.equal(result.status, 'ready');
|
|
assert.equal(result.active, false);
|
|
assert.equal(result.storeRevision, 1);
|
|
assert.equal(result.providerVersion, '0.154.0');
|
|
assert.equal(path.dirname(path.dirname(result.home)), path.join(options.nativeRoot, 'generations'));
|
|
assert.equal(path.dirname(result.codexHome), result.home);
|
|
assert.equal(result.discovery, 'verified');
|
|
assert.equal(result.selectedIds.length, 3);
|
|
assert.equal(dependency.calls.filter(call => call.args[1] === 'add').length, 1);
|
|
assert.equal(native().getNativeProfileStatus(options, dependency).status, 'ready');
|
|
}));
|
|
|
|
test('native Full Lean rollback follows managed authority and preserves unrelated bytes', () => fixture((options, repoRoot) => {
|
|
const dependency = provider();
|
|
store.applyStore({ repoRoot, stateRoot: options.stateRoot, target: 'codex', profileId: 'full@1' });
|
|
const full = native().prepareNativeProfile(options, dependency);
|
|
const sentinel = path.join(full.home, 'unrelated.txt');
|
|
fs.writeFileSync(sentinel, 'user owned');
|
|
store.applyStore({ repoRoot, stateRoot: options.stateRoot, target: 'codex', profileId: 'lean@1' });
|
|
assert.equal(native().getNativeProfileStatus(options, dependency).status, 'stale');
|
|
const lean = native().prepareNativeProfile(options, dependency);
|
|
assert.notEqual(lean.home, full.home);
|
|
assert.throws(() => native().rollbackNativeProfile(options, dependency), /managed|store/i);
|
|
store.rollbackStore({ stateRoot: options.stateRoot });
|
|
const restored = native().rollbackNativeProfile(options, dependency);
|
|
assert.equal(restored.home, full.home);
|
|
assert.equal(restored.storeRevision, 4);
|
|
assert.equal(fs.readFileSync(sentinel, 'utf8'), 'user owned');
|
|
}));
|
|
|
|
test('native idempotency re-verifies the current home without registration writes', () => fixture(options => {
|
|
const dependency = provider();
|
|
const first = native().prepareNativeProfile(options, dependency);
|
|
const calls = dependency.calls.length;
|
|
const repeated = native().prepareNativeProfile(options, dependency);
|
|
assert.equal(repeated.home, first.home);
|
|
assert.equal(repeated.revision, first.revision);
|
|
assert.equal(dependency.calls.slice(calls).some(call => call.args[0] === 'plugin'), false);
|
|
}));
|
|
|
|
test('unowned roots, provider home roots, overlaps and symlinks reject before provider execution', () => fixture((options, _repoRoot, parent) => {
|
|
const dependency = provider();
|
|
fs.mkdirSync(options.nativeRoot);
|
|
const sentinel = path.join(options.nativeRoot, 'sentinel');
|
|
fs.writeFileSync(sentinel, 'user');
|
|
assert.throws(() => native().prepareNativeProfile(options, dependency), /owned/i);
|
|
for (const root of [os.homedir(), path.join(os.homedir(), '.codex'), options.stateRoot, path.dirname(options.stateRoot)]) {
|
|
assert.throws(() => native().prepareNativeProfile({ ...options, nativeRoot: root }, dependency), /root|overlap|dedicated/i);
|
|
}
|
|
const link = path.join(parent, 'link');
|
|
fs.symlinkSync(options.nativeRoot, link, process.platform === 'win32' ? 'junction' : 'dir');
|
|
assert.throws(() => native().prepareNativeProfile({ ...options, nativeRoot: link }, dependency), /link/i);
|
|
assert.equal(fs.readFileSync(sentinel, 'utf8'), 'user');
|
|
assert.equal(dependency.calls.length, 0);
|
|
}));
|
|
|
|
test('unsupported versions fail before provider registration and require explicit recovery', () => fixture(options => {
|
|
const dependency = provider({ version: 'codex-cli 0.153.0' });
|
|
assert.throws(() => native().prepareNativeProfile(options, dependency), /version/i);
|
|
assert.equal(dependency.calls.some(call => call.args[0] === 'plugin'), false);
|
|
assert.equal(native().getNativeProfileStatus(options, dependency).status, 'recovery-required');
|
|
assert.equal(native().recoverNativeProfile(options, dependency).status, 'unconfigured');
|
|
}));
|
|
|
|
for (const corruption of ['missing', 'extra', 'bytes', 'disabled', 'escaped']) {
|
|
test(`native ${corruption} discovery never commits a ready pointer`, () => fixture(options => {
|
|
const dependency = provider({ alter: ({ skills, cache }) => {
|
|
if (corruption === 'missing') skills.pop();
|
|
if (corruption === 'extra') skills.push({ name: 'extra', pluginId: 'other', scope: 'user', enabled: true, path: '/outside' });
|
|
if (corruption === 'bytes') fs.appendFileSync(skills[0].path, 'tamper');
|
|
if (corruption === 'disabled') skills[0].enabled = false;
|
|
if (corruption === 'escaped') skills[0].path = path.join(cache, '../elsewhere/SKILL.md');
|
|
} });
|
|
assert.throws(() => native().prepareNativeProfile(options, dependency), /native|discovery|digest|path|skill/i);
|
|
assert.equal(fs.existsSync(path.join(options.nativeRoot, 'state.json')), false);
|
|
}));
|
|
}
|
|
|
|
test('store drift after readback blocks publication and recovery preserves previous home', () => fixture((options, repoRoot) => {
|
|
const dependency = provider();
|
|
const before = native().prepareNativeProfile(options, dependency);
|
|
store.applyStore({ repoRoot, stateRoot: options.stateRoot, target: 'codex', profileId: 'full@1' });
|
|
const drifting = provider({ onCheckpoint: point => {
|
|
if (point === 'verified') store.rollbackStore({ stateRoot: options.stateRoot });
|
|
} });
|
|
assert.throws(() => native().prepareNativeProfile(options, drifting), /store.*changed|binding/i);
|
|
const result = native().recoverNativeProfile(options, dependency);
|
|
assert.equal(result.status, 'stale');
|
|
assert.equal(result.home, before.home);
|
|
}));
|
|
|
|
for (const checkpoint of ['prepared', 'registered', 'verified', 'state-published']) {
|
|
test(`native recovery preserves the selected generation after ${checkpoint} interruption`, () => fixture(options => {
|
|
const interrupted = provider({ onCheckpoint: point => { if (point === checkpoint) throw new Error('interrupted'); } });
|
|
assert.throws(() => native().prepareNativeProfile(options, interrupted), /interrupted/);
|
|
const recovered = native().recoverNativeProfile(options, provider());
|
|
assert.equal(recovered.status, checkpoint === 'state-published' ? 'ready' : 'unconfigured');
|
|
assert.equal(fs.existsSync(path.join(options.nativeRoot, 'pending.json')), false);
|
|
}));
|
|
}
|
|
|
|
test('stale native revision and carrier preview fail before provider calls', () => fixture(options => {
|
|
const dependency = provider();
|
|
assert.throws(() => native().prepareNativeProfile({ ...options, expectedRevision: 4 }, dependency), /revision/i);
|
|
assert.throws(() => native().prepareNativeProfile({ ...options, expectedCarrierDigest: '0'.repeat(64) }, dependency), /digest/i);
|
|
assert.equal(dependency.calls.length, 0);
|
|
assert.equal(fs.existsSync(options.nativeRoot), false);
|
|
}));
|
|
|
|
test('native state revision is bound to an immutable transition receipt', () => fixture(options => {
|
|
const dependency = provider(); native().prepareNativeProfile(options, dependency);
|
|
const file = path.join(options.nativeRoot, 'state.json');
|
|
const state = JSON.parse(fs.readFileSync(file));
|
|
fs.writeFileSync(file, JSON.stringify({ ...state, storeRevision: 9 }));
|
|
assert.throws(() => native().getNativeProfileStatus(options), /receipt/i);
|
|
}));
|
|
|
|
test('native control drift after verification cannot replace the previous pointer', () => fixture((options, repoRoot) => {
|
|
const original = native().prepareNativeProfile(options, provider());
|
|
store.applyStore({ repoRoot, stateRoot: options.stateRoot, target: 'codex', profileId: 'full@1' });
|
|
const dependency = provider({ onCheckpoint: point => {
|
|
if (point === 'verified') {
|
|
const pending = JSON.parse(fs.readFileSync(path.join(options.nativeRoot, 'pending.json')));
|
|
fs.writeFileSync(path.join(options.nativeRoot, 'generations', pending.generationId, 'home/.codex/config.toml'), 'changed');
|
|
}
|
|
} });
|
|
assert.throws(() => native().prepareNativeProfile(options, dependency), /changed/);
|
|
assert.equal(JSON.parse(fs.readFileSync(path.join(options.nativeRoot, 'state.json'))).revision, original.revision);
|
|
assert.equal(fs.existsSync(path.join(options.nativeRoot, 'pending.json')), true);
|
|
}));
|
|
|
|
test('managed descriptor drift rejects before native registration', () => fixture(options => {
|
|
const dependency = provider({ onCheckpoint: point => {
|
|
if (point === 'prepared') {
|
|
const current = store.getStoreStatus({ stateRoot: options.stateRoot });
|
|
const file = path.join(path.dirname(current.generationRoot), 'carrier.json');
|
|
const carrier = JSON.parse(fs.readFileSync(file));
|
|
carrier.files[0].destinationPath = '../outside';
|
|
fs.writeFileSync(file, JSON.stringify(carrier));
|
|
}
|
|
} });
|
|
assert.throws(() => native().prepareNativeProfile(options, dependency), /carrier|schema|descriptor/i);
|
|
assert.equal(dependency.calls.length, 0);
|
|
}));
|
|
|
|
test('provider project trust bookkeeping does not invalidate native readiness', () => fixture(options => {
|
|
const dependency = provider();
|
|
const prepared = native().prepareNativeProfile(options, dependency);
|
|
// Codex rewrites config.toml with a project trust entry at every session start;
|
|
// that bookkeeping does not change skill discovery.
|
|
fs.appendFileSync(path.join(prepared.codexHome, 'config.toml'),
|
|
'\n[trust."/tmp/ecc-workspace"]\ntrust_level = "trusted"\n');
|
|
const status = native().getNativeProfileStatus(options, dependency);
|
|
assert.equal(status.status, 'ready');
|
|
assert.equal(status.ready, true);
|
|
}));
|
|
|
|
test('discovery-relevant provider config change still invalidates readiness', () => fixture(options => {
|
|
const dependency = provider();
|
|
const prepared = native().prepareNativeProfile(options, dependency);
|
|
fs.appendFileSync(path.join(prepared.codexHome, 'config.toml'), '\nmodel = "codex-99"\n');
|
|
assert.throws(() => native().getNativeProfileStatus(options, dependency), /changed/);
|
|
}));
|
|
|
|
test('executable digest tampering rejects readiness without provider execution', () => fixture((options, _repoRoot, parent) => {
|
|
const executable = path.join(parent, 'native-codex');
|
|
fs.writeFileSync(executable, Buffer.from([0x7f, 0x45, 0x4c, 0x46, 1, 2, 3, 4]), { mode: 0o700 });
|
|
const input = { ...options, codexPath: executable };
|
|
native().prepareNativeProfile(input, provider());
|
|
fs.appendFileSync(executable, 'changed');
|
|
assert.throws(() => native().getNativeProfileStatus(input), /executable.*changed/);
|
|
}));
|
|
|
|
test('config drift and added user skills reject static native readiness', () => fixture(options => {
|
|
const prepared = native().prepareNativeProfile(options, provider());
|
|
const extra = path.join(prepared.codexHome, 'skills/extra');
|
|
fs.mkdirSync(extra, { recursive: true });
|
|
fs.writeFileSync(path.join(extra, 'SKILL.md'), 'extra');
|
|
assert.throws(() => native().getNativeProfileStatus(options), /changed/);
|
|
}));
|
|
|
|
test('live lock is preserved and cannot be recovered by another native operation', () => fixture(options => {
|
|
native().prepareNativeProfile(options, provider());
|
|
const file = path.join(options.nativeRoot, '.lock');
|
|
const bytes = JSON.stringify({ pid: process.pid, hostname: os.hostname(), nonce: 'live' });
|
|
fs.writeFileSync(file, bytes);
|
|
assert.equal(native().getNativeProfileStatus(options).ready, false);
|
|
assert.throws(() => native().recoverNativeProfile(options), /live process/);
|
|
assert.equal(fs.readFileSync(file, 'utf8'), bytes);
|
|
}));
|
|
|
|
test('native executable FIFO is rejected without opening a blocking descriptor', context => fixture((options, _repoRoot, parent) => {
|
|
if (process.platform === 'win32') { context.skip('Named pipe creation is platform-specific'); return; }
|
|
const pipe = path.join(parent, 'codex-pipe');
|
|
const created = require('node:child_process').spawnSync('mkfifo', [pipe]);
|
|
assert.equal(created.status, 0);
|
|
assert.throws(() => native().prepareNativeProfile({ ...options, codexPath: pipe }, provider()), /regular file/);
|
|
assert.equal(fs.existsSync(options.nativeRoot), false);
|
|
}));
|
|
|
|
test('pinned npm shim resolves and hashes its native platform binary', () => fixture((_options, _repoRoot, parent) => {
|
|
const shim = path.join(parent, 'node_modules/@openai/codex/bin/codex.js');
|
|
fs.mkdirSync(path.dirname(shim), { recursive: true });
|
|
fs.writeFileSync(shim, '#!/usr/bin/env node\n');
|
|
const targets = { 'linux/arm64': 'aarch64-unknown-linux-musl', 'linux/x64': 'x86_64-unknown-linux-musl',
|
|
'darwin/arm64': 'aarch64-apple-darwin', 'darwin/x64': 'x86_64-apple-darwin',
|
|
'win32/arm64': 'aarch64-pc-windows-msvc', 'win32/x64': 'x86_64-pc-windows-msvc' };
|
|
const packageRoot = path.join(parent, 'node_modules/@openai', `codex-${process.platform}-${process.arch}`);
|
|
const binary = path.join(packageRoot, 'vendor', targets[`${process.platform}/${process.arch}`], 'bin', process.platform === 'win32' ? 'codex.exe' : 'codex');
|
|
fs.mkdirSync(path.dirname(binary), { recursive: true });
|
|
fs.writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({ name: '@openai/codex', version: '0.154.0' }));
|
|
fs.writeFileSync(binary, Buffer.from([0x7f, 0x45, 0x4c, 0x46, 1, 2, 3, 4]), { mode: 0o700 });
|
|
const resolved = require('../../scripts/lib/context-profile-native-executable').resolveExecutable(shim);
|
|
assert.equal(resolved.path, binary);
|
|
assert.equal(resolved.bytes, 8);
|
|
assert.match(resolved.digest, /^[a-f0-9]{64}$/);
|
|
}));
|
|
|
|
for (const change of ['changed', 'removed']) {
|
|
test(`explicit preparation refreshes a ${change} executable and preserves the old generation`, () => fixture((options, _repoRoot, parent) => {
|
|
const binary = path.join(parent, 'old-codex');
|
|
fs.writeFileSync(binary, Buffer.from('7f454c4601020304', 'hex'), { mode: 0o700 });
|
|
const first = native().prepareNativeProfile({ ...options, codexPath: binary }, provider());
|
|
const oldReceipt = fs.readFileSync(path.join(path.dirname(first.home), 'receipt.json'));
|
|
if (change === 'changed') fs.appendFileSync(binary, 'new build');
|
|
else fs.unlinkSync(binary);
|
|
assert.throws(() => native().getNativeProfileStatus(options));
|
|
const refreshed = native().prepareNativeProfile({ ...options, expectedRevision: first.revision }, provider());
|
|
assert.equal(refreshed.ready, true);
|
|
assert.equal(refreshed.revision, first.revision + 1);
|
|
assert.notEqual(refreshed.home, first.home);
|
|
assert.deepEqual(fs.readFileSync(path.join(path.dirname(first.home), 'receipt.json')), oldReceipt);
|
|
}));
|
|
}
|
|
|
|
test('failed executable refresh preserves pointer and can recover even when old binary is gone', () => fixture((options, _repoRoot, parent) => {
|
|
const binary = path.join(parent, 'old-codex');
|
|
fs.writeFileSync(binary, Buffer.from('7f454c4601020304', 'hex'), { mode: 0o700 });
|
|
native().prepareNativeProfile({ ...options, codexPath: binary }, provider());
|
|
const pointer = fs.readFileSync(path.join(options.nativeRoot, 'state.json'));
|
|
fs.unlinkSync(binary);
|
|
assert.throws(() => native().prepareNativeProfile(options, provider({ failInstall: true })), /command failed/);
|
|
assert.deepEqual(fs.readFileSync(path.join(options.nativeRoot, 'state.json')), pointer);
|
|
const recovered = native().recoverNativeProfile(options);
|
|
assert.equal(recovered.ready, false);
|
|
assert.equal(recovered.status, 'refresh-required');
|
|
assert.throws(() => native().getNativeProfileStatus(options));
|
|
assert.equal(native().prepareNativeProfile(options, provider()).ready, true);
|
|
}));
|
|
|
|
test('refresh never excuses modified old managed files', () => fixture((options, _repoRoot, parent) => {
|
|
const binary = path.join(parent, 'old-codex');
|
|
fs.writeFileSync(binary, Buffer.from('7f454c4601020304', 'hex'), { mode: 0o700 });
|
|
const first = native().prepareNativeProfile({ ...options, codexPath: binary }, provider());
|
|
fs.unlinkSync(binary);
|
|
fs.writeFileSync(path.join(first.codexHome, 'AGENTS.md'), 'tampered');
|
|
const dependency = provider();
|
|
assert.throws(() => native().prepareNativeProfile(options, dependency), /changed/);
|
|
assert.equal(dependency.calls.length, 0);
|
|
}));
|
|
|
|
for (const version of ['0.154.0', '0.155.1']) {
|
|
test(`native preparation pins discovered supported version ${version}`, () => fixture(options => {
|
|
const result = native().prepareNativeProfile(options, provider({ version: `codex-cli ${version}` }));
|
|
assert.equal(result.providerVersion, version);
|
|
assert.equal(native().getNativeProfileStatus(options).providerVersion, version);
|
|
}));
|
|
}
|
|
for (const version of ['0.155.0', '0.155.10', '0.155.1-dev', '0.156.0', '0.155.1 extra']) {
|
|
test(`native version gate rejects ${version} before plugin registration`, () => fixture(options => {
|
|
const dependency = provider({ version: `codex-cli ${version}` });
|
|
assert.throws(() => native().prepareNativeProfile(options, dependency), /version/);
|
|
assert.equal(dependency.calls.some(call => call.args[0] === 'plugin'), false);
|
|
}));
|
|
}
|
|
|
|
test('same-path replacement is refreshed and version drift during discovery blocks publication', () => fixture((options, _repoRoot, parent) => {
|
|
const binary = path.join(parent, 'codex');
|
|
fs.writeFileSync(binary, Buffer.from('7f454c4601020304', 'hex'), { mode: 0o700 });
|
|
const input = { ...options, codexPath: binary };
|
|
const first = native().prepareNativeProfile(input, provider());
|
|
fs.appendFileSync(binary, 'replacement');
|
|
const second = native().prepareNativeProfile(input, provider({ version: 'codex-cli 0.155.1' }));
|
|
assert.notEqual(second.home, first.home);
|
|
assert.equal(second.providerVersion, '0.155.1');
|
|
fs.appendFileSync(binary, 'another replacement');
|
|
const dependency = provider();
|
|
const execute = dependency.execute;
|
|
let calls = 0;
|
|
dependency.execute = (command, args, config) => args[0] === '--version' && ++calls > 1
|
|
? { status: 0, stdout: 'codex-cli 0.155.1' } : execute(command, args, config);
|
|
assert.throws(() => native().prepareNativeProfile(input, dependency), /version changed/);
|
|
assert.equal(JSON.parse(fs.readFileSync(path.join(input.nativeRoot, 'state.json'))).revision, second.revision);
|
|
}));
|