mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-18 07:37:59 +02:00
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>
367 lines
14 KiB
JavaScript
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();
|