Files
ECC/tests/ci/packed-artifact-lifecycle.js

513 lines
18 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const assert = require('assert');
const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { pathToFileURL } = require('url');
const { spawnSync } = require('child_process');
const PACKAGE_NAME = 'ecc-universal';
const HASH_PATTERN = /^[a-f0-9]{64}$/i;
const PACKAGE_PATH_PATTERN = /^release-artifacts\/ecc-universal-[0-9A-Za-z.+-]+\.tgz$/;
function parseEnvironment(environment = process.env, cwd = process.cwd()) {
const packageValue = environment.ECC_RELEASE_PACKAGE;
const hashValue = environment.ECC_RELEASE_SHA256;
if (!packageValue) {
throw new Error('ECC_RELEASE_PACKAGE must name the downloaded release .tgz');
}
if (!PACKAGE_PATH_PATTERN.test(String(packageValue))) {
throw new Error('ECC_RELEASE_PACKAGE must name one ECC .tgz under release-artifacts');
}
if (!HASH_PATTERN.test(hashValue || '')) {
throw new Error('ECC_RELEASE_SHA256 must be a 64-character SHA-256 digest');
}
return {
packagePath: path.resolve(cwd, packageValue),
expectedSha256: hashValue.toLowerCase(),
};
}
function assertDownloadedArtifact(packagePath, cwd) {
const artifactRoot = path.resolve(cwd, 'release-artifacts');
const packageStat = fs.lstatSync(packagePath);
if (!packageStat.isFile() || packageStat.isSymbolicLink()) {
throw new Error('Release package must be a regular, non-symlink file');
}
const realArtifactRoot = fs.realpathSync(artifactRoot);
const realPackagePath = fs.realpathSync(packagePath);
const relativePath = path.relative(realArtifactRoot, realPackagePath);
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
throw new Error('Release package escapes release-artifacts');
}
const archives = fs.readdirSync(realArtifactRoot).filter(name => name.endsWith('.tgz'));
if (archives.length !== 1 || archives[0] !== path.basename(realPackagePath)) {
throw new Error('Expected exactly one downloaded release archive');
}
}
function hashFile(filePath) {
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
}
function assertHash(actualSha256, expectedSha256) {
if (actualSha256 !== expectedSha256) {
throw new Error(
`Downloaded artifact SHA-256 ${actualSha256} does not match packed artifact ${expectedSha256}`
);
}
}
function createLifecycleEnvironment(baseEnvironment, homeDir) {
const environment = {};
const inheritedNames = [
'CI',
'ComSpec',
'LANG',
'LC_ALL',
'NO_COLOR',
'PATH',
'Path',
'PATHEXT',
'SystemRoot',
'TEMP',
'TMP',
'TMPDIR',
'WINDIR',
];
for (const name of inheritedNames) {
if (baseEnvironment[name] !== undefined) {
environment[name] = baseEnvironment[name];
}
}
return {
...environment,
HOME: homeDir,
USERPROFILE: homeDir,
APPDATA: path.join(homeDir, 'AppData', 'Roaming'),
LOCALAPPDATA: path.join(homeDir, 'AppData', 'Local'),
XDG_CONFIG_HOME: path.join(homeDir, '.config'),
XDG_DATA_HOME: path.join(homeDir, '.local', 'share'),
NPM_CONFIG_CACHE: path.join(homeDir, '.npm'),
NPM_CONFIG_USERCONFIG: path.join(homeDir, '.npmrc'),
};
}
function runProcess(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: options.cwd,
env: options.env,
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024,
stdio: ['ignore', 'pipe', 'pipe'],
});
if (result.error) {
throw result.error;
}
const expectedStatus = options.expectedStatus ?? 0;
if (result.status !== expectedStatus) {
throw new Error([
`${options.label || command} exited ${result.status}, expected ${expectedStatus}.`,
result.stdout ? `stdout:\n${result.stdout}` : '',
result.stderr ? `stderr:\n${result.stderr}` : '',
].filter(Boolean).join('\n'));
}
return result;
}
function getNpmExecInvocation(publicArgs, environment, platform = process.platform) {
const npmArgs = ['exec', '--offline', '--yes=false', '--', ...publicArgs];
if (platform !== 'win32') {
return { command: 'npm', args: npmArgs };
}
const commandParts = ['npm', ...npmArgs];
for (const part of commandParts) {
if (!/^[A-Za-z0-9_.=+,:/-]+$/.test(part)) {
throw new Error(`Unsafe npm exec argument for Windows lifecycle: ${part}`);
}
}
return {
command: environment.ComSpec || 'cmd.exe',
args: ['/d', '/s', '/c', commandParts.join(' ')],
};
}
function installPackage(projectDir, packagePath, environment) {
const projectManifest = {
name: 'ecc-packed-artifact-lifecycle',
version: '1.0.0',
private: true,
dependencies: {
[PACKAGE_NAME]: pathToFileURL(packagePath).href,
},
};
fs.writeFileSync(
path.join(projectDir, 'package.json'),
`${JSON.stringify(projectManifest, null, 2)}\n`,
'utf8'
);
if (process.platform === 'win32') {
runProcess(
environment.ComSpec || 'cmd.exe',
['/d', '/s', '/c', 'npm install --no-audit --no-fund'],
{ cwd: projectDir, env: environment, label: 'npm install packed artifact' }
);
return;
}
runProcess('npm', ['install', '--no-audit', '--no-fund'], {
cwd: projectDir,
env: environment,
label: 'npm install packed artifact',
});
}
function parseJsonOutput(result, label) {
try {
return JSON.parse(result.stdout);
} catch (error) {
throw new Error(`${label} did not emit valid JSON: ${error.message}\n${result.stdout}`);
}
}
function resolveManagedExistingPath(destinationPath, cursorRoot) {
const normalizedRoot = fs.realpathSync(cursorRoot);
const lexicalPath = path.resolve(destinationPath);
const lexicalRelativePath = path.relative(normalizedRoot, lexicalPath);
if (
lexicalRelativePath === ''
|| lexicalRelativePath.startsWith('..')
|| path.isAbsolute(lexicalRelativePath)
|| !fs.existsSync(lexicalPath)
) {
return null;
}
const pathStat = fs.lstatSync(lexicalPath);
if (pathStat.isSymbolicLink()) {
throw new Error(`Managed lifecycle path must not be a symlink: ${lexicalPath}`);
}
const realPath = fs.realpathSync(lexicalPath);
const realRelativePath = path.relative(normalizedRoot, realPath);
if (realRelativePath.startsWith('..') || path.isAbsolute(realRelativePath)) {
throw new Error(`Managed lifecycle path escapes Cursor root: ${lexicalPath}`);
}
return { path: realPath, stat: pathStat };
}
function getManagedOperationSnapshot(state, cursorRoot) {
const snapshot = [];
for (const operation of state.operations) {
if (operation.ownership !== 'managed' || typeof operation.destinationPath !== 'string') {
continue;
}
const resolved = resolveManagedExistingPath(operation.destinationPath, cursorRoot);
if (resolved) {
snapshot.push({ path: resolved.path, isFile: resolved.stat.isFile() });
}
}
return [...new Map(snapshot.map(entry => [entry.path, entry])).values()]
.sort((left, right) => left.path.localeCompare(right.path));
}
function getOperationLedger(state) {
return state.operations.map(operation => ({
kind: operation.kind,
moduleId: operation.moduleId,
sourceRelativePath: operation.sourceRelativePath || null,
destinationPath: operation.destinationPath,
strategy: operation.strategy,
ownership: operation.ownership,
contentSha256: operation.contentSha256 || null,
}));
}
function findDriftCandidate(state, cursorRoot) {
const operation = state.operations.find(candidate => {
if (candidate.kind !== 'copy-file' || typeof candidate.destinationPath !== 'string') {
return false;
}
const resolved = resolveManagedExistingPath(candidate.destinationPath, cursorRoot);
return resolved && resolved.stat.isFile();
});
assert.ok(operation, 'installed state must contain a managed Cursor file that can be drifted');
return resolveManagedExistingPath(operation.destinationPath, cursorRoot).path;
}
function runLifecycle(options) {
assert.ok(fs.existsSync(options.packagePath), `release package does not exist: ${options.packagePath}`);
assertDownloadedArtifact(options.packagePath, process.cwd());
assertHash(hashFile(options.packagePath), options.expectedSha256);
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-packed-lifecycle-'));
const homeDir = path.join(tempRoot, 'home');
const projectDir = path.join(tempRoot, 'project');
fs.mkdirSync(homeDir, { recursive: true });
fs.mkdirSync(projectDir, { recursive: true });
const environment = createLifecycleEnvironment(process.env, homeDir);
try {
installPackage(projectDir, options.packagePath, environment);
const cursorRoot = path.join(projectDir, '.cursor');
const statePath = path.join(cursorRoot, 'ecc-install-state.json');
const sentinelPath = path.join(cursorRoot, 'user-sentinel.txt');
fs.mkdirSync(cursorRoot, { recursive: true });
fs.writeFileSync(sentinelPath, 'keep this user file\n', 'utf8');
const runPublicCli = (publicArgs, commandOptions = {}) => {
const invocation = getNpmExecInvocation(publicArgs, environment);
return runProcess(invocation.command, invocation.args, {
cwd: projectDir,
env: environment,
label: `npm exec -- ${publicArgs.join(' ')}`,
...commandOptions,
});
};
const runCli = (args, commandOptions = {}) => runPublicCli(
['ecc', ...args],
commandOptions
);
const setupHelp = runPublicCli(['ecc-universal', 'setup', '--help']);
assert.match(setupHelp.stdout, /ECC guided setup/);
assert.match(setupHelp.stdout, /ecc setup --mode claude-plugin/);
const itoInstallArgs = [
'install',
'--profile', 'core',
'--with', 'capability:ito-compute',
'--with', 'capability:prediction-markets',
'--target', 'cursor',
'--json',
];
parseJsonOutput(
runCli(itoInstallArgs),
'initial Itô install'
);
assert.ok(fs.existsSync(statePath), 'initial install must write Cursor install-state');
const initialState = JSON.parse(fs.readFileSync(statePath, 'utf8'));
const initialLedger = getOperationLedger(initialState);
assert.ok(
initialState.operations.some(operation => operation.moduleId === 'ito-compute'),
'installed ledger must include the Itô compute module'
);
assert.ok(
initialState.operations.some(operation => operation.moduleId === 'prediction-market-skills'),
'installed ledger must include the Itô baskets module'
);
for (const relativePath of [
'skills/ito-baskets/SKILL.md',
'skills/ito-baskets/agents/openai.yaml',
'skills/ito-baskets/scripts/ito-baskets.js',
'skills/ito-compute/SKILL.md',
'skills/ito-compute/agents/openai.yaml',
'skills/ito-inference/SKILL.md',
'skills/ito-training/SKILL.md',
]) {
const installedPath = path.join(cursorRoot, relativePath);
const installedStat = fs.lstatSync(installedPath);
assert.ok(installedStat.isFile(), `packed Itô asset is not a file: ${relativePath}`);
assert.ok(!installedStat.isSymbolicLink(), `packed Itô asset is a symlink: ${relativePath}`);
assert.ok(installedStat.size > 0, `packed Itô asset is empty: ${relativePath}`);
}
const hostileBin = path.join(tempRoot, 'hostile-bin');
const hostileItoSentinel = path.join(tempRoot, 'hostile-ito-spawned');
fs.mkdirSync(hostileBin, { recursive: true });
const hostileIto = path.join(hostileBin, process.platform === 'win32' ? 'ito.cmd' : 'ito');
if (process.platform === 'win32') {
fs.writeFileSync(hostileIto, `@echo hostile>"${hostileItoSentinel}"\r\n`, 'utf8');
} else {
fs.writeFileSync(hostileIto, `#!${process.execPath}\nrequire('fs').writeFileSync(${JSON.stringify(hostileItoSentinel)}, 'spawned');\n`, 'utf8');
fs.chmodSync(hostileIto, 0o755);
}
const itoStatus = runCli(['ito', 'status'], {
expectedStatus: 1,
env: {
...environment,
PATH: `${hostileBin}${path.delimiter}${environment.PATH || environment.Path || ''}`,
ITO_API_KEY: 'must-not-reach-hostile-path',
},
});
assert.match(itoStatus.stderr, /canonical ito-compute-cli is unpublished/i);
assert.doesNotMatch(itoStatus.stderr, /npx|npm exec|npm link|install -g/i);
assert.ok(!fs.existsSync(hostileItoSentinel), 'packed Itô bridge executed a PATH collision');
const managedSnapshot = getManagedOperationSnapshot(initialState, cursorRoot);
assert.ok(managedSnapshot.length > 0, 'initial install must create managed Cursor files');
parseJsonOutput(
runCli(itoInstallArgs),
'repeat install'
);
const repeatState = JSON.parse(fs.readFileSync(statePath, 'utf8'));
assert.deepStrictEqual(
getOperationLedger(repeatState),
initialLedger,
'repeat install must preserve the complete ownership ledger'
);
for (const entry of managedSnapshot) {
assert.ok(fs.existsSync(entry.path), `repeat install lost managed path: ${entry.path}`);
}
assert.strictEqual(
fs.readFileSync(sentinelPath, 'utf8'),
'keep this user file\n',
'repeat install must preserve user-owned files'
);
const statusAfterInstall = parseJsonOutput(
runCli(['status', '--json']),
'status after install'
);
assert.strictEqual(statusAfterInstall.installHealth.status, 'healthy');
assert.strictEqual(statusAfterInstall.installHealth.totalCount, 1);
assert.strictEqual(statusAfterInstall.installStateProjection.status, 'ok');
assert.strictEqual(statusAfterInstall.installStateProjection.warningCount, 0);
assert.strictEqual(statusAfterInstall.readiness.status, 'ok');
const healthyBeforeDrift = parseJsonOutput(
runCli(['doctor', '--target', 'cursor', '--json']),
'doctor before drift'
);
assert.strictEqual(healthyBeforeDrift.summary.errorCount, 0);
assert.strictEqual(healthyBeforeDrift.summary.warningCount, 0);
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
const driftPath = findDriftCandidate(state, cursorRoot);
fs.appendFileSync(driftPath, '\nECC_PACKED_LIFECYCLE_DRIFT\n', 'utf8');
const driftedDoctor = parseJsonOutput(
runCli(['doctor', '--target', 'cursor', '--json'], { expectedStatus: 1 }),
'doctor after drift'
);
assert.ok(
driftedDoctor.summary.errorCount + driftedDoctor.summary.warningCount > 0,
'doctor must detect induced managed-file drift'
);
const repair = parseJsonOutput(
runCli(['repair', '--target', 'cursor', '--json']),
'repair'
);
assert.ok(repair.summary.repairedCount > 0, 'repair must restore the drifted managed file');
const healthyAfterRepair = parseJsonOutput(
runCli(['doctor', '--target', 'cursor', '--json']),
'doctor after repair'
);
assert.strictEqual(healthyAfterRepair.summary.errorCount, 0);
assert.strictEqual(healthyAfterRepair.summary.warningCount, 0);
const statusAfterRepair = parseJsonOutput(
runCli(['status', '--json']),
'status after repair'
);
assert.strictEqual(statusAfterRepair.installHealth.status, 'healthy');
assert.strictEqual(statusAfterRepair.installHealth.totalCount, 1);
assert.strictEqual(statusAfterRepair.installStateProjection.status, 'ok');
assert.strictEqual(statusAfterRepair.installStateProjection.warningCount, 0);
assert.strictEqual(statusAfterRepair.readiness.status, 'ok');
parseJsonOutput(
runCli(['uninstall', '--target', 'cursor', '--json']),
'uninstall'
);
assert.ok(!fs.existsSync(statePath), 'uninstall must remove Cursor install-state');
for (const entry of managedSnapshot) {
assert.ok(!fs.existsSync(entry.path), `uninstall left managed path behind: ${entry.path}`);
}
assert.strictEqual(
fs.readFileSync(sentinelPath, 'utf8'),
'keep this user file\n',
'uninstall must preserve user-owned files'
);
const statusAfterUninstall = parseJsonOutput(
runCli(['status', '--json']),
'status after uninstall'
);
assert.strictEqual(statusAfterUninstall.installHealth.status, 'missing');
assert.strictEqual(statusAfterUninstall.installHealth.totalCount, 0);
assert.strictEqual(statusAfterUninstall.installStateProjection.status, 'ok');
assert.strictEqual(statusAfterUninstall.installStateProjection.warningCount, 0);
assert.strictEqual(statusAfterUninstall.readiness.status, 'ok');
return {
packageSha256: options.expectedSha256,
platform: process.platform,
node: process.version,
lifecycle: [
'npm-install',
'public-ecc-universal-setup',
'cursor-ito-install',
'public-ecc-ito-fail-closed',
'cursor-repeat-install',
'doctor-clean',
'status-installed',
'doctor-drift',
'repair',
'doctor-repaired',
'status-repaired',
'uninstall',
'status-uninstalled',
'sentinel-preserved',
],
};
} finally {
try {
fs.rmSync(tempRoot, {
recursive: true,
force: true,
maxRetries: 10,
retryDelay: 100,
});
} catch (cleanupError) {
process.stderr.write(
`Could not remove lifecycle temp root ${tempRoot}: ${cleanupError.message}\n`
);
}
}
}
function main() {
try {
const report = runLifecycle(parseEnvironment());
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
} catch (error) {
process.stderr.write(`Packed-artifact lifecycle failed: ${error.message}\n`);
process.exitCode = 1;
}
}
module.exports = {
assertDownloadedArtifact,
assertHash,
createLifecycleEnvironment,
getNpmExecInvocation,
hashFile,
parseEnvironment,
runLifecycle,
};
if (require.main === module) {
main();
}