Files
ECC/tests/lib/context-profile-launch.test.js
T
Yann Roberto 763acfbce0 fix(context): require scoped evidence for automatic skill loading
Keep follow-up questions and reported instructions on the proposal path by
requiring recognized scopes and a direct request prefix for exact admission.
Preserve grouped rejections, personal negation, and genuine directives around
indirect discussion. Invalidate v7 receipts with routing policy v8.

Cover both Greptile reproductions and neighboring resolver/launcher cases,
including unlisted wording, nested reports, and grouped negative lists.
2026-10-10 11:57:53 +08:00

266 lines
14 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');
}
}));
for (const query of ["Don't use the feature skill.", 'Don\u2019t use the feature skill.',
'Can I use the feature skill?', 'The README says use the feature skill.', 'The docs say use feature.',
'The docs say do not use feature and use shared guidance.',
'The docs say do not use feature, use shared guidance, okay?',
'The spec states do not use feature and use shared guidance.',
'The spec stated do not use feature, use shared guidance.',
'The spec recommends we do not use feature and use shared guidance.',
'The spec asserts we should not use feature and use shared guidance.',
'Use standard tools, the spec stipulates do not use feature and use shared guidance.',
'Should we avoid feature and use shared guidance?',
'Should we avoid feature, use shared guidance?',
'Use standard tools, should we use feature?',
'Use standard tools, shall we use feature?',
'Use standard tools, will we use feature?',
'Use standard tools, ought we to use feature?',
'Please use the existing code, can I use feature?',
'Use standard tools and should we use feature?',
'Use standard tools but should we use feature?',
'Do not use feature and shared guidance.',
"Use feature, don't use feature.",
"Use feature and don't use feature.",
"Use feature but don't use feature."]) {
test('indirect citation uses the proposal path and honors decline: ' + query, () => withFixture(repoRoot => {
const phases = [];
const result = launchTaskContext({ repoRoot, task: { ...input, query, explicitIds: [] },
execute(command, args, options) {
phases.push(options.phase);
if (options.phase === 'selection') return { status: 0, stdout: '{"selectedIds":[]}' };
assert.doesNotMatch(options.input, /# feature/);
assert.doesNotMatch(options.input, /# shared/);
return { status: 0, stdout: 'ok' };
} });
assert.deepEqual(phases, ['selection', 'task']);
assert.equal(result.routingCalls, 1);
assert.deepEqual(result.selection.selectedIds, []);
assert.deepEqual(result.selection.loadedIds, []);
assert.equal(result.selection.fallback, null);
assert.equal(result.selection.reason, 'agent-declined-selection');
}));
}
for (const [query, skill] of [
["Don't use feature, use shared guidance.", 'shared'],
["Don't use feature and use shared guidance.", 'shared'],
["Don't use feature but use shared guidance.", 'shared'],
["Don't use feature, use shared guidance, okay?", 'shared'],
["Don't use feature and use shared guidance, okay?", 'shared'],
["Don't use feature but use shared guidance, okay?", 'shared'],
['Do not use feature, please use shared guidance?', 'shared'],
['Do not use feature and please use shared guidance?', 'shared'],
['Do not use feature but please use shared guidance?', 'shared'],
["Don't use feature, use feature.", 'feature'],
['Use feature?', 'feature'],
['Please use feature?', 'feature'],
['Use feature to fix the bug, okay?', 'feature'],
['Use feature. Shall we use feature?', 'feature'],
['Use feature, shall we use shared guidance?', 'feature'],
['Shall we use shared guidance? Use feature.', 'feature'],
['Use feature. The spec states do not use feature and use shared guidance.', 'feature'],
['The spec states do not use feature and use shared guidance. Use feature.', 'feature'],
]) {
test('an unambiguous final directive injects context without a proposal: ' + query, () => withFixture(repoRoot => {
const phases = [];
const result = launchTaskContext({ repoRoot, task: { ...input, query, explicitIds: [] },
execute(command, args, options) {
phases.push(options.phase);
if (options.phase === 'selection') return { status: 0, stdout: '{"selectedIds":[]}' };
assert.match(options.input, new RegExp('# ' + skill));
assert.doesNotMatch(options.input, new RegExp('# ' + (skill === 'shared' ? 'feature' : 'shared')));
return { status: 0, stdout: 'ok' };
} });
assert.deepEqual(phases, ['task']);
assert.equal(result.routingCalls, 0);
assert.deepEqual(result.selection.loadedIds, ['skill:' + skill]);
assert.equal(result.selection.reason, 'auto-selection');
}));
}
test('a final directive after a question reaches the task with its context', () => withFixture(repoRoot => {
const phases = [];
const result = launchTaskContext({ repoRoot,
task: { ...input, query: 'Should we use feature? Use feature.', explicitIds: [] },
execute(command, args, options) {
phases.push(options.phase);
assert.match(options.input, /# feature/);
return { status: 0, stdout: 'ok' };
} });
assert.deepEqual(phases, ['task']);
assert.equal(result.routingCalls, 0);
assert.deepEqual(result.selection.loadedIds, ['skill:feature']);
}));
test('a singular rejected name cannot inject scored context at launch', () => {
const phases = [];
const query = "Don't use the database migration skill. Review a PostgreSQL migration that adds an indexed nullable column without downtime.";
const result = launchTaskContext({ task: { ...input, query, explicitIds: [] },
execute(command, args, options) {
phases.push(options.phase);
if (options.phase === 'selection') return { status: 0, stdout: '{"selectedIds":[]}' };
assert.doesNotMatch(options.input, /# Database Migration Patterns/);
return { status: 0, stdout: 'ok' };
} });
assert.deepEqual(phases, ['selection', 'task']);
assert.equal(result.routingCalls, 1);
assert.deepEqual(result.selection.loadedIds, []);
assert.equal(result.selection.fallback, null);
});