feat(profiles): add opt-in Lean/Full and hybrid Auto selection

This commit is contained in:
haelyra
2026-09-27 16:46:07 -04:00
parent e482e57941
commit 0765f7a2ca
342 changed files with 18212 additions and 209 deletions
+1
View File
@@ -404,6 +404,7 @@ function runHermeticPythonPrePush({
? process.env.PATH
: `${toBashPath(pathBin)}${path.delimiter}${process.env.PATH}`,
HOME: process.env.HOME ?? '',
ECC_PREPUSH_RUN_CHECKS: '1',
ECC_SKIP_GIT_HOOKS: '0',
ECC_SKIP_PREPUSH: '0',
MSYS_NO_PATHCONV: '1',
+4 -3
View File
@@ -604,9 +604,10 @@ async function runTests() {
if (
await test('CLI browser opener handles spawn errors', async () => {
const source = fs.readFileSync(SCRIPT, 'utf8');
assert.match(source, /child\.on\('error'/);
assert.match(source, /child\.unref\(\)/);
const helper = fs.readFileSync(path.join(path.dirname(SCRIPT), 'lib/platform-launch.js'), 'utf8');
assert.match(source, /require\('\.\/lib\/platform-launch'\)/);
assert.match(helper, /child\.on\('error'/);
assert.match(helper, /child\.unref\(\)/);
})
)
passed++;
+2 -1
View File
@@ -1104,7 +1104,7 @@ function runTests() {
assert.strictEqual(fs.readFileSync(scriptsPackagePath, 'utf8'), userScriptsPackage);
const state = readJson(path.join(claudeRoot, 'ecc', 'install-state.json'));
const boundaryPaths = [hooksPackagePath, libPackagePath];
const boundaryPaths = [hooksPackagePath, libPackagePath].map(file => fs.realpathSync(file));
const packageBoundaryOperations = state.operations.filter(operation => (
boundaryPaths.includes(operation.destinationPath)
));
@@ -1191,6 +1191,7 @@ function runTests() {
applyInstallPlan({
targetRoot: path.join(tempDir, 'installed'),
adapter: { id: 'test-install', target: 'test-install' },
installStatePath,
statePreview: {
schemaVersion: 'ecc.install.v1',
+27
View File
@@ -51,6 +51,7 @@ function buildExpectedPublishPaths(repoRoot) {
"scripts/ci/scan-supply-chain-iocs.js",
"scripts/ci/supply-chain-advisory-sources.js",
"scripts/consult.js",
"scripts/profile.js",
"scripts/control-pane.js",
"scripts/dashboard-web.js",
"scripts/discussion-audit.js",
@@ -108,6 +109,9 @@ function buildExpectedPublishPaths(repoRoot) {
"docs/COMMAND-AGENT-MAP.md",
"docs/ROADMAP.md",
"docs/design/ecc-memory-vault.md",
"docs/design/context-profiles.md",
"docs/design/context-carriers.md",
"docs/design/context-profile-delivery.md",
"assets/images/sponsors",
]
const exclusionPaths = [
@@ -185,6 +189,29 @@ function main() {
"scripts/ci/scan-supply-chain-iocs.js",
"scripts/ci/supply-chain-advisory-sources.js",
"scripts/consult.js",
"scripts/profile.js",
"scripts/lib/context-profiles.js",
"scripts/lib/context-pack-registry.js",
"scripts/lib/context-profile-support.js",
"scripts/lib/context-carriers.js",
"scripts/lib/context-selection.js",
"scripts/lib/context-profile-commands.js",
"scripts/lib/context-profile-launch.js",
"scripts/lib/context-profile-proposal.js",
"scripts/lib/context-profile-native.js",
"scripts/lib/context-profile-native-discovery.js",
"scripts/lib/context-profile-native-executable.js",
"scripts/lib/context-profile-store.js",
"scripts/lib/context-profile-store-fs.js",
"schemas/context-profile.schema.json",
"schemas/context-pack-registry.schema.json",
"schemas/context-carrier.schema.json",
"manifests/context-profiles/lean@1.json",
"manifests/context-profiles/full@1.json",
"manifests/context-packs/skill-registry@1.json",
"docs/design/context-profiles.md",
"docs/design/context-carriers.md",
"docs/design/context-profile-delivery.md",
"scripts/control-pane.js",
"scripts/feedback.js",
"scripts/ito.js",
+114
View File
@@ -0,0 +1,114 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const test = require('node:test');
const ROOT = path.resolve(__dirname, '../..');
function withReadOnlyCli(fn) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-carrier-cli-'));
const user = path.join(root, 'user');
const workspace = path.join(root, 'workspace');
fs.mkdirSync(user);
fs.mkdirSync(workspace);
fs.writeFileSync(path.join(user, 'settings.json'), '{"existing":true}\n');
try {
fn(args => spawnSync(process.execPath, [path.join(ROOT, 'scripts/ecc.js'), 'profile', ...args], {
cwd: workspace, encoding: 'utf8', timeout: 30000, maxBuffer: 4 * 1024 * 1024,
env: {
PATH: process.env.PATH, SystemRoot: process.env.SystemRoot,
HOME: user, USERPROFILE: user, CODEX_HOME: path.join(user, '.codex'),
CLAUDE_CONFIG_DIR: path.join(user, '.claude'),
XDG_CONFIG_HOME: path.join(user, 'config'), XDG_STATE_HOME: path.join(user, 'state'),
...(process.env.NODE_V8_COVERAGE ? { NODE_V8_COVERAGE: process.env.NODE_V8_COVERAGE } : {}),
},
}));
} finally {
try {
assert.deepEqual(fs.readdirSync(root).sort(), ['user', 'workspace']);
assert.deepEqual(fs.readdirSync(user), ['settings.json']);
assert.equal(fs.readFileSync(path.join(user, 'settings.json'), 'utf8'), '{"existing":true}\n');
assert.deepEqual(fs.readdirSync(workspace), []);
} finally { fs.rmSync(root, { recursive: true, force: true }); }
}
}
function payload(result) {
assert.equal(result.status, 0, result.stderr || result.stdout);
const value = JSON.parse(result.stdout);
assert.equal(value.status, 'warning');
assert.equal(value.activation, 'unobserved');
assert.equal(value.carrier.active, false);
assert.equal(value.carrier.nativeSupport, 'unobserved');
return value;
}
test('profile help exposes carrier planning without a write command', () => withReadOnlyCli(run => {
const result = run(['--help']);
assert.equal(result.status, 0);
assert.match(result.stdout, /ecc profile carrier/);
assert.match(result.stdout, /read-only/i);
}));
test('default carrier preview is a deterministic Lean Codex proposal', () => withReadOnlyCli(run => {
const first = payload(run(['carrier', '--json']));
assert.deepEqual(payload(run(['carrier', '--json'])), first);
assert.equal(first.carrier.target, 'codex');
assert.equal(first.carrier.profileId, 'lean@1');
assert.equal(first.carrier.selectionMode, 'auto');
assert.equal(first.carrier.selectedIds.length, 3);
assert.equal(first.carrier.status, 'planned');
assert.equal(first.artifacts[0].digest, first.carrier.carrierDigest);
assert.ok(!JSON.stringify(first).includes(ROOT));
}));
test('Full carrier keeps explicit exclusions and manual intent', () => withReadOnlyCli(run => {
const { carrier } = payload(run(['carrier', 'full@1', '--selection', 'manual',
'--exclude', 'skill:python-patterns', '--json']));
assert.equal(carrier.selectionMode, 'manual');
assert.ok(carrier.excludedIds.includes('skill:python-patterns'));
assert.ok(carrier.files.every(file => file.skillId !== 'skill:python-patterns'));
}));
test('every implemented layout remains explicitly native-unobserved', () => withReadOnlyCli(run => {
for (const target of ['claude', 'codex', 'cursor', 'opencode', 'pi']) {
const { carrier } = payload(run(['carrier', '--target', target, '--json']));
assert.equal(carrier.target, target);
assert.equal(carrier.status, 'planned');
assert.ok(carrier.files.length > 0);
}
}));
test('recognized unsupported target returns inventory without generated files', () => withReadOnlyCli(run => {
const { carrier } = payload(run(['carrier', '--target', 'kimi', '--json']));
assert.equal(carrier.status, 'unsupported');
assert.deepEqual(carrier.files, []);
assert.equal(carrier.selectedIds.length, 3);
}));
test('carrier text and dry-run output preserve read-only and unobserved boundaries', () => withReadOnlyCli(run => {
const result = run(['carrier', '--dry-run']);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /carrier/i);
assert.match(result.stdout, /unobserved/i);
assert.match(result.stdout, /planned|proposed/i);
const expected = payload(run(['carrier', 'lean@1', '--target', 'codex', '--json']));
for (const args of [
['--dry-run', 'carrier', 'lean@1', '--target', 'codex', '--json'],
['carrier', 'lean@1', '--target', '--dry-run', 'codex', '--json'],
]) assert.deepEqual(payload(run(args)), expected);
}));
test('carrier rejects unknown targets, write destinations, and hook flags', () => withReadOnlyCli(run => {
for (const args of [['--target', 'unknown'], ['--output', 'user'], ['--hooks', 'strict']]) {
const result = run(['carrier', ...args, '--json']);
assert.equal(result.status, 1);
const error = JSON.parse(result.stdout);
assert.equal(error.status, 'error');
assert.doesNotMatch(error.summary, /Cannot find module|Require stack/);
}
}));
+60
View File
@@ -0,0 +1,60 @@
'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 CLI = path.resolve(__dirname, '../../scripts/ecc.js');
const task = { sessionId: 'stdin-session', taskId: 'stdin-task', revision: 1,
phase: 'implement', explicitIds: ['skill:python-patterns'] };
function invoke(args, input) {
return spawnSync(process.execPath, [CLI, 'profile', ...args, '--json'], {
input, encoding: 'utf8', timeout: 30000, maxBuffer: 1024 * 1024 });
}
test('resolve accepts bounded task JSON on stdin without creating task files', () => {
const result = invoke(['resolve', '--task-input', '-', '--load'], JSON.stringify(task));
assert.equal(result.status, 0, result.stdout);
assert.deepEqual(JSON.parse(result.stdout).selection.loadedIds, ['skill:python-patterns']);
});
test('stdin task JSON rejects overflow, malformed UTF-8, NUL, and invalid JSON', () => {
for (const [input, message] of [[Buffer.alloc(65537, 32), /65536/], [Buffer.from([0xff]), /UTF-8/],
['\0', /UTF-8/], ['{', /JSON/]]) {
const result = invoke(['resolve', '--task-input', '-'], input);
assert.equal(result.status, 1);
assert.match(JSON.parse(result.stdout).summary, message);
}
});
test('malformed task JSON never echoes private input through the CLI envelope', () => {
const secret = 'PRIVATE_TASK_SENTINEL';
const result = invoke(['resolve', '--task-input', '-'], `{"task":"${secret}"`);
assert.equal(result.status, 1);
assert.match(JSON.parse(result.stdout).summary, /valid JSON/);
assert.doesNotMatch(result.stdout + result.stderr, new RegExp(secret));
});
test('start requires both roots, rejects authority flags, and dry-run never prepares a native home', () => {
const parent = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'ecc-start-cli-'));
try {
const stateRoot = path.join(parent, 'state');
const nativeRoot = path.join(parent, 'native');
for (const [args, message] of [[[], /requires --state-root/],
[['--state-root', stateRoot], /requires --native-root/],
[['--state-root', stateRoot, '--native-root', nativeRoot, '--dangerously-bypass-approvals-and-sandbox'], /Unknown argument/]]) {
const result = invoke(['start', ...args]);
assert.equal(result.status, 1);
assert.match(JSON.parse(result.stdout).summary, message);
}
assert.equal(invoke(['set', 'lean', '--state-root', stateRoot]).status, 0);
const jsonStart = invoke(['start', '--state-root', stateRoot, '--native-root', nativeRoot]);
assert.equal(jsonStart.status, 1);
assert.match(JSON.parse(jsonStart.stdout).summary, /--json requires --dry-run/);
const result = invoke(['start', '--state-root', stateRoot, '--native-root', nativeRoot, '--dry-run']);
assert.equal(result.status, 0, result.stdout);
assert.equal(JSON.parse(result.stdout).interactive.status, 'proposed');
assert.equal(fs.existsSync(nativeRoot), false);
} finally { fs.rmSync(parent, { recursive: true, force: true }); }
});
+211
View File
@@ -0,0 +1,211 @@
'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);
}));
+206
View File
@@ -0,0 +1,206 @@
/** 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 };
+43 -12
View File
@@ -78,6 +78,35 @@ function runSetup(fixture, args, options = {}) {
function quoteShellArgument(value) {
return `'${String(value).replace(/'/g, `'\\''`)}'`;
}
// Answer only after the PTY displays a prompt. Fixed-delay pipes can deliver
// blank defaults and EOF before the wizard creates its readline interface.
function driveInteractiveTerminal() {
const { spawn } = require('child_process');
const { pseudoTerminalCommand, answers } = JSON.parse(process.argv[1]);
// Node pipes are sockets on macOS; script requires a real pipe for stdin.
const child = spawn('sh', ['-c', `cat | ${pseudoTerminalCommand}`], { stdio: ['pipe', 'pipe', 'pipe'] });
let pending = '';
let answerIndex = 0;
child.stdout.on('data', chunk => {
process.stdout.write(chunk);
pending += chunk.toString('utf8');
const prompt = /Choose(?: \[\d+\])?: |\[y\/N\] /.exec(pending);
if (!prompt) return;
pending = pending.slice(prompt.index + prompt[0].length);
if (answerIndex >= answers.length) { child.stdin.end(); return; }
const answer = answers[answerIndex++];
child.stdin.write(answer === '\u0004' ? answer : `${answer}\n`);
if (answerIndex === answers.length) child.stdin.end();
});
child.stderr.on('data', chunk => process.stderr.write(chunk));
child.stdin.on('error', error => {
if (error.code !== 'EPIPE') { process.stderr.write(error.message); process.exitCode = 1; }
});
child.on('error', error => { process.stderr.write(error.message); process.exitCode = 1; });
child.on('close', code => { process.exitCode = code ?? 1; });
}
function runInteractiveEccSetup(fixture, options = {}) {
if (process.platform === 'win32') {
return null;
@@ -85,12 +114,18 @@ function runInteractiveEccSetup(fixture, options = {}) {
const args = options.args || ['--dry-run'];
const answers = options.answers || ['3', '3'];
const command = [
const setupCommand = [
process.execPath,
eccScript,
'setup',
...args,
];
const command = options.delayedStartup
? [process.execPath, '-e', `setTimeout(() => {
const result = require('child_process').spawnSync(process.argv[1], process.argv.slice(2), { stdio: 'inherit' });
process.exitCode = result.status ?? 1;
}, 1250);`, ...setupCommand]
: setupCommand;
const scriptArgs = process.platform === 'darwin'
? ['-q', '-e', '/dev/null', ...command]
: [
@@ -100,16 +135,11 @@ function runInteractiveEccSetup(fixture, options = {}) {
command.map(quoteShellArgument).join(' '),
'/dev/null',
];
const pseudoTerminalCommand = ['script', ...scriptArgs]
.map(quoteShellArgument)
.join(' ');
const answerCommands = answers
.map(answer => `sleep 0.5; printf '%s\\n' ${quoteShellArgument(answer)}`)
.join('; ');
return spawnSync('sh', [
'-c',
`(${answerCommands}; sleep 0.1) | ${pseudoTerminalCommand}`,
const pseudoTerminalCommand = ['script', ...scriptArgs].map(quoteShellArgument).join(' ');
return spawnSync(process.execPath, [
'-e',
`(${driveInteractiveTerminal.toString()})();`,
JSON.stringify({ pseudoTerminalCommand, answers }),
], {
cwd: fixture.projectRoot,
env: {
@@ -386,8 +416,8 @@ test('setup automatically migrates an existing install to the selected scope and
const calls = readCalls(fixture);
assert.ok(calls.some(argv => (
argv.join(' ') === 'plugin install ecc@ecc --scope user'
+ ' --config hooks_enabled=true --config hook_profile=minimal'
)));
assert.ok(calls.every(argv => !argv.includes('--config')));
assert.ok(calls.some(argv => (
argv.join(' ') === 'plugin uninstall ecc@ecc --scope local --keep-data'
)));
@@ -914,6 +944,7 @@ test('interactive defaults preserve an existing install scope and hook preferenc
const result = runInteractiveEccSetup(fixture, {
args: ['--dry-run'],
answers: ['', ''],
delayedStartup: true,
});
assert.ifError(result.error);
assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`);