Files
ECC/tests/scripts/ecc.test.js
T
59b74901c4 fix(install): preserve user files and honor uninstall previews
Generalize PR #2981 ownership protection to every managed target. Reject mismatched target state and preserve files that appear during writes or failed-install checkpoints. Keep prior hashes for managed files a failed attempt never writes.

Integrate PR #2980 preview wording and global dry-run propagation, with PR #2956 fail-closed environment validation and CLI/legacy regression coverage.

Fixes #2964. Fixes #2952.

Co-authored-by: ilkmajans-cpu <ilkmajans-cpu@users.noreply.github.com>

Co-authored-by: wellkilo <wellkilo@foxmail.com>
2026-09-07 16:40:47 -04:00

367 lines
14 KiB
JavaScript

/**
* Tests for scripts/ecc.js
*/
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 { createInstallState, writeInstallState } = require('../../scripts/lib/install-state');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'ecc.js');
function runCli(args, options = {}) {
const envOverrides = {
...(options.env || {}),
};
const inheritedEnv = Object.fromEntries(
Object.entries(process.env).filter(([key]) => key !== 'ECC_DRY_RUN')
);
const homeAlias = typeof envOverrides.HOME === 'string' && !('USERPROFILE' in envOverrides)
? { USERPROFILE: envOverrides.HOME }
: typeof envOverrides.USERPROFILE === 'string' && !('HOME' in envOverrides)
? { HOME: envOverrides.USERPROFILE }
: {};
const env = { ...inheritedEnv, ...envOverrides, ...homeAlias };
return spawnSync('node', [SCRIPT, ...args], {
encoding: 'utf8',
cwd: options.cwd || process.cwd(),
maxBuffer: 10 * 1024 * 1024,
env,
});
}
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function parseJson(stdout) {
return JSON.parse(stdout.trim());
}
function runTest(name, fn) {
try {
fn();
console.log(` ✓ ${name}`);
return true;
} catch (error) {
console.log(` ✗ ${name}`);
console.error(` ${error.message}`);
return false;
}
}
function main() {
console.log('\n=== Testing ecc.js ===\n');
let passed = 0;
let failed = 0;
const tests = [
['shows top-level help', () => {
const result = runCli(['--help']);
assert.strictEqual(result.status, 0);
assert.match(result.stdout, /ECC selective-install CLI/);
assert.match(result.stdout, /catalog/);
assert.match(result.stdout, /list-installed/);
assert.match(result.stdout, /doctor/);
assert.match(result.stdout, /auto-update/);
assert.match(result.stdout, /consult/);
assert.match(result.stdout, /control-pane/);
assert.match(result.stdout, /loop-status/);
assert.match(result.stdout, /work-items/);
assert.match(result.stdout, /platform-audit/);
assert.match(result.stdout, /security-ioc-scan/);
assert.match(result.stdout, /feedback/);
}],
['delegates explicit install command', () => {
const homeDir = createTempDir('ecc-cli-install-home-');
try {
const result = runCli(['install', '--dry-run', '--json', 'typescript'], {
env: {
CLAUDE_CONFIG_DIR: path.join(homeDir, '.claude'),
HOME: homeDir,
},
});
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.dryRun, true);
assert.strictEqual(payload.plan.mode, 'legacy-compat');
assert.deepStrictEqual(payload.plan.legacyLanguages, ['typescript']);
assert.ok(payload.plan.selectedModuleIds.includes('framework-language'));
} finally {
fs.rmSync(homeDir, { force: true, recursive: true });
}
}],
['routes implicit top-level args to install', () => {
const homeDir = createTempDir('ecc-cli-install-home-');
try {
const result = runCli(['--dry-run', '--json', 'typescript'], {
env: {
CLAUDE_CONFIG_DIR: path.join(homeDir, '.claude'),
HOME: homeDir,
},
});
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.dryRun, true);
assert.strictEqual(payload.plan.mode, 'legacy-compat');
assert.deepStrictEqual(payload.plan.legacyLanguages, ['typescript']);
assert.ok(payload.plan.selectedModuleIds.includes('framework-language'));
} finally {
fs.rmSync(homeDir, { force: true, recursive: true });
}
}],
['delegates plan command', () => {
const result = runCli(['plan', '--list-profiles', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.ok(Array.isArray(payload.profiles));
assert.ok(payload.profiles.length > 0);
}],
['delegates catalog command', () => {
const result = runCli(['catalog', 'show', 'framework:nextjs', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.id, 'framework:nextjs');
assert.deepStrictEqual(payload.moduleIds, ['framework-language']);
}],
['delegates consult command', () => {
const result = runCli(['consult', 'security', 'reviews', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.schemaVersion, 'ecc.consult.v1');
assert.strictEqual(payload.matches[0].componentId, 'capability:security');
}],
['supports help for the control-pane subcommand', () => {
const result = runCli(['help', 'control-pane']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Usage:/);
assert.match(result.stdout, /control-pane/);
}],
['delegates lifecycle commands', () => {
const homeDir = createTempDir('ecc-cli-home-');
const projectRoot = createTempDir('ecc-cli-project-');
const result = runCli(['list-installed', '--json'], {
cwd: projectRoot,
env: { HOME: homeDir },
});
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.deepStrictEqual(payload.records, []);
}],
['keeps uninstall read-only when global --dry-run precedes the command', () => {
const homeDir = createTempDir('ecc-cli-uninstall-home-');
const projectRoot = createTempDir('ecc-cli-uninstall-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const statePath = path.join(targetRoot, 'ecc-install-state.json');
const managedPath = path.join(targetRoot, 'managed-rule.md');
const managedContent = 'managed\n';
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(managedPath, managedContent);
writeInstallState(statePath, createInstallState({
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
targetRoot,
installStatePath: statePath,
request: {
profile: null,
modules: [],
includeComponents: [],
excludeComponents: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: {
selectedModules: ['legacy-cursor-install'],
skippedModules: [],
},
source: {
repoVersion: null,
repoCommit: null,
manifestVersion: 1,
},
operations: [{
kind: 'copy-file',
moduleId: 'rules-core',
sourceRelativePath: 'rules/common/coding-style.md',
destinationPath: managedPath,
strategy: 'preserve-relative-path',
ownership: 'managed',
scaffoldOnly: false,
contentSha256: crypto.createHash('sha256').update(managedContent).digest('hex'),
}],
}));
const jsonResult = runCli(['--dry-run', 'uninstall', '--target', 'cursor', '--json'], {
cwd: projectRoot,
env: { HOME: homeDir },
});
assert.strictEqual(jsonResult.status, 0, jsonResult.stderr);
const preview = parseJson(jsonResult.stdout);
assert.strictEqual(preview.dryRun, true);
assert.strictEqual(preview.results[0].status, 'planned');
assert.deepStrictEqual(
preview.results[0].plannedRemovals.map(candidate => fs.realpathSync(candidate)).sort(),
[managedPath, statePath].map(candidate => fs.realpathSync(candidate)).sort()
);
const humanResult = runCli(['--dry-run', 'uninstall', '--target', 'cursor'], {
cwd: projectRoot,
env: { HOME: homeDir },
});
assert.strictEqual(humanResult.status, 0, humanResult.stderr);
assert.match(humanResult.stdout, /Status: WOULD UNINSTALL/);
assert.match(humanResult.stdout, /Would remove: 2/);
assert.doesNotMatch(humanResult.stdout, /Status: UNINSTALLED|Removed paths:/);
assert.ok(fs.existsSync(managedPath), 'global dry-run must preserve managed files');
assert.ok(fs.existsSync(statePath), 'global dry-run must preserve install-state');
} finally {
fs.rmSync(homeDir, { force: true, recursive: true });
fs.rmSync(projectRoot, { force: true, recursive: true });
}
}],
['delegates auto-update command', () => {
const homeDir = createTempDir('ecc-cli-home-');
const projectRoot = createTempDir('ecc-cli-project-');
const result = runCli(['auto-update', '--dry-run', '--json'], {
cwd: projectRoot,
env: { HOME: homeDir },
});
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.deepStrictEqual(payload.results, []);
}],
['delegates session-inspect command', () => {
const homeDir = createTempDir('ecc-cli-home-');
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
fs.writeFileSync(
path.join(sessionsDir, '2026-03-13-a1b2c3d4-session.tmp'),
'# ECC Session\n\n**Branch:** feat/ecc-cli\n'
);
const result = runCli(['session-inspect', 'claude:latest'], {
env: { HOME: homeDir },
});
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.adapterId, 'claude-history');
assert.strictEqual(payload.workers[0].branch, 'feat/ecc-cli');
}],
['delegates loop-status command', () => {
const homeDir = createTempDir('ecc-cli-home-');
const transcriptDir = path.join(homeDir, '.claude', 'projects', '-tmp-ecc');
fs.mkdirSync(transcriptDir, { recursive: true });
fs.writeFileSync(
path.join(transcriptDir, 'session-loop.jsonl'),
JSON.stringify({
timestamp: '2026-04-30T09:00:00.000Z',
sessionId: 'session-loop',
message: {
role: 'assistant',
content: [
{
type: 'tool_use',
id: 'toolu_loop',
name: 'ScheduleWakeup',
input: { delaySeconds: 300 },
},
],
},
}) + '\n'
);
const result = runCli(['loop-status', '--home', homeDir, '--now', '2026-04-30T10:00:00.000Z', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.schemaVersion, 'ecc.loop-status.v1');
assert.strictEqual(payload.sessions[0].sessionId, 'session-loop');
}],
['supports help for a subcommand', () => {
const result = runCli(['help', 'repair']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Usage: node scripts\/repair\.js/);
}],
['delegates feedback command', () => {
const result = runCli(['feedback', '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.strictEqual(payload.schemaVersion, 'ecc.feedback.v1');
assert.strictEqual(payload.diagnosticsUploaded, false);
}],
['supports help for the auto-update subcommand', () => {
const result = runCli(['help', 'auto-update']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Usage: node scripts\/auto-update\.js/);
}],
['supports help for the catalog subcommand', () => {
const result = runCli(['help', 'catalog']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /node scripts\/catalog\.js show <component-id>/);
}],
['supports help for the consult subcommand', () => {
const result = runCli(['help', 'consult']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /node scripts\/consult\.js "security reviews"/);
}],
['supports help for the work-items subcommand', () => {
const result = runCli(['help', 'work-items']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /node scripts\/work-items\.js upsert/);
}],
['supports help for the platform-audit subcommand', () => {
const result = runCli(['help', 'platform-audit']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Usage: node scripts\/platform-audit\.js/);
}],
['supports help for the security-ioc-scan subcommand', () => {
const result = runCli(['help', 'security-ioc-scan']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Usage: node scripts\/ci\/scan-supply-chain-iocs\.js/);
}],
['delegates security-ioc-scan command', () => {
const projectRoot = createTempDir('ecc-cli-ioc-scan-');
fs.writeFileSync(
path.join(projectRoot, 'package.json'),
JSON.stringify({ dependencies: { leftpad: '1.0.0' } }, null, 2)
);
const result = runCli(['security-ioc-scan', '--root', projectRoot, '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.deepStrictEqual(payload.findings, []);
}],
['fails on unknown commands instead of treating them as installs', () => {
const result = runCli(['bogus']);
assert.strictEqual(result.status, 1);
assert.match(result.stderr, /Unknown command: bogus/);
}],
['fails on unknown help subcommands', () => {
const result = runCli(['help', 'bogus']);
assert.strictEqual(result.status, 1);
assert.match(result.stderr, /Unknown command: bogus/);
}],
];
for (const [name, fn] of tests) {
if (runTest(name, fn)) {
passed += 1;
} else {
failed += 1;
}
}
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
main();