mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-28 12:35:13 +02:00
207 lines
9.2 KiB
JavaScript
207 lines
9.2 KiB
JavaScript
/** Read-only context profile journeys, exercised through the shipped CLI. */
|
|
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { spawnSync } = require('child_process');
|
|
|
|
const ROOT = path.resolve(__dirname, '../..');
|
|
const CLI = path.join(ROOT, 'scripts/ecc.js');
|
|
const PROFILE = path.join(ROOT, 'scripts/profile.js');
|
|
|
|
function snapshot(directory) {
|
|
return fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))
|
|
.flatMap(entry => {
|
|
const file = path.join(directory, entry.name);
|
|
if (entry.isSymbolicLink()) return [`${entry.name}:link:${fs.readlinkSync(file)}`];
|
|
return entry.isDirectory()
|
|
? snapshot(file).map(item => `${entry.name}/${item}`)
|
|
: [`${entry.name}:${crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')}`];
|
|
});
|
|
}
|
|
|
|
function withFixture(fn) {
|
|
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-profile-cli-'));
|
|
let before;
|
|
try {
|
|
const userDirectory = path.join(fixture, 'user');
|
|
const workspace = path.join(fixture, 'workspace');
|
|
fs.mkdirSync(userDirectory);
|
|
fs.mkdirSync(workspace);
|
|
fs.writeFileSync(path.join(userDirectory, 'settings.json'), '{"keep":"user preference"}\n');
|
|
fs.writeFileSync(path.join(workspace, 'owned.txt'), 'existing user work\n');
|
|
before = snapshot(fixture);
|
|
const run = (args, direct = false) => spawnSync(process.execPath,
|
|
[direct ? PROFILE : CLI, ...(direct ? [] : ['profile']), ...args], {
|
|
cwd: workspace, encoding: 'utf8', timeout: 30_000, maxBuffer: 4 * 1024 * 1024,
|
|
env: {
|
|
PATH: process.env.PATH, SystemRoot: process.env.SystemRoot,
|
|
HOME: userDirectory, USERPROFILE: userDirectory,
|
|
XDG_CONFIG_HOME: path.join(userDirectory, 'config'),
|
|
XDG_STATE_HOME: path.join(userDirectory, 'state'),
|
|
CLAUDE_CONFIG_DIR: path.join(userDirectory, '.claude'),
|
|
CODEX_HOME: path.join(userDirectory, '.codex'),
|
|
...(process.env.NODE_V8_COVERAGE ? { NODE_V8_COVERAGE: process.env.NODE_V8_COVERAGE } : {}),
|
|
},
|
|
});
|
|
fn(run);
|
|
} finally {
|
|
try {
|
|
if (before) assert.deepStrictEqual(snapshot(fixture), before,
|
|
'inspection must preserve user and workspace files, including failure paths');
|
|
} finally {
|
|
fs.rmSync(fixture, { recursive: true, force: true });
|
|
}
|
|
}
|
|
}
|
|
|
|
function success(result) {
|
|
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
|
const payload = JSON.parse(result.stdout);
|
|
assert.ok(['success', 'warning'].includes(payload.status));
|
|
assert.strictEqual(typeof payload.summary, 'string');
|
|
assert.ok(Array.isArray(payload.next_actions));
|
|
assert.ok(Array.isArray(payload.artifacts));
|
|
return payload;
|
|
}
|
|
|
|
const tests = [
|
|
['the dispatcher exposes read-only profile help', () => withFixture(run => {
|
|
const result = run(['--help']);
|
|
assert.strictEqual(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /read-only/i);
|
|
for (const command of ['show', 'preview', 'explain']) assert.ok(result.stdout.includes(command));
|
|
})],
|
|
['show lists versioned definitions without claiming an active installation', () => withFixture(run => {
|
|
const result = success(run(['show', '--json']));
|
|
assert.deepStrictEqual(result.profiles.map(profile => profile.id).sort(), ['full@1', 'lean@1']);
|
|
assert.strictEqual(result.activation, 'unobserved');
|
|
})],
|
|
['show reads one profile definition through the direct packaged entrypoint', () => withFixture(run => {
|
|
const result = success(run(['show', 'lean@1', '--json'], true));
|
|
assert.strictEqual(result.profile.id, 'lean@1');
|
|
})],
|
|
['Lean preview is deterministic and reports no observed activation', () => withFixture(run => {
|
|
const args = ['preview', 'lean@1', '--target', 'codex', '--selection', 'auto', '--json'];
|
|
const first = run(args);
|
|
const payload = success(first);
|
|
assert.deepStrictEqual(JSON.parse(run(args).stdout), payload);
|
|
assert.strictEqual(payload.activation, 'unobserved');
|
|
assert.ok(payload.plan);
|
|
assert.ok(!first.stdout.includes(ROOT), 'portable output must omit local checkout path');
|
|
})],
|
|
['Full remains inspectable with explicit manual selection', () => withFixture(run => {
|
|
assert.ok(success(run(['preview', 'full@1', '--target', 'claude', '--selection', 'manual', '--json'])).plan);
|
|
})],
|
|
['explicit includes and exclusions remain inspection only', () => withFixture(run => {
|
|
success(run(['preview', '--target', 'codex', '--include', 'skill:security-review',
|
|
'--exclude', 'skill:python-patterns', '--json']));
|
|
})],
|
|
['exact-ID explanation includes an entry and never invokes the skill', () => withFixture(run => {
|
|
const payload = success(run(['explain', 'skill:security-review', '--target', 'codex', '--json']));
|
|
assert.strictEqual(payload.entry.id, 'skill:security-review');
|
|
assert.strictEqual(payload.activation, 'unobserved');
|
|
})],
|
|
['text output identifies estimates and unobserved runtime state', () => withFixture(run => {
|
|
const result = run(['preview', '--target', 'codex']);
|
|
assert.strictEqual(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /estimate/i);
|
|
assert.match(result.stdout, /unobserved/i);
|
|
})],
|
|
['text error output renders terminal controls inert', () => withFixture(run => {
|
|
const control = String.fromCharCode(27);
|
|
const result = run(['explain', `skill:unknown${control}]52;c;example${String.fromCharCode(7)}`]);
|
|
assert.strictEqual(result.status, 1);
|
|
assert.ok(!result.stderr.includes(control), 'terminal escape must not reach the text output');
|
|
assert.match(result.stderr, /\\u001b/);
|
|
})],
|
|
['global dry-run remains compatible with profile inspection', () => withFixture(run => {
|
|
success(run(['preview', '--target', 'codex', '--dry-run', '--json']));
|
|
})],
|
|
['global dry-run is ignored at every argument position without changing parsed controls', () => {
|
|
const { parseArgs } = require('../../scripts/profile');
|
|
for (const args of [
|
|
['show', 'lean@1', '--json'],
|
|
['preview', 'lean@1', '--target', 'codex', '--selection', 'auto',
|
|
'--include', 'skill:security-review', '--exclude', 'skill:python-patterns', '--json'],
|
|
['explain', 'skill:ecc-guide', '--target', 'codex', '--json'],
|
|
]) {
|
|
const expected = parseArgs(args);
|
|
for (let index = 0; index <= args.length; index++) {
|
|
const invocation = [...args.slice(0, index), '--dry-run', ...args.slice(index)];
|
|
const before = [...invocation];
|
|
assert.deepStrictEqual(parseArgs(invocation), expected, invocation.join(' '));
|
|
assert.deepStrictEqual(invocation, before, 'parsing must preserve caller arguments');
|
|
}
|
|
assert.deepStrictEqual(parseArgs(['--dry-run', ...args, '--dry-run']), expected);
|
|
}
|
|
}],
|
|
['package includes the direct profile entrypoint and public schemas', () => {
|
|
const { files } = require('../../package.json');
|
|
assert.ok(files.includes('scripts/profile.js'));
|
|
assert.ok(files.includes('schemas/'));
|
|
assert.ok(files.includes('manifests/'));
|
|
}],
|
|
];
|
|
|
|
for (const args of [
|
|
['show', 'lean@1', '--json'],
|
|
['preview', 'lean@1', '--target', 'codex', '--selection', 'auto', '--json'],
|
|
['explain', 'skill:ecc-guide', '--target', 'codex', '--json'],
|
|
]) {
|
|
tests.push([`leading global dry-run preserves ${args[0]} through both CLI entrypoints`, () => withFixture(run => {
|
|
const expected = success(run(args));
|
|
for (const direct of [false, true]) {
|
|
const observed = success(run(['--dry-run', ...args], direct));
|
|
assert.deepStrictEqual(observed, expected);
|
|
assert.strictEqual(observed.activation, 'unobserved');
|
|
}
|
|
})]);
|
|
}
|
|
|
|
for (const args of [
|
|
['use', 'lean@1'],
|
|
['--dry-run', 'use', 'lean@1'],
|
|
['show', 'unknown@1'],
|
|
['preview', '--target', 'unknown-host'],
|
|
['preview', '--selection', 'eager'],
|
|
['preview', '--target'],
|
|
['preview', '--target', '--json'],
|
|
['preview', '--target', 'codex', '--target', 'claude'],
|
|
['preview', '--include', 'skill:missing-workflow'],
|
|
['preview', '--include', '../../outside'],
|
|
['preview', '--hooks', 'strict'],
|
|
['--dry-run', 'preview', '--hooks', 'strict'],
|
|
['show', '--include', 'skill:security-review'],
|
|
['explain'],
|
|
['explain', 'skill:missing-workflow'],
|
|
['explain', 'skill:ecc-guide', 'extra'],
|
|
]) {
|
|
tests.push([`rejects unsupported or malformed input: ${args.join(' ')}`, () => withFixture(run => {
|
|
const result = run([...args, '--json']);
|
|
assert.notStrictEqual(result.status, 0);
|
|
const payload = JSON.parse(result.stdout);
|
|
assert.strictEqual(payload.status, 'error');
|
|
assert.doesNotMatch(payload.summary, /Cannot find module|Require stack/,
|
|
'validation must fail for the request, not a missing implementation');
|
|
assert.ok(payload.next_actions.length > 0);
|
|
assert.strictEqual(payload.activation, 'unobserved');
|
|
})]);
|
|
}
|
|
|
|
function main() {
|
|
let passed = 0;
|
|
for (const [name, test] of tests) {
|
|
try { test(); passed++; console.log(` PASS ${name}`); }
|
|
catch (error) { console.error(` FAIL ${name}: ${error.message}`); }
|
|
}
|
|
console.log(`\nPassed: ${passed}\nFailed: ${tests.length - passed}`);
|
|
process.exitCode = passed === tests.length ? 0 : 1;
|
|
}
|
|
|
|
if (require.main === module) main();
|
|
module.exports = { main };
|