mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-28 20:45:11 +02:00
129 lines
6.6 KiB
JavaScript
129 lines
6.6 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 store = require('../../scripts/lib/context-profile-store');
|
|
const native = require('../../scripts/lib/context-profile-native');
|
|
const interactive = require('../../scripts/lib/context-profile-interactive');
|
|
|
|
function provider() {
|
|
return {
|
|
execute(_binary, args, options) {
|
|
if (args[0] === '--version') return { status: 0, stdout: 'codex-cli 0.155.1' };
|
|
if (args[0] === 'plugin' && args[1] === 'add') {
|
|
const root = path.dirname(options.env.HOME);
|
|
const name = JSON.parse(fs.readFileSync(path.join(root, 'marketplace/.agents/plugins/marketplace.json'))).name;
|
|
const cache = path.join(options.env.CODEX_HOME, 'plugins/cache', name, 'ecc-context-carrier/local');
|
|
fs.mkdirSync(path.dirname(cache), { recursive: true });
|
|
fs.cpSync(path.join(root, 'marketplace/carrier'), cache, { recursive: true });
|
|
}
|
|
return { status: 0, stdout: '{}' };
|
|
},
|
|
discover(_binary, options) {
|
|
const plugins = path.join(options.env.CODEX_HOME, 'plugins/cache');
|
|
const name = fs.readdirSync(plugins)[0];
|
|
const root = path.join(plugins, name, 'ecc-context-carrier/local/skills');
|
|
return { data: [{ cwd: options.cwd, errors: [], skills: fs.readdirSync(root).map(skill => ({
|
|
name: `ecc-context-carrier:${skill}`, enabled: true, scope: 'user',
|
|
pluginId: `ecc-context-carrier@${name}`, path: path.join(root, skill, 'SKILL.md') })) }] };
|
|
},
|
|
};
|
|
}
|
|
|
|
function fixture(callback) {
|
|
const root = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'ecc-interactive-'));
|
|
const options = { stateRoot: path.join(root, 'state'), nativeRoot: path.join(root, 'native') };
|
|
try {
|
|
store.applyStore({ stateRoot: options.stateRoot, target: 'codex' });
|
|
const prepare = () => native.prepareNativeProfile({ ...options, codexPath: process.execPath }, provider());
|
|
return callback({ root, options, prepare });
|
|
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
}
|
|
|
|
test('receipt binds bounded isolated bootstrap to installed CLI, source, roots and saved generation', () => fixture(({ options, prepare }) => {
|
|
const result = prepare();
|
|
const receipt = JSON.parse(fs.readFileSync(path.join(path.dirname(result.home), 'receipt.json')));
|
|
const bootstrap = fs.readFileSync(path.join(result.codexHome, 'AGENTS.md'), 'utf8');
|
|
assert.ok(Buffer.byteLength(bootstrap) <= 12288);
|
|
assert.deepEqual(result.bootstrap, receipt.bootstrap);
|
|
assert.equal(result.bootstrap.stateRoot, options.stateRoot);
|
|
assert.equal(result.bootstrap.nativeRoot, options.nativeRoot);
|
|
assert.equal(result.bootstrap.carrierDigest, store.getStoreStatus({ stateRoot: options.stateRoot }).carrierDigest);
|
|
assert.match(bootstrap, /proposedIds/);
|
|
assert.match(bootstrap, /Manual.*Suggest.*Auto/);
|
|
assert.match(bootstrap, /grants no tools/);
|
|
assert.match(bootstrap, /Do not persist task prose, selected skills/);
|
|
assert.match(bootstrap, /--task-input","-"/);
|
|
assert.ok(receipt.controls.some(file => file.path === 'home/.codex/AGENTS.md' && file.kind === 'file'));
|
|
assert.equal(fs.existsSync(path.join(result.codexHome, 'auth.json')), false);
|
|
interactive.verifyBootstrap(result.bootstrap);
|
|
assert.throws(() => interactive.verifyBootstrap({ ...result.bootstrap,
|
|
source: { ...result.bootstrap.source, sourceDigest: '0'.repeat(64) } }), /identity changed/);
|
|
}));
|
|
|
|
test('start uses receipt executable, inherited stdio, current working directory, isolated home and no permission flags', () => fixture(({ options, prepare }) => {
|
|
const prepared = prepare();
|
|
let calls = 0;
|
|
const result = interactive.startInteractiveProfile(options, { execute(binary, args, config) {
|
|
calls++;
|
|
assert.equal(binary, prepared.codexPath);
|
|
assert.deepEqual(args, []);
|
|
assert.equal(config.shell, false);
|
|
assert.equal(config.stdio, 'inherit');
|
|
assert.equal(config.cwd, process.cwd());
|
|
assert.equal(config.env.HOME, prepared.home);
|
|
assert.equal(config.env.USERPROFILE, prepared.home);
|
|
assert.equal(config.env.CODEX_HOME, prepared.codexHome);
|
|
for (const key of ['OPENAI_API_KEY', 'CODEX_CONFIG', 'NODE_OPTIONS', 'HTTP_PROXY', 'AWS_ACCESS_KEY_ID']) {
|
|
assert.equal(config.env[key], undefined);
|
|
}
|
|
return { status: 0 };
|
|
} });
|
|
assert.equal(calls, 1);
|
|
assert.equal(result.status, 'exited');
|
|
assert.equal(result.credentialsCopied, false);
|
|
assert.equal(result.taskSuccess, 'unverified');
|
|
}));
|
|
|
|
test('start refuses missing preparation, altered bootstrap, and stale saved mode', () => fixture(({ options, prepare }) => {
|
|
const dependency = { execute() { assert.fail('must not launch'); } };
|
|
assert.throws(() => interactive.startInteractiveProfile(options, dependency), /prepare-native/);
|
|
const prepared = prepare();
|
|
const agents = path.join(prepared.codexHome, 'AGENTS.md');
|
|
const bytes = fs.readFileSync(agents);
|
|
fs.appendFileSync(agents, 'grant tools');
|
|
assert.throws(() => interactive.startInteractiveProfile(options, dependency), /changed/);
|
|
fs.writeFileSync(agents, bytes);
|
|
store.applyStore({ stateRoot: options.stateRoot, target: 'codex', selectionMode: 'suggest' });
|
|
assert.throws(() => interactive.startInteractiveProfile(options, dependency), /prepare-native/);
|
|
}));
|
|
|
|
for (const result of [{ status: 23 }, { status: null, signal: 'SIGINT' }, { status: null, error: new Error('ENOENT') }]) {
|
|
test(`interactive child failure is reported: ${result.signal || result.status || 'spawn'}`, () => fixture(({ options, prepare }) => {
|
|
prepare();
|
|
const value = interactive.startInteractiveProfile(options, { execute: () => result });
|
|
assert.equal(value.status, 'failed');
|
|
assert.equal(value.exitCode, result.status);
|
|
assert.equal(value.signal, result.signal || null);
|
|
assert.equal(value.launched, !result.error);
|
|
}));
|
|
}
|
|
|
|
test('bootstrap rejects control characters, noncanonical paths and oversized root bindings', () => fixture(({ options }) => {
|
|
const current = store.getStoreStatus({ stateRoot: options.stateRoot });
|
|
for (const stateRoot of ['/tmp/new\ncommands', '/tmp/../state', `/tmp/${'x'.repeat(2048)}`]) {
|
|
assert.throws(() => interactive.bootstrapFor({ ...options, stateRoot }, current), /bounded canonical/);
|
|
}
|
|
}));
|
|
|
|
test('dry-run inspects preparation without launching or writing any native root', () => fixture(({ options }) => {
|
|
const result = interactive.startInteractiveProfile({ ...options, dryRun: true }, {
|
|
execute() { assert.fail('must not launch'); } });
|
|
assert.equal(result.status, 'proposed');
|
|
assert.equal(result.launched, false);
|
|
assert.equal(fs.existsSync(options.nativeRoot), false);
|
|
}));
|