/** 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 };