Files
ECC/tests/scripts/profile-selection.test.js
T

212 lines
12 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 { spawnSync } = require('node:child_process');
const test = require('node:test');
const { withFixture } = require('../lib/helpers/context-fixture');
const CLI = path.resolve(__dirname, '../../scripts/ecc.js');
const PROFILE_CLI = path.resolve(__dirname, '../../scripts/profile.js');
function cliFixture(run) {
const root = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'ecc-selection-cli-'));
const stateRoot = path.join(root, 'managed');
const input = path.join(root, 'task.json');
const setTask = values => fs.writeFileSync(input, JSON.stringify({ sessionId: 'test', taskId: 'test',
revision: 1, phase: 'implement', query: 'Explain Python lists', ...values }));
setTask({ proposedIds: ['skill:python-patterns'] });
const invoke = (args, { preload, env = {} } = {}) => {
const entry = preload ? ['--require', preload, PROFILE_CLI] : [CLI, 'profile'];
const child = spawnSync(process.execPath, [...entry, ...args, '--json'], {
cwd: root, encoding: 'utf8', timeout: 30000, maxBuffer: 1024 * 1024,
env: { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot,
NODE_V8_COVERAGE: process.env.NODE_V8_COVERAGE, ...env },
});
assert.ok(child.stdout, child.stderr || child.error?.message);
return { code: child.status, response: JSON.parse(child.stdout) };
};
try { return run({ root, stateRoot, input, setTask, invoke }); }
finally { fs.rmSync(root, { recursive: true, force: true }); }
}
function providerFixture(root) {
const preload = path.join(root, 'provider-preload.cjs');
const sentinel = path.join(root, 'provider-executed');
fs.writeFileSync(preload, `
const cp = require('node:child_process');
const fs = require('node:fs');
const original = cp.spawnSync;
cp.spawnSync = (command, ...args) => {
if (command !== 'codex' && command !== 'claude') return original(command, ...args);
fs.writeFileSync(process.env.ECC_TEST_PROVIDER_SENTINEL, 'executed');
return { status: Number(process.env.ECC_TEST_PROVIDER_STATUS || 0),
stdout: 'fixture output', stderr: 'fixture provider failure' };
};
`);
return { preload, sentinel, env: { ECC_TEST_PROVIDER_SENTINEL: sentinel } };
}
test('CLI resolves and loads explicit task context with JSON output', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-selection-cli-'));
try {
const input = path.join(root, 'task.json');
fs.writeFileSync(input, JSON.stringify({ sessionId: 'test', taskId: 'test', revision: 1, phase: 'implement',
explicitIds: ['skill:python-patterns'] }));
const args = ['profile', 'resolve', '--task-input', input, '--load', '--json'];
const run = extra => spawnSync(process.execPath, [CLI, ...extra, ...args], {
cwd: root, encoding: 'utf8', timeout: 30000, maxBuffer: 1024 * 1024,
env: { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot,
NODE_V8_COVERAGE: process.env.NODE_V8_COVERAGE },
});
const result = run([]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.deepEqual(JSON.parse(result.stdout).selection.loadedIds, ['skill:python-patterns']);
const preview = run(['--dry-run']);
assert.equal(preview.status, 0, preview.stderr || preview.stdout);
assert.deepEqual(JSON.parse(preview.stdout).selection.loadedIds, []);
assert.deepEqual(fs.readdirSync(root), ['task.json']);
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});
test('resolver rejects unknown flags before reading task input', () => {
const result = spawnSync(process.execPath, [CLI, 'profile', 'resolve', '--task-input', 'missing', '--hooks', 'full', '--json'], { encoding: 'utf8' });
assert.equal(result.status, 1);
assert.match(JSON.parse(result.stdout).summary, /Unknown argument/);
});
test('saved mode and exclusions govern resolution and survive mode changes', () => cliFixture(({ stateRoot, input, setTask, invoke }) => {
const setup = invoke(['set', 'lean', '--state-root', stateRoot, '--selection', 'suggest',
'--include', 'skill:python-patterns', '--exclude', 'skill:python-testing']);
assert.equal(setup.code, 0, setup.response.summary);
const resolveArgs = ['resolve', '--state-root', stateRoot, '--task-input', input, '--load'];
const suggestion = invoke(resolveArgs);
assert.equal(suggestion.code, 0, suggestion.response.summary);
assert.equal(suggestion.response.selection.selectionMode, 'suggest');
assert.deepEqual(suggestion.response.selection.selectedIds, ['skill:python-patterns']);
assert.deepEqual(suggestion.response.selection.loadedIds, []);
const preview = invoke(['mode', 'manual', '--state-root', stateRoot, '--dry-run']);
assert.equal(preview.code, 0, preview.response.summary);
assert.equal(preview.response.store.status, 'proposed');
assert.equal(invoke(['status', '--state-root', stateRoot]).response.store.selectionMode, 'suggest');
const changed = invoke(['mode', 'manual', '--state-root', stateRoot, '--expected-revision', '1']);
assert.equal(changed.code, 0, changed.response.summary);
assert.deepEqual(changed.response.store.include, ['skill:python-patterns']);
assert.deepEqual(changed.response.store.exclude, ['skill:python-testing']);
const manual = invoke(resolveArgs);
assert.equal(manual.code, 0, manual.response.summary);
assert.deepEqual(manual.response.selection.selectedIds, []);
assert.equal(manual.response.selection.selectionMode, 'manual');
setTask({ explicitIds: ['skill:python-testing'] });
const excluded = invoke(resolveArgs);
assert.equal(excluded.code, 1);
assert.match(excluded.response.summary, /excluded/);
setTask({ proposedIds: ['skill:python-patterns'] });
assert.equal(invoke(['mode', 'auto', '--state-root', stateRoot]).code, 0);
const automatic = invoke(resolveArgs);
assert.equal(automatic.code, 0, automatic.response.summary);
assert.deepEqual(automatic.response.selection.loadedIds, ['skill:python-patterns']);
}));
test('stored resolution and launch reject every configuration override before reading input', () => cliFixture(({ stateRoot, invoke }) => {
for (const command of ['resolve', 'run']) {
for (const override of [['full'], ['--target', 'claude'], ['--selection', 'manual'],
['--include', 'skill:python-patterns'], ['--exclude', 'skill:python-testing']]) {
const result = invoke([command, '--state-root', stateRoot, '--task-input', 'missing', ...override]);
assert.equal(result.code, 1);
assert.match(result.response.summary, /cannot override/);
}
}
}));
test('launch dry runs never load bodies or execute a provider', () => cliFixture(({ root, input, invoke }) => {
const provider = providerFixture(root);
for (const dry of [{ args: ['--dry-run'], env: {} }, { args: [], env: { ECC_DRY_RUN: '1' } }]) {
const result = invoke(['run', '--task-input', input, ...dry.args],
{ preload: provider.preload, env: { ...provider.env, ...dry.env } });
assert.equal(result.code, 0, result.response.summary);
assert.equal(result.response.launch.status, 'proposed');
assert.deepEqual(result.response.launch.selection.loadedIds, []);
assert.equal(fs.existsSync(provider.sentinel), false);
}
}));
test('provider exit failures produce a failed CLI result and preserve the native exit code', () => cliFixture(({ root, input, invoke }) => {
const provider = providerFixture(root);
const result = invoke(['run', '--task-input', input], { preload: provider.preload,
env: { ...provider.env, ECC_TEST_PROVIDER_STATUS: '23' } });
assert.equal(fs.readFileSync(provider.sentinel, 'utf8'), 'executed');
assert.equal(result.code, 1);
assert.equal(result.response.status, 'error');
assert.equal(result.response.launch.status, 'failed');
assert.equal(result.response.launch.exitCode, 23);
assert.equal(result.response.launch.taskSuccess, 'unverified');
assert.match(result.response.launch.error, /fixture provider failure/);
}));
test('unsupported targets and stale selection digests fail before provider execution', () => cliFixture(({ root, input, invoke }) => {
const provider = providerFixture(root);
for (const args of [['--target', 'pi'], ['--expected-digest', '0'.repeat(64)]]) {
const result = invoke(['run', '--task-input', input, ...args], provider);
assert.equal(result.code, 1);
assert.match(result.response.summary, /Unsupported|stale/);
assert.equal(fs.existsSync(provider.sentinel), false);
}
}));
test('unconfigured and source-stale stores cannot resolve task context', () => cliFixture(({ stateRoot, input, invoke }) => {
const args = ['resolve', '--state-root', stateRoot, '--task-input', input, '--load'];
const absent = invoke(args);
assert.equal(absent.code, 1);
assert.match(absent.response.summary, /Configure or recover/);
withFixture(repoRoot => require('../../scripts/lib/context-profile-store').applyStore({ repoRoot, stateRoot }));
const stale = invoke(args);
assert.equal(stale.code, 1);
assert.match(stale.response.summary, /source is stale/);
}));
test('malformed operation flags and stale write preconditions fail without creating a store', () => cliFixture(({ stateRoot, invoke }) => {
for (const [args, message] of [
[['mode', 'unknown', '--state-root', stateRoot], /Choose mode/],
[['set', 'lean', '--state-root'], /Missing value/],
[['set', 'lean', '--state-root', stateRoot, '--state-root', stateRoot], /Duplicate argument/],
[['set', 'lean', '--state-root', stateRoot, '--task-input', 'missing'], /unavailable/],
[['set', 'lean', '--state-root', stateRoot, '--expected-revision', '01'], /nonnegative integer/],
[['set', 'lean', '--state-root', stateRoot, '--expected-revision', '1'], /revision changed/],
[['set', 'lean', '--state-root', stateRoot, '--expected-digest', 'bad'], /Invalid expected/],
[['set', 'lean', '--state-root', stateRoot, '--expected-digest', '0'.repeat(64)], /digest changed/],
[['run', '--task-input', 'missing', '--load'], /Unknown argument/],
]) {
const result = invoke(args);
assert.equal(result.code, 1);
assert.match(result.response.summary, message);
assert.equal(fs.existsSync(stateRoot), false);
}
}));
test('native command routing rejects missing roots and unsupported flags before provider or filesystem work', () => cliFixture(({ root, stateRoot, invoke }) => {
const nativeRoot = path.join(root, 'native');
for (const command of ['prepare-native', 'native-status', 'native-rollback', 'native-recover']) {
for (const [args, pattern] of [
[[], /requires --state-root/],
[['--state-root', stateRoot], /requires --native-root/],
[['--state-root', stateRoot, '--native-root', nativeRoot, '--target', 'codex'], /unavailable/],
[['--state-root', stateRoot, '--native-root', nativeRoot, '--expected-revision', '1e2'], /nonnegative integer/],
]) {
const result = invoke([command, ...args]);
assert.equal(result.code, 1);
assert.match(result.response.summary, pattern);
}
}
const orphan = invoke(['run', '--native-root', nativeRoot, '--task-input', 'missing']);
assert.equal(orphan.code, 1);
assert.match(orphan.response.summary, /--native-root requires --state-root/);
const invalidResolve = invoke(['resolve', '--state-root', stateRoot, '--native-root', nativeRoot, '--task-input', 'missing']);
assert.equal(invalidResolve.code, 1);
assert.match(invalidResolve.response.summary, /--native-root is unavailable/);
assert.equal(fs.existsSync(stateRoot), false);
assert.equal(fs.existsSync(nativeRoot), false);
}));