Files
ECC/tests/lib/context-profile-launch.test.js

158 lines
8.3 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const { withFixture } = require('./helpers/context-fixture');
const { launchTaskContext } = require('../../scripts/lib/context-profile-launch');
const input = { sessionId: 'launch', taskId: 'task', revision: 1, phase: 'implement', query: 'Explain a Python list', explicitIds: ['skill:feature'] };
function nativeFixture(repoRoot) {
const home = path.join(fs.realpathSync(repoRoot), 'isolated-home');
const codexPath = path.join(fs.realpathSync(repoRoot), 'provider-bin');
const bytes = Buffer.from('7f454c460102030405060708', 'hex');
fs.writeFileSync(codexPath, bytes);
return { home, codexHome: path.join(home, '.codex'), codexPath,
executableDigest: crypto.createHash('sha256').update(bytes).digest('hex') };
}
test('Auto launcher resolves context and supplies it on stdin without permission overrides', () => withFixture(repoRoot => {
let called = 0;
const result = launchTaskContext({ repoRoot, task: input, target: 'codex', execute(command, args, options) {
called++;
assert.equal(command, 'codex');
assert.deepEqual(args, ['exec', '-']);
assert.ok(options.input.includes(input.query));
assert.match(options.input, /# feature/);
assert.equal(options.shell, false);
assert.equal(options.killSignal, 'SIGKILL');
return { status: 0, stdout: 'A list is a sequence.', stderr: '' };
} });
assert.equal(called, 1);
assert.equal(result.status, 'completed');
assert.equal(result.taskSuccess, 'unverified');
assert.equal(result.selection.receipt.loadedIds.length, 1);
}));
test('dry-run neither loads bodies nor invokes a provider', () => withFixture(repoRoot => {
const result = launchTaskContext({ repoRoot, task: input, dryRun: true, execute() { assert.fail('must not execute'); } });
assert.equal(result.status, 'proposed');
assert.deepEqual(result.selection.loadedIds, []);
}));
test('Claude uses documented print mode and receives context as ordinary input', () => withFixture(repoRoot => {
launchTaskContext({ repoRoot, task: input, target: 'claude', execute(command, args) {
assert.equal(command, 'claude');
assert.deepEqual(args, ['--print']);
return { status: 0, stdout: 'ok', stderr: '' };
} });
}));
test('unsupported providers and failed selection cannot invoke a process', () => withFixture(repoRoot => {
assert.throws(() => launchTaskContext({ repoRoot, task: input, target: 'pi' }), /unsupported/i);
assert.throws(() => launchTaskContext({ repoRoot, task: input, exclude: ['skill:feature'], execute() { assert.fail('must not execute'); } }), /excluded/);
}));
test('provider failure is distinct from successful task completion', () => withFixture(repoRoot => {
const result = launchTaskContext({ repoRoot, task: input, execute: () => ({ status: 2, stdout: '', stderr: 'authentication required' }) });
assert.equal(result.status, 'failed');
assert.equal(result.exitCode, 2);
assert.equal(result.taskSuccess, 'unverified');
}));
test('isolated native launches replace every provider home without mutating the parent environment', () => withFixture(repoRoot => {
const nativeEnvironment = nativeFixture(repoRoot);
const before = { ...process.env };
// Windows may expose the inherited key as Path while process.env resolves PATH case-insensitively.
const inheritedPath = process.env.PATH;
let called = false;
const result = launchTaskContext({ repoRoot, task: input, nativeEnvironment, execute(command, args, options) {
called = true;
assert.equal(command, nativeEnvironment.codexPath);
assert.deepEqual(args, ['exec', '-']);
assert.notEqual(options.env, process.env);
assert.equal(options.env.HOME, nativeEnvironment.home);
assert.equal(options.env.USERPROFILE, nativeEnvironment.home);
assert.equal(options.env.CODEX_HOME, nativeEnvironment.codexHome);
assert.equal(options.env.PATH, inheritedPath);
for (const key of ['AWS_ACCESS_KEY_ID', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'HTTP_PROXY', 'NODE_OPTIONS']) {
assert.equal(options.env[key], undefined);
}
assert.equal(options.shell, false);
assert.equal(options.timeout, 120000);
assert.equal(options.killSignal, 'SIGKILL');
assert.equal(options.maxBuffer, 1024 * 1024);
return { status: 0, stdout: 'ok' };
} });
assert.equal(called, true);
assert.equal(result.providerConfiguration, 'isolated-native-generation');
assert.deepEqual({ ...process.env }, before);
}));
test('isolated native dry-run avoids provider calls and leaves context unloaded', () => withFixture(repoRoot => {
const result = launchTaskContext({ repoRoot, task: input, dryRun: true,
nativeEnvironment: nativeFixture(repoRoot),
execute() { assert.fail('Dry-run must not invoke a provider'); } });
assert.equal(result.status, 'proposed');
assert.equal(result.providerConfiguration, 'isolated-native-generation');
assert.deepEqual(result.selection.resources, []);
}));
test('invalid native environment and empty query fail before provider calls', () => withFixture(repoRoot => {
const execute = () => assert.fail('Invalid launch must not invoke a provider');
for (const nativeEnvironment of [{}, { home: 'relative', codexHome: repoRoot },
{ home: repoRoot, codexHome: 'relative' }, { home: repoRoot, codexHome: repoRoot },
{ ...nativeFixture(repoRoot), codexPath: 'relative' },
{ ...nativeFixture(repoRoot), executableDigest: 'not-a-digest' }]) {
assert.throws(() => launchTaskContext({ repoRoot, task: input, nativeEnvironment, execute }), /Invalid isolated/);
}
assert.throws(() => launchTaskContext({ repoRoot, task: input, target: 'claude', execute,
nativeEnvironment: { home: repoRoot, codexHome: repoRoot } }), /Invalid isolated/);
assert.throws(() => launchTaskContext({ repoRoot, task: { ...input, query: ' ' }, execute }), /non-empty query/);
}));
test('pinned native executable digest mismatch stops before any provider call', () => withFixture(repoRoot => {
const nativeEnvironment = { ...nativeFixture(repoRoot), executableDigest: '0'.repeat(64) };
assert.throws(() => launchTaskContext({ repoRoot, task: input, nativeEnvironment,
execute() { assert.fail('Mismatched executable must never run'); } }), /executable.*changed|digest.*mismatch/i);
}));
test('native executable drift during Auto proposal prevents the task process', () => withFixture(repoRoot => {
const nativeEnvironment = nativeFixture(repoRoot);
fs.writeFileSync(path.join(repoRoot, 'skills/feature/SKILL.md'),
'---\nname: feature\ndescription: Handle database changes\n---\nUse an explicit transaction.');
const task = { ...input, explicitIds: [], query: 'Handle database changes' };
let calls = 0;
assert.throws(() => launchTaskContext({ repoRoot, task, nativeEnvironment, execute(command, args, options) {
calls++;
assert.equal(command, nativeEnvironment.codexPath);
assert.ok(args.includes('read-only'));
assert.equal(options.env.CODEX_HOME, nativeEnvironment.codexHome);
fs.appendFileSync(nativeEnvironment.codexPath, Buffer.from([9]));
return { status: 0, stdout: '{"selectedIds":["skill:feature"]}' };
} }), /executable.*changed|digest.*mismatch/i);
assert.equal(calls, 1);
}));
test('configured-state refusal precedes the Auto proposal process', () => withFixture(repoRoot => {
fs.writeFileSync(path.join(repoRoot, 'skills/feature/SKILL.md'),
'---\nname: feature\ndescription: Handle database changes\n---\nUse an explicit transaction.');
assert.throws(() => launchTaskContext({ repoRoot,
task: { ...input, explicitIds: [], query: 'Handle database changes' },
assertCurrent() { throw new Error('Stored profile changed'); },
execute() { assert.fail('Stale state must not start proposal'); } }), /Stored profile changed/);
}));
test('spawn failures and timeout signals remain unsuccessful without a native exit status', () => withFixture(repoRoot => {
for (const error of [new Error('spawn codex ENOENT'), new Error('spawn codex ETIMEDOUT')]) {
const result = launchTaskContext({ repoRoot, task: input,
execute: () => ({ status: null, signal: 'SIGTERM', error }) });
assert.equal(result.status, 'failed');
assert.equal(result.exitCode, 1);
assert.equal(result.output, '');
assert.equal(result.error, error.message);
assert.equal(result.taskSuccess, 'unverified');
}
}));